作者:楊恆
背景
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、程序應用