1. 行鎖
InnoDB存儲(chǔ)引擎實(shí)現(xiàn)了如下兩種標(biāo)準(zhǔn)的行級(jí)鎖:
?共享鎖(S Lock),允許事務(wù)讀一行數(shù)據(jù)。
?排他鎖(X Lock),允許事務(wù)刪除或更新一行數(shù)據(jù)。
如果一個(gè)事務(wù)T1已經(jīng)獲得了行r的共享鎖,那么另外的事務(wù)T2可以立即獲得行r的共享鎖,因?yàn)樽x取并沒(méi)有改變行r的數(shù)據(jù),稱這種情況為鎖兼容(Lock Compatible)。但若有其他的事務(wù)T3想獲得行r的排他鎖,則其必須等待事務(wù)T1、T2釋放行r上的共享鎖——這種情況稱為鎖不兼容。下表顯示了共享鎖和排他鎖的兼容性。

對(duì)于insert、update、delete,InnoDB會(huì)自動(dòng)給涉及的數(shù)據(jù)加排他鎖(X);對(duì)于一般的Select語(yǔ)句,InnoDB不會(huì)加任何鎖,事務(wù)可以通過(guò)以下語(yǔ)句給顯示加共享鎖或排他鎖。
1.1 共享鎖
select * from table_name where .....lock in share mode`
事務(wù)1: select * from table_1 where id=1 lock in share mode;
事務(wù)2: select * from table_1 where id=1 lock in share mode;
事務(wù)1: update table_1 set age=10 where id=1;
事務(wù)1: 事務(wù)1更新時(shí)發(fā)現(xiàn)此行鎖被其他事務(wù)享用,等待
事務(wù)2: update table_1 set age=12 where id=1;
事務(wù)2: 事務(wù)2更新時(shí)發(fā)現(xiàn)此行鎖被其他事務(wù)享用,也等待,導(dǎo)致死鎖
1.2 排他鎖
select * from table_name where .....for update
事務(wù)1: select * from table_1 where id=1 for update;
事務(wù)2: select * from table_1 where id=1 for update;
事務(wù)2: 等待...
事務(wù)1: update table_1 set age=10 where id=1;
事務(wù)1: 更新完后釋放鎖
事務(wù)2: 獲得鎖后,得到其他事務(wù)提交的記錄
1.3 行鎖的特性
- innodb 的行鎖是在有索引的情況下,沒(méi)有索引的表是鎖定全表的。下面是一個(gè)示例,其中id是主鍵:
| id | name |
|---|---|
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
事務(wù)1update第一條id=1的數(shù)據(jù),事務(wù)不提交;事務(wù)2接著update第二條id=2的數(shù)據(jù)的時(shí)候等待,原因是id沒(méi)有加上索引,導(dǎo)致事務(wù)1鎖的是表鎖而不是行鎖。
2. 意向鎖
InnoDB存儲(chǔ)引擎支持多粒度(granular)鎖定,這種鎖定允許事務(wù)在行級(jí)上的鎖和表級(jí)上的鎖同時(shí)存在。為了支持在不同粒度上進(jìn)行加鎖操作,InnoDB存儲(chǔ)引擎支持一種額外的鎖方式,稱之為意向鎖(Intention Lock)。意向鎖是將鎖定的對(duì)象分為多個(gè)層次,意向鎖意味著事務(wù)希望在更細(xì)粒度(fine granularity)上進(jìn)行加鎖,如圖所示。

若將上鎖的對(duì)象看成一棵樹,那么對(duì)最下層的對(duì)象上鎖,也就是對(duì)最細(xì)粒度的對(duì)象進(jìn)行上鎖,那么首先需要對(duì)粗粒度的對(duì)象上鎖。例如圖6-3,如果需要對(duì)頁(yè)上的記錄r進(jìn)行上X鎖,那么分別需要對(duì)數(shù)據(jù)庫(kù)A、表、頁(yè)上意向鎖IX,最后對(duì)記錄r上X鎖。若其中任何一個(gè)部分導(dǎo)致等待,那么該操作需要等待粗粒度鎖的完成。舉例來(lái)說(shuō),在對(duì)記錄r加X鎖之前,已經(jīng)有事務(wù)對(duì)表1進(jìn)行了S表鎖,那么表1上已存在S鎖,之后事務(wù)需要對(duì)記錄r在表1上加上IX,由于不兼容,所以該事務(wù)需要等待表鎖操作的完成。
InnoDB存儲(chǔ)引擎支持意向鎖設(shè)計(jì)比較簡(jiǎn)練,其意向鎖即為表級(jí)別的鎖。設(shè)計(jì)目的主要是為了在一個(gè)事務(wù)中揭示下一行將被請(qǐng)求的鎖類型。其支持兩種意向鎖:
1)意向共享鎖(IS Lock),事務(wù)想要獲得一張表中某幾行的共享鎖
2)意向排他鎖(IX Lock),事務(wù)想要獲得一張表中某幾行的排他鎖
由于InnoDB存儲(chǔ)引擎支持的是行級(jí)別的鎖,因此意向鎖其實(shí)不會(huì)阻塞除全表掃以外的任何請(qǐng)求。故表級(jí)意向鎖與行級(jí)鎖的兼容性如表所示。

3.快照讀
快照讀又稱一致性非鎖定讀。即讀取到的數(shù)據(jù)始終是快照數(shù)據(jù),而不是最新數(shù)據(jù)。
一致性非鎖定讀是指InnoDB存儲(chǔ)引擎通過(guò)多版本并發(fā)控制技術(shù)來(lái)讀取當(dāng)前數(shù)據(jù)庫(kù)的數(shù)據(jù)。如果當(dāng)前讀取的行正在執(zhí)行delete或者update操作,這時(shí)讀取操作不會(huì)等行鎖的釋放,而是去讀取行的快照數(shù)據(jù)。
快照數(shù)據(jù)是指改行之前版本的數(shù)據(jù),該實(shí)現(xiàn)是通過(guò)undo段來(lái)實(shí)現(xiàn)的,而undo段用來(lái)在事務(wù)中保存回滾數(shù)據(jù),因此使用快照沒(méi)有增加額外的開銷。 這是InnoDB存儲(chǔ)引擎的默認(rèn)讀取方式,即默認(rèn)的事務(wù)隔離級(jí)別為REPEATABLE READ下的讀取方式。由于讀到的不是最新數(shù)據(jù),某些情況下可能會(huì)有問(wèn)題。
3.1 注意
不同的事務(wù)隔離級(jí)別下讀取的方式不同,并不是每個(gè)事務(wù)隔離級(jí)別下都是采用非鎖定的一致性讀。四種隔離級(jí)別中,READ COMMITTED和REPEATABLE READ這兩種隔離級(jí)別使用非鎖定的一致性讀。
-
不同的事務(wù)即使都使用非鎖定的一致性讀,但是對(duì)于快照數(shù)據(jù)的定義也各不相同
READ COMMITTED級(jí)別下非鎖定讀總是讀取鎖定行的最新一份快照數(shù)據(jù),即事務(wù)開始后不止一個(gè)快照,有很多版本的數(shù)據(jù),取的是最新的數(shù)據(jù)。
REPEATABLE READ級(jí)別下非鎖定讀總是讀取事務(wù)開始后第一次執(zhí)行select操作的數(shù)據(jù)版本(重點(diǎn)在select操作在哪里調(diào)用,取的就是哪個(gè)時(shí)間的數(shù)據(jù))。
3.2 例子
| 事務(wù)A | 事務(wù)B |
|---|---|
| select * from table where id='1'; | |
| . | update table set id =3 where id=1; |
| select * from table where id='1'; | |
| . | commit; |
| select * from table where id='1'; |
- 上述例子中事務(wù)B update以后,事務(wù)A第一次select的時(shí)候RC級(jí)別和RR級(jí)別獲取的結(jié)果都是id=1的那一條數(shù)據(jù);第二次select的時(shí)候,由于事務(wù)B已經(jīng)提交,RC級(jí)別select的結(jié)果就是事務(wù)Bcommit后更新的最新數(shù)據(jù):id=3,而RR級(jí)別讀取的是事務(wù)A開始時(shí)的數(shù)據(jù),id=1。
4. 當(dāng)前讀
當(dāng)前讀又稱一致性鎖定讀,即不管什么時(shí)候讀,讀到的數(shù)據(jù)都是最新的數(shù)據(jù),而不是事務(wù)開始時(shí)的快照數(shù)據(jù)。
默認(rèn)配置下事務(wù)的隔離級(jí)別為REPEATABLE READ,select操作為一致性非鎖定讀,但某些情況下需要對(duì)數(shù)據(jù)庫(kù)讀取操作進(jìn)行加鎖保證數(shù)據(jù)的一致性。select 有兩種一致的鎖定讀:
select ... for update
select ... lock in share mode
5. 自增長(zhǎng)與鎖
InnoDB存儲(chǔ)引擎內(nèi)部對(duì)每個(gè)含有自增長(zhǎng)列的表有一個(gè)自增長(zhǎng)計(jì)數(shù)器,當(dāng)進(jìn)行insert操作時(shí),首先獲取計(jì)數(shù)器的最大值,加1后進(jìn)行insert操作。這個(gè)操作會(huì)加一個(gè)特殊的表鎖,AUTO-INC LOCK。這個(gè)鎖并不是在事務(wù)提交后才釋放,而是在insert語(yǔ)句執(zhí)行完后釋放。
5.1 缺點(diǎn)
雖然是insert后就釋放鎖,不是事務(wù)提交后才釋放,但是必須等前一個(gè)insert的完成才能進(jìn)行下一次insert,性能較差。
5.2 改進(jìn)
InnoDB存儲(chǔ)引擎中提供了一種輕量級(jí)互斥量的自增長(zhǎng)實(shí)現(xiàn)機(jī)制,大大提高了自增長(zhǎng)值插入的性能。
6. 外鍵與鎖
在對(duì)外鍵值進(jìn)行update和insert操作時(shí)首先需要查詢父表的記錄,即select父表,這個(gè)select操作不是使用一致性非鎖定讀,因?yàn)闀?huì)發(fā)生數(shù)據(jù)不一致的問(wèn)題,為此需要使用一致性鎖定讀,這時(shí)使用的select ... lock in share mode方式。當(dāng)父表對(duì)應(yīng)記錄加X鎖后,子表的操作將會(huì)阻塞。
7. 鎖的算法
InnoDB存儲(chǔ)引擎有三種行鎖的算法
Record Lock:單行記錄上鎖
Gap Lock:間隙鎖,鎖定一個(gè)范圍,但不包含記錄本身
Next-Key Lock:相當(dāng)于Record Lock+Gap Lock,鎖定一個(gè)范圍,并且鎖定記錄本身
7.1 示例
如果一個(gè)索引有10,11,13,20這四個(gè)值,那么該索引可能被Next-Key Lock分為如下幾個(gè)區(qū)間:
(-∞,10],
(10,11],
(11,13],
(13,20],
(20,+∞)
除了Next-Key Lock外,還有previous-key lock技術(shù),可鎖定的范圍為:
(-∞,10),
[10,11),
[11,13),
[13,20),
[20,+∞)
然而,當(dāng)索引為唯一索引時(shí),InnoDB存儲(chǔ)引擎會(huì)對(duì)Next-key Lock進(jìn)行優(yōu)化,將其降級(jí)為Record Lock,即僅鎖住索引本身,而不是范圍。示例如下:
| 事務(wù)A | 事務(wù)B |
|---|---|
| Begin | |
| select * from t where a=5 for update; | |
| Begin | |
| Insert into t select 4; | |
| Commit; |
該示例中a字段先設(shè)置為唯一索引,當(dāng)事務(wù)a執(zhí)行for update后,事務(wù)b插入4的時(shí)候,由于鎖定的是5這一行,而不是范圍,所以事務(wù)B不會(huì)阻塞。
問(wèn)題一:什么時(shí)候會(huì)用到next-key鎖?
- 在出現(xiàn)幻讀的時(shí)候,為解決該問(wèn)題,需要鎖定一個(gè)范圍,而不是固定一行,這個(gè)時(shí)候就會(huì)用到next-key鎖了。
- mysql四個(gè)事務(wù)隔離級(jí)別下,READ UNCOMMITTED和READ COMMITTED這兩個(gè)事務(wù)隔離級(jí)別是不支持next-key鎖的;REPEATABLE READ 和 SERIALZABLE這兩種事務(wù)隔離級(jí)別是支持next-key鎖的。
問(wèn)題二:Next-key鎖只存在于查詢條件為區(qū)間的時(shí)候嗎?
- 答案不是的,如果查詢條件指定了某一行,但是查詢條件的字段是非唯一索引或不是索引的當(dāng)前讀下,也會(huì)使用next-key鎖。
8. 鎖問(wèn)題
8.1 臟讀
- 臟讀(Dirty Reads):一個(gè)事務(wù)正在對(duì)一條記錄做修改,在這個(gè)事務(wù)并提交前,這條記錄的數(shù)據(jù)就處于不一致狀態(tài);這時(shí),另一個(gè)事務(wù)也來(lái)讀取同一條記錄,如果不加控制,第二個(gè)事務(wù)讀取了這些“臟”的數(shù)據(jù),并據(jù)此做進(jìn)一步的處理,就會(huì)產(chǎn)生未提交的數(shù)據(jù)依賴關(guān)系。這種現(xiàn)象被形象地叫做“臟讀”。
- 當(dāng)事務(wù)的隔離級(jí)別為READ UNCOMMITTED會(huì)產(chǎn)生臟讀現(xiàn)象,其他隔離級(jí)別則不會(huì)。
8.2 不可重復(fù)讀
- 不可重復(fù)讀是指在一個(gè)事務(wù)內(nèi)多次讀取同一數(shù)據(jù)集合。在這個(gè)事務(wù)還沒(méi)有結(jié)束時(shí),另外一個(gè)事務(wù)也訪問(wèn)該同一數(shù)據(jù)集合,并做了一些DML操作。因此,在第一個(gè)事務(wù)中的兩次讀數(shù)據(jù)之間,由于第二個(gè)事務(wù)的修改,那么第一個(gè)事務(wù)兩次讀到的數(shù)據(jù)可能是不一樣的。這樣就發(fā)生了在一個(gè)事務(wù)內(nèi)兩次讀到的數(shù)據(jù)是不一樣的情況,這種情況稱為不可重復(fù)讀。
- 不可重復(fù)讀和臟讀的區(qū)別是:臟讀是讀到未提交的數(shù)據(jù),而不可重復(fù)讀讀到的卻是已經(jīng)提交的數(shù)據(jù),但是其違反了數(shù)據(jù)庫(kù)事務(wù)一致性的要求。
- 事務(wù)隔離級(jí)別為READ UNCOMMITTED和READ COMMITTED會(huì)產(chǎn)生不可重復(fù)讀現(xiàn)象,為解決此問(wèn)題,可將事務(wù)隔離級(jí)別設(shè)置為REPEATABLE READ,該級(jí)別默認(rèn)實(shí)現(xiàn)為Next-key Lock鎖技術(shù),避免了不可重復(fù)讀的現(xiàn)象。當(dāng)然光設(shè)置事務(wù)隔離級(jí)別為REPEATABLE READ還不行,還得使用
select … for update或者select … lock in share mode語(yǔ)句才行。
8.3 幻讀
- 幻讀指的是一個(gè)事務(wù)按相同的查詢條件重新讀取以前檢索過(guò)的數(shù)據(jù),卻發(fā)現(xiàn)其他事務(wù)插入了滿足其查詢條件的新數(shù)據(jù),這種現(xiàn)象就稱為“幻讀”。
- 幻讀和不可重復(fù)讀的區(qū)別在于不可重復(fù)讀注重的是同一條記錄前后多次讀取出現(xiàn)結(jié)果不一致的問(wèn)題,而幻讀注重的是范圍內(nèi)增加了以前沒(méi)有的新數(shù)據(jù)而導(dǎo)致總的記錄數(shù)發(fā)生變化的問(wèn)題。
8.4 丟失更新
當(dāng)兩個(gè)或多個(gè)事務(wù)選擇同一行,然后基于最初選定的值更新該行時(shí),由于每個(gè)事務(wù)都不知道其他事務(wù)的存在,就會(huì)發(fā)生丟失更新問(wèn)題——最后的更新覆蓋了其他事務(wù)所做的更新。
比如:
| 事務(wù)A | 事務(wù)B |
|---|---|
| Begin | Begin |
| select * from t where a=5; | |
| ... | select * from t where a= 5; |
| update t set b='1' where a=5; | ... |
| update t set b='2' where a=5; |
上述兩個(gè)事務(wù)互相不知道對(duì)方也在同時(shí)執(zhí)行,并且都先進(jìn)行了select操作,后進(jìn)行了update操作,但是最后事務(wù)B的結(jié)果覆蓋了事務(wù)A的結(jié)果,即丟失了A更新的結(jié)果。為避免這個(gè)問(wèn)題應(yīng)該在事務(wù)隔離級(jí)別采用REPEATABLE READ的前提上,select 查詢語(yǔ)句都加上for update就可以了。
9. 阻塞
因?yàn)椴煌i之間的兼容性關(guān)系,在有些時(shí)刻一個(gè)事務(wù)中的鎖需要等待另一個(gè)事務(wù)中的鎖釋放它所占用的資源,這就是阻塞。阻塞并不是一件壞事,其是為了確保事務(wù)可以并發(fā)且正常地運(yùn)行。
在InnoDB存儲(chǔ)引擎中:
- 參數(shù)innodb_lock_wait_timeout用來(lái)控制等待的時(shí)間(默認(rèn)是50秒)
- innodb_rollback_on_timeout用來(lái)設(shè)定是否在等待超時(shí)時(shí)對(duì)進(jìn)行中的事務(wù)進(jìn)行回滾操作(默認(rèn)是OFF,代表不回滾)。
參數(shù)innodb_lock_wait_timeout是動(dòng)態(tài)的,可以在MySQL數(shù)據(jù)庫(kù)運(yùn)行時(shí)進(jìn)行調(diào)整,而innodb_rollback_on_timeout是靜態(tài)的,不可在啟動(dòng)時(shí)進(jìn)行修改。