mysql鎖機制要覽+示例講解

作者:楊恆

背景

2、隔離級別

理論

1、read uncommited
可以讀取未提交記錄。此隔離級別,不會使用,忽略
2、read commited
針對當前讀,rc隔離級別保證對讀取到的記錄加鎖(記錄鎖),存在幻讀現象
3、repeatable read
針對當前讀,rr隔離級別保證對讀取到的記錄加鎖(記錄鎖),同時保證對讀取的範圍加鎖,新的滿足查詢條件的記錄不能插入(間隙鎖),不存在幻讀現象
4、seriablizable
從mvcc併發控制退化爲基於鎖的併發控制。不區別快照讀和當前讀,所有的讀操作均爲當前讀,讀加讀鎖(S鎖),寫加寫鎖(X鎖)

隔離級別 髒讀可能性 不可重複讀可能性 幻讀可能性 加鎖讀
read uncommited yes yes yes no
read commited no yes yes no
repeatable read no no yes no
serializable no no no yes

5、rc 與rr對比:
set global transaction isolation level read commited;
set global transaction isolation level repeatable read;

測試

drop table if exists t;
create table t(id int,name varchar(10),key idx_id(id),primary key(name))engine=innodb;
insert into t values(1,‘a’),(3,‘c’),(5,‘e’),(8,‘g’),(11,‘j’);

t1 t2
begin;
select * from t where id=1;
commit;
begin;
update t set name=‘yy’ where id=1;
commit;

3、鎖

理論

基本鎖:共享鎖與排它鎖

mysql允許拿到S鎖的事務讀一行,允許拿到X鎖的事務更新或刪除一行
加了S鎖的記錄,允許其他事務再加S鎖,不允許其他事務再加X鎖;
加了X鎖的記錄,不允許其他事務再加S鎖或X鎖

mysql對外提供加這兩種鎖的語法如下:
加S鎖: select … lock in share mode
加X鎖: select … for update

意向鎖(表級鎖):意向共享鎖(IS鎖)和意向排它鎖(IX鎖)

事務在請求S鎖和X鎖前,需要先獲得對應的IS、IX鎖
意向鎖產生的主要目的是爲了處理行鎖和表鎖之間的衝突,用於表明“某個事務正在某一行上持有了鎖,或者準備去持有鎖”

共享鎖、排它鎖與意向鎖的兼容矩陣(先行後列)

  X IX S IS
X 衝突 衝突 衝突 衝突
IX 衝突 衝突 兼容 兼容
S 衝突 衝突 兼容 兼容
IS 衝突 兼容 兼容 兼容

行鎖

記錄鎖

僅僅鎖住索引記錄的一行。單行索引記錄上加鎖,record lock鎖住的永遠是索引,而非記錄本身,即使該表上沒有任何索引,那麼innodb會在後臺創建一個隱藏的聚簇主鍵索引,那麼鎖住的就是這個隱藏的聚簇主鍵索引。
所以說當一條sql沒有走任何索引時,那麼將會在每一條聚簇索引後面加X鎖,這個類似於表鎖,但原理上和表鎖應該是完全不同的。(Is it true??)

間隙鎖

區間鎖,僅僅鎖住一個索引區間(開區間)
在索引記錄之間的間隙中加鎖,或者是在某一條索引記錄之前或之後加鎖,並不包括該索引記錄本身

next-key鎖

record lock + gap lock,左開右閉區間。默認情況下,innodb使用next-key locks來鎖定記錄。但當查詢的索引含有唯一屬性的時候,next-key lock會進行優化,將其降級爲record lock,即僅鎖住索引本身,不是範圍

插入意向鎖:特殊的間隙鎖

gap lock中存在一種插入意向鎖,在insert操作時產生。在多事務同時寫入不同數據至同一索引間隙的時候,並不需要等待其他事務完成,不會發生鎖等待。
假設有一個記錄索引包含鍵值4和7,不同的事務分別插入5和6,每個事務都會產生一個加在4-7之間的插入意向鎖,獲取在插入行上的排它鎖,但是不會被互相鎖住,因爲數據行並不衝突。

行鎖的兼容矩陣 (先列(如gap)後行的鎖定順序)

  gap insert record next-key
gap 兼容 兼容 兼容 兼容
insert 衝突 兼容 兼容 衝突
record 兼容 兼容 衝突 衝突
next-key 兼容 兼容 衝突 衝突

1、已有的insert鎖不阻止任何準備加的鎖
2、gap、next-key會阻止insert
3、gap和record、next-key不會衝突
4、record和record、next-key之間相互衝突

4、測試

實例一、

t1 t2
begin;
select * from t where id=8 for update;
commit;
begin;
insert into t values(4);
insert into t values(5);
insert into t values(6);
insert into t values(11);
insert into t values(12);
rollback;

drop table if exists t;
create table t(id int,key idx_a(id))engine=innodb;
insert into t values(1),(3),(5),(8),(11);
select * from t;

分析:
因爲innoDB對於行的查詢都是採用了next-key lock的算法,鎖定的不是單個值,而是一個範圍。上面索引值有(1,3,5,8,11),其記錄的gap區間如下:是一個左開右閉的空間(原因是默認主鍵的有序自增的特性,結合後面的例子說明)(-∞,1],(1,3],(3,5],(5,8],(8,11],(11,+∞)
innoDB存儲引擎還會對輔助索引下一個鍵值加上gap lock。

實例二、

t1 t2
begin;
delete from t where id=8;
commit;
begin;
insert into t(id,name) values(6,‘f’);
insert into t(id,name) values(5,‘e1’);
insert into t(id,name) values(8,‘gg’);
insert into t(id,name) values(10,‘p’);
insert into t(id,name) values(11,‘iz’);
insert into t(id,name) values(5,‘cz’);

分析:因爲會話1已經對id=8的記錄加了一個X鎖,由於是RR隔離級別,innodb要防止幻讀需要加gap鎖:即id=5(8的左邊),id=11(8的右邊)之間需要加間隙鎖(gap)。這樣[5,e]和[8,g],[8,g]和[11,j]之間的數據都要被鎖。上面測試已經驗證了這一點,根據索引的有序性,數據按照主鍵name排序,後面寫入的[5,cz] ([5,e]的左邊)和[11,ja] ([11,j]的右邊)不屬於上面的範圍從而可以寫入。

實例三、

t1 t2
begin;
select * from t where id=8 for update;
commit;
begin;
insert into t values(7);
insert into t values(9);
rollback;

drop table if exists t;
create table t(id int primary key)engine=innodb;
insert into t values(1),(3),(5),(8),(11);
select * from t;

分析:
因爲innoDB對於行的查詢都是採用了next-key lock的算法,鎖定的不是單個值,而是一個範圍,按照這個方法和第一個測試結果一樣。但是,當查詢的索引含有唯一屬性的時候,next-key lock會進行優化,將其降級爲record lock,即僅鎖住索引本身,不是範圍。

實例四、

t1 t2
begin;
select * from t where id=15 for update;
commit;
begin;
insert into t(id,name) values(10,‘k’);
insert into t(id,name) values(12,‘k’);
rollback;

drop table if exists t;
create table t(id int,name varchar(10),primary key(id))engine=innodb;
insert into t values(1,‘a’),(3,‘c’),(5,‘e’),(8,‘g’),(11,‘j’);
select * from t;

分析:
通過主鍵或者唯一索引來鎖定不存在的值,也會產生gap鎖定。

5、死鎖

show engine innodb status;

duplicate key error 引發的死鎖

drop table if exists t;
create table t(id int(10) unsigned not null ,
name varchar(20) not null default ‘’,
age int(11) not null default ‘0’ ,
stage int(11) not null default ‘0’ ,
primary key(id),
unique key udx_name(name),
key idx_stage(stage))engine=innodb;

insert into t(id,name,age,stage) values(1,‘yst’,11,8),(2,‘dxj’,7,4),(3,‘lb’,13,7),(4,‘zsq’,5,7),(5,‘lxr’,13,4);
select * from t;
select * from information_schema.innodb_locks;

t1 t2 t3
begin;
insert into t values(6,‘test’,12,3);
rollback;
begin;
insert into t values(6,‘test’,12,3);
OK
begin;
insert into t values(6,‘test’,12,3);
ERROR

死鎖成因
事務t1成功插入記錄,並獲得索引id=6上的排他記錄鎖(lock_x)
緊接着事務t2、t3也開始插入記錄,請求排他插入意向鎖(lock_insert_intention);但由於發生重複唯一鍵衝突,各自請求的排他記錄鎖(lock_x)轉成共享記錄鎖(lock_s)

t1回滾釋放索引id=6上的排他記錄鎖(lock_x),t2和t3都要請求索引id=6上的排他記錄鎖(lock_x)。
由於x鎖和s鎖互斥,t2和t3都等待對方釋放s鎖。
於是,死鎖便產生了。

如果此場景下,只有兩個事務t1與t2或者t1與t3,則不會引發如上死鎖情況發生。

gap與insert intention衝突引發的死鎖

drop table if exists t;
create table t(a int(11) not null, b int(11) default null,primary key(a),key idx_b(b))engine=innodb default charset=utf8;
insert into t(a,b) values(1,2),(2,3),(3,4),(11,55);
select * from t;

t1 t2
begin;
select * from t where b=6 for update;
insert into t values(4,5);
commit;
begin;
select * from t where b=8 for update;
insert into t values(4,5);
commit;

死鎖成因
事務t1執行查詢語句,在索引b=6上加排他next-key鎖(lock_x),會鎖住idx_b索引範圍(4,22)。
事務t2執行查詢語句,在索引b=8上加排他next-key鎖(lock_x),會鎖住idx_b索引範圍(4,22)。
由於請求的gap與已持有的gap是兼容的,因此,事務t2在idx_b索引範圍(4,22)也能加鎖成功。

事務t1執行插入語句,會先加他insert intention鎖。由於請求的insert intention鎖與已有的gap鎖不兼容,則事務t1等待t2釋放gap鎖。
事務t2執行插入語句,也會等待t1釋放gap鎖。於是,死鎖便產生了。

6、程序應用

這裏寫圖片描述
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章