前言
前一段時間我在找工作的時候,經(jīng)常會被問到事務(wù)以及事務(wù)的隔離級別等問題,正好最近在公司對事務(wù)隔離級別的原理做了一次技術(shù)分享,今天把它整理出來分享給大家。如果大家覺得有不對的地方呢,可以在評論區(qū)指出。
事務(wù)
一、什么是事務(wù)
事務(wù),即一組數(shù)據(jù)庫操作的集合,這組操作要么全部執(zhí)行成功,任意一個操作失敗那么所有操作全部回滾。
二、事務(wù)的特性
事務(wù)有四大特性,即原子性,一致性,隔離性,持久性。這四個特性通常稱為ACID特性。
-
原子性(Atomicity):一個事務(wù)是一個不可分割的單位。一個事務(wù)中的操作要么全部執(zhí)行,要么全不執(zhí)行。
例如一個賬戶A向另一個賬戶B轉(zhuǎn)賬,A賬戶的余額減少,B賬戶的余額就要增加,兩個操作一定同時成功或者同時失敗。
-
一致性(Consistency):事務(wù)使數(shù)據(jù)由一個狀態(tài)變?yōu)榱硪粋€狀態(tài),數(shù)據(jù)的完整性保持穩(wěn)定。
例如轉(zhuǎn)賬操作中,A賬戶轉(zhuǎn)賬給B賬戶,A賬戶減少的金額與B賬戶增加的金額必須是相同的。
-
隔離性(Isolation):一個事務(wù)的執(zhí)行不能被其他事務(wù)干擾。即一個事務(wù)的內(nèi)部操作及使用的數(shù)據(jù)對并發(fā)的其他事務(wù)是隔離的,并發(fā)執(zhí)行的各個事務(wù)之間不能互相干擾。
事務(wù)互相干擾就會造成數(shù)據(jù)不一致,隔離性的最終目的是為了保證一致性。
-
持久性(Durability):一個事務(wù)一旦提交,它對數(shù)據(jù)庫中的數(shù)據(jù)的改變應(yīng)是永久的。
只要事務(wù)提交成功,對數(shù)據(jù)庫的修改就保存下來了,不會因為任何原因再回到修改前的狀態(tài)。
三、事務(wù)的狀態(tài)
事務(wù)共有五種狀態(tài):
- 活動狀態(tài)
事務(wù)在執(zhí)行時所處的狀態(tài)稱為活動狀態(tài)。 - 部分提交狀態(tài)
事務(wù)中的最后一個操作被執(zhí)行后的狀態(tài)叫做部分提交狀態(tài)。此時變更還未刷新到磁盤上。 - 失敗狀態(tài)
事務(wù)不能正常執(zhí)行的狀態(tài)叫做失敗狀態(tài)。 - 提交狀態(tài)
事務(wù)在經(jīng)過部分提交狀態(tài)后,將所有的變更數(shù)據(jù)寫入磁盤,寫完后進(jìn)入提交狀態(tài),標(biāo)志著事務(wù)成功執(zhí)行完成。 - 中止?fàn)顟B(tài)
事務(wù)執(zhí)行失敗,數(shù)據(jù)回滾至事務(wù)執(zhí)行前,此時事務(wù)所處的狀態(tài)叫做中止?fàn)顟B(tài)。
四、事務(wù)的隔離級別
事務(wù)具有隔離性,隔離性的最終目的是為了保證數(shù)據(jù)一致性。而實現(xiàn)事務(wù)隔離性最簡單的辦法就是讓事務(wù)串行執(zhí)行,類似線程的串行執(zhí)行,但這種方式效率低下,性能較差。所以在保證隔離性的前提下,為了不犧牲性能,事務(wù)能夠支持多種隔離級別。
我們先來看一下當(dāng)多個事務(wù)同時處理同一批數(shù)據(jù)時,如果沒有采取有效的隔離機制,會發(fā)生哪些問題。
- 丟失修改
事務(wù)1和事務(wù)2對同一數(shù)據(jù)進(jìn)行修改,其中事務(wù)2提交的數(shù)據(jù)結(jié)果破壞了事務(wù)1提交的結(jié)果,導(dǎo)致事務(wù)1的修改失效。例如火車售票系統(tǒng),售票點1(事務(wù)1)查詢車票余票有200張,售票點2(事務(wù)2)查詢車片也有200張;售票點1賣出1張車票,剩余車票199張,寫回數(shù)據(jù)庫;售票點2同樣賣出1張車票,剩余車票199張,寫回數(shù)據(jù)庫;此時共賣出2張車票,應(yīng)剩余198張車票,但庫存仍有199張,售票點1的修改被破壞了。 - 臟讀
事務(wù)1修改某一數(shù)據(jù)時,事務(wù)2讀取該數(shù)據(jù),此時事務(wù)1因某些原因需要撤銷修改,將數(shù)據(jù)回滾,恢復(fù)為修改前,而事務(wù)2讀取的是修改后的數(shù)據(jù),與此時數(shù)據(jù)庫中的該值不一致,事務(wù)2讀取到的數(shù)據(jù)即為臟讀。 - 幻讀
事務(wù)1進(jìn)行兩次讀操作,第一次讀取了n條數(shù)據(jù),此時事務(wù)2對這n條數(shù)據(jù)進(jìn)行了刪除或插入m條數(shù)據(jù),事務(wù)1進(jìn)行第二次讀操作,此時讀到的數(shù)據(jù)為n-m或n+m條,仿佛出現(xiàn)了幻覺,即為幻讀。 - 不可重復(fù)讀
事務(wù)1進(jìn)行兩次讀操作,第一次讀取了某一數(shù)據(jù),此時事務(wù)2對這條數(shù)據(jù)進(jìn)行了修改,事務(wù)1進(jìn)行第二次讀操作,此時讀到的數(shù)據(jù)與第一次讀到數(shù)據(jù)結(jié)果不一致,即為不可重復(fù)讀。
注:幻讀與不可重復(fù)讀有些相似,但幻讀強調(diào)的是數(shù)據(jù)記錄的增加或刪減,不可重復(fù)讀強調(diào)的是數(shù)據(jù)記錄的修改。
產(chǎn)生以上四類情況的主要原因是并發(fā)操作破壞了事務(wù)的隔離性,所以要對事務(wù)進(jìn)行并發(fā)控制,使得一個事務(wù)的執(zhí)行不受其他事務(wù)的干擾,避免造成數(shù)據(jù)不一致。根據(jù)以上造成數(shù)據(jù)不一致的情況不同,數(shù)據(jù)庫也具有不同的處理方式,即事務(wù)隔離級別。事務(wù)的隔離級別越高,能解決的數(shù)據(jù)不一致問題越多,但性能損耗也越大。四種事務(wù)的隔離級別如下。
- 讀未提交(Read uncommitted)
一個事務(wù)可以讀取另一個事務(wù)未提交的修改。是最低的隔離級別。 - 讀已提交(Read committed)
一個事務(wù)只能讀取另一個事務(wù)已提交的修改。該級別可以解決臟讀問題。 - 可重復(fù)讀(Repeatable read)
事務(wù)開始讀取數(shù)據(jù)時不允許其他事務(wù)對這些數(shù)據(jù)進(jìn)行修改。保證了同一事務(wù)多次讀取同樣的記錄結(jié)果一致。該級別可以解決不可重復(fù)讀的問題,但不能完全解決幻讀問題。 - 可串行化(Serializable)
最該級別的事務(wù)隔離級別。強制事務(wù)串行執(zhí)行,可以解決臟讀、不可重復(fù)讀、幻讀問題,但效率低下,通常不使用這種方式。
注:可重復(fù)讀是InnoDB的默認(rèn)事務(wù)隔離級別,且已能夠達(dá)到了SQL標(biāo)準(zhǔn)的可串行化。
MVCC與鎖
一、什么是MVCC
MVCC(Multi Version Concurrency Control),即多版本并發(fā)控制。通過維護(hù)數(shù)據(jù)的歷史版本,解決并發(fā)訪問下的數(shù)據(jù)不一致性。
二、MVCC的原理
1.undo log
undo log是InnoDB的事務(wù)日志。undo log是回滾日志,記錄的是行數(shù)據(jù)的修改記錄,即哪些行被修改成怎樣,提供回滾操作。事務(wù)的操作記錄會被記錄到undo log中,用于事務(wù)進(jìn)行回滾操作。
2.版本鏈
在InnoDB中,每個行記錄都隱藏著兩個字段:
??1)trx_id:事務(wù)id。該字段用于記錄修改當(dāng)前行記錄的事務(wù)的id。
??2)roll_pointer:回滾指針。該字段用于記錄修改當(dāng)前行記錄的undo log地址。
假設(shè)一張學(xué)生分?jǐn)?shù)表t_student_score有id,name,class,score四個字段,此時只有一條記錄,當(dāng)前執(zhí)行插入操作的事務(wù)id為1,則有如下示例圖:
假設(shè)此時有兩個事務(wù)t1、t2,id分別為2、3的事務(wù)對這條記錄進(jìn)行修改,執(zhí)行如下操作:
由于每次數(shù)據(jù)的修改都會在undo log中產(chǎn)生日志記錄下來,且roll_pointer會指向undo log的地址。所以,兩次修改后的日志通過roll_pointer串聯(lián)起來,形成的版本鏈如下圖所示:
如圖示,版本鏈的頭結(jié)點是最新的行記錄,而歷史行記錄由roll_pointer記錄的undo log地址串聯(lián)起來。如果數(shù)據(jù)庫隔離級別為讀未提交,那么讀取版本鏈中最新的數(shù)據(jù)即可;如果數(shù)據(jù)庫隔離級別為可串行化,事務(wù)之間是串行執(zhí)行的,不會發(fā)生數(shù)據(jù)不一致的情況,直接執(zhí)行讀操作即可;如果數(shù)據(jù)庫隔離級別為讀已提交或可重復(fù)讀,那么就需要遍歷整條版本鏈,找到trx_id與當(dāng)前事務(wù)相同的記錄,即需要判斷版本鏈中哪個版本是當(dāng)前事務(wù)可見的。InnoDB通過ReadView實現(xiàn)了這個功能。
ReadView主要包括四個部分:
- m_ids:表示在生成ReadView時當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的id。
- min_trx_id:表示在生成ReadView時當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的最小id。
- max_trx_id:表示在生成ReadView時系統(tǒng)應(yīng)該分配給下一個事務(wù)的id值。
- creator_trx_id:表示生成該ReadView的事務(wù)的id。
有了這個ReadView,這樣在訪問某條記錄時,只需要按照下邊的步驟判斷記錄的某個版本是否可見:
- 如果被訪問版本的trx_id的值與ReadView中的creator_trx_id值相同,意味著當(dāng)前事務(wù)在訪問它自己修改過的記錄,所以該版本可以被當(dāng)前事務(wù)訪問。
- 如果被訪問版本的trx_id的值小于ReadView中的min_trx_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪問。
- 如果被訪問版本的trx_id的值大于或等于ReadView中的max_trx_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView后才開啟,所以該版本不可以被當(dāng)前事務(wù)訪問。
- 如果被訪問版本的trx_id的值在ReadView的min_trx_id和max_trx_id之間,那就需要判斷一下trx_id屬性值是不是在m_ids列表中,如果在,說明創(chuàng)建ReadView時生成該版本的事務(wù)還是活躍的,該版本不可以被訪問;如果不在,說明創(chuàng)建ReadView時生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問。
在MySQL中,讀已提交和可重復(fù)讀兩種隔離級別的一個非常大的區(qū)別就是它們生成ReadView的時機不同。讀已提交在每次讀數(shù)據(jù)前都會生成一個ReadView,這樣可以保證每次都能讀到其他事務(wù)已提交的數(shù)據(jù)。可重復(fù)讀只在第一次讀取數(shù)據(jù)時生成一個ReadView,這樣就能保證后續(xù)讀取的結(jié)果一致。
三、鎖
我們在開發(fā)過程中,常常遇到多個線程同時訪問一個共享資源,往往需要并發(fā)控制來確保執(zhí)行結(jié)果正確,在數(shù)據(jù)庫事務(wù)中同樣如此。事務(wù)也存在并發(fā)訪問,即多個事務(wù)同時訪問數(shù)據(jù)。事務(wù)的并發(fā)訪問一般有三種情況:
1. 讀-讀操作:并發(fā)事務(wù)同時訪問同一行或同一段數(shù)據(jù)記錄。由于事務(wù)都是進(jìn)行讀操作,不會對數(shù)據(jù)造成影響,因此并發(fā)讀操作完全允許。
2. 寫-寫操作:并發(fā)事務(wù)同時修改同一行或同一段數(shù)據(jù)記錄。由于事務(wù)都是進(jìn)行寫操作,極易發(fā)生丟失修改問題,因此要通過加鎖解決,即當(dāng)一個事務(wù)要對某行修改時,首先會給該行加鎖,如果加鎖成功才可以進(jìn)行修改;如果加鎖失敗,就要排隊等待,待加鎖事務(wù)提交或回滾后將鎖釋放。
3. 讀-寫操作:一個事務(wù)進(jìn)行讀操作,另一個事務(wù)進(jìn)行寫操作。這種情況容易產(chǎn)生臟讀、幻讀、不可重復(fù)讀問題。最好的解決方案是讀操作進(jìn)行多版本并發(fā)控制(MVCC),寫操作加鎖。
根據(jù)鎖的作用范圍分類,可以將鎖分為表級鎖和行級鎖。表級鎖作用于數(shù)據(jù)庫表上,粒度較大;行級鎖作用于數(shù)據(jù)行上,粒度較小。
為了實現(xiàn)讀-讀操作不受影響,寫-寫操作、讀-寫操作能夠互相阻塞,MySQL使用了讀寫鎖的思想,實現(xiàn)了共享鎖與排他鎖:
1. 共享鎖(S鎖):用于不更改或不更新數(shù)據(jù)的操作,如SELECT語句。共享鎖可以在同一時刻被多個事務(wù)持有。獲得共享鎖的事務(wù)只能讀取數(shù)據(jù),不能更改數(shù)據(jù)。我們可以通過SELECT ... LOCK IN SHARE MODE手工加共享鎖。需要注意的是如果一個事務(wù)對數(shù)據(jù)加上了共享鎖,其他事務(wù)只能對這部分?jǐn)?shù)據(jù)再加共享鎖,不能加排它鎖。
2. 排他鎖(X鎖):用于修改數(shù)據(jù)操作,如INSERT、UPDATE、DELETE。確保事務(wù)不會同時對同一部分?jǐn)?shù)據(jù)進(jìn)行多重修改,在同一時刻只能被一個事務(wù)持有。排他鎖的加鎖方式有兩種,第一種是自動加鎖,在對數(shù)據(jù)進(jìn)行增刪改時都會默認(rèn)加上一個排他鎖。另一種是手工加鎖,使用SELECT ... FOR UPDATE可以實現(xiàn)手工加排他鎖。
考慮一個場景,事務(wù)t1給某行數(shù)據(jù)加行級共享鎖,讓該行數(shù)據(jù)只能讀不能寫,之后事務(wù)t2申請表級排他鎖,讓整張表的數(shù)據(jù)只能寫不能讀。如果事務(wù)t2的鎖申請成功,那么它可以修改表里的任意一行數(shù)據(jù),這與t1持有的行鎖是沖突的。那么數(shù)據(jù)庫要如何判斷這個沖突呢?
首先需要判斷表是否已被其他事務(wù)加了表鎖;然后需要判斷表中每一行是否加了行鎖。這種判斷方式需要遍歷表中的每一行,效率低下。于是有了意向鎖(I鎖)。
意向鎖可以認(rèn)為是S鎖和X鎖在數(shù)據(jù)表上的標(biāo)識,通過意向鎖可以快速判斷表中是否有記錄被上鎖,從而避免通過遍歷的方式來查看表中有沒有記錄被上鎖,提升加鎖效率。意向鎖是由數(shù)據(jù)庫自己維護(hù)的。當(dāng)我們給一行數(shù)據(jù)加上共享鎖之前,數(shù)據(jù)庫會自動先申請表的意向共享鎖(IS鎖);當(dāng)我們給一行數(shù)據(jù)加上排他鎖之前,數(shù)據(jù)庫會自動先申請表的意向排他鎖(IX鎖)。例如,我們要加表級別的X鎖,首先判斷表上是否有被其他事務(wù)加了表鎖,如果沒有,再檢查是否有意向鎖,此時直接根據(jù)意向鎖就能知道這張表是否有行級別的X鎖或者S鎖,這時候數(shù)據(jù)表里面如果存在行級別的X鎖或者S鎖的,加鎖就會失敗。
四、InnoDB中的鎖
1.表級鎖
InnoDB中的表級鎖主要包括表級別的意向共享鎖(IS鎖),意向排他鎖(IX鎖)以及自增鎖(AUTO-INC鎖)。IX鎖和IS鎖已經(jīng)介紹過了,下面介紹一下自增鎖。
大家都知道,如果我們給某列字段加了AUTO_INCREMENT自增屬性,插入的時候不需要為該字段指定值,系統(tǒng)會自動保證遞增。系統(tǒng)實現(xiàn)這種自動給AUTO_INCREMENT修飾的列遞增賦值的原理主要是兩個:
??1)AUTO-INC鎖:在執(zhí)行插入語句的時先加上表級別的AUTO-INC鎖,插入執(zhí)行完成后立即釋放鎖。如果我們的插入語句在執(zhí)行前無法確定具體要插入多少條記錄,比如INSERT ... SELECT這種插入語句,一般采用AUTO-INC鎖的方式。
??2)輕量級鎖:在插入語句生成AUTO_INCREMENT值時先才獲取這個輕量級鎖,然后在AUTO_INCREMENT值生成之后就釋放輕量級鎖。如果我們的插入語句在執(zhí)行前就可以確定具體要插入多少條記錄,那么一般采用輕量級鎖的方式對AUTO_INCREMENT修飾的列進(jìn)行賦值。這種方式可以避免鎖定表,可以提升插入性能。
2.行級鎖
在了解InnoDB的行級鎖之前,我們先簡單了解一下當(dāng)前讀和快照讀。
1)當(dāng)前讀:即加鎖讀。讀取記錄的最新版本,會加鎖保證其他并發(fā)事務(wù)不能修改當(dāng)前記錄,直至獲取鎖的事務(wù)釋放鎖。使用當(dāng)前讀的操作主要包括:顯示加鎖的讀操作與插入、更新、刪除等寫操作。
2)快照讀:即不加鎖讀。讀取記錄的快照版本而非最新版本,通過MVCC實現(xiàn)。InooDB在可重復(fù)讀隔離級別下,如果不顯示的加LOCK IN SHARE MODE、FOR UPDATE的SELECT操作都屬于快照讀,保證事務(wù)執(zhí)行過程中只有第一次讀之前提交的修改和自己的修改可見,其他的均不可見。
綜上可知,通過MVCC可以解決臟讀、不可重復(fù)讀、幻讀這些讀一致性問題,但是這只是解決了普通SELECTD的數(shù)據(jù)讀取問題,即快照讀的讀取問題。在當(dāng)前讀,即加鎖讀的情況下依然要解決臟讀、不可重復(fù)讀、幻讀問題。這個時候需要在讀取的記錄上加鎖,由于都是在行記錄上加鎖,這些鎖都稱為行級鎖。
InnoDB的行鎖是通過鎖住索引來實現(xiàn)的,如果加鎖查詢時沒有使用索引,會將整個表的聚簇索引鎖住,相當(dāng)于鎖住整個表。根據(jù)鎖定范圍不同,行鎖可分為:
- 記錄鎖(Record Lock):單個行記錄上的鎖。
- 間隙鎖(Gap Lock):鎖定一個范圍,但不包括記錄本身。
- 臨鍵鎖(Next-Key Lock):是記錄鎖和間隙鎖的結(jié)合。鎖定一個范圍,包括記錄本身。是MySQL的默認(rèn)行鎖。
間隙鎖和臨鍵鎖都是用來解決幻讀的。
我們來測試一下在什么情況下會產(chǎn)生鎖。
測試準(zhǔn)備
環(huán)境:數(shù)據(jù)庫MySQL,數(shù)據(jù)庫引擎InnoDB,默認(rèn)的事務(wù)隔離級別(RR)。
庫表:
主鍵索引測試表
CREATE TABLE `t_num_test` (
`id` int NOT NULL AUTO_INCREMENT,
`num` int DEFAULT NULL
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
數(shù)據(jù)初始化
INSERT INTO `t_num_test` VALUES ('1', '200');
INSERT INTO `t_num_test` VALUES ('5', '300');
INSERT INTO `t_num_test` VALUES ('9', '400');
INSERT INTO `t_num_test` VALUES ('13', '500');
普通索引測試表
CREATE TABLE `t_num_normal_test` (
`id` int NOT NULL AUTO_INCREMENT,
`num` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_num` (`num`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
數(shù)據(jù)初始化
INSERT INTO `t_num_normal_test` VALUES ('1', '1');
INSERT INTO `t_num_normal_test` VALUES ('5', '5');
INSERT INTO `t_num_normal_test` VALUES ('9', '9');
INSERT INTO `t_num_normal_test` VALUES ('13', '13');
關(guān)閉事務(wù)自動提交
#查看事務(wù)自動提交是否開啟
SHOW VARIABLES LIKE '%autocommit%';
#關(guān)閉事務(wù)自動提交
SET autocommit = 0;
為了測試方便,我們讓兩個數(shù)據(jù)表的id相同,普通索引測試表的普通索引列的值與id列的值相同。
開始測試之前,我們來看一下兩個數(shù)據(jù)表存在的區(qū)間有哪些。
(-∞,1]
(1,5]
(5,9]
(9,13]
(13,+∞)
因為兩個表的數(shù)據(jù)值基本一致,所以對于主鍵索引測試表來說,上述區(qū)間根據(jù)t_num_test.id列的值生成;對于普通索引測試表來說,上述區(qū)間根據(jù)t_num_normal_test.num列的值生成。
1.記錄鎖測試
??1)主鍵索引
#開啟事務(wù)1
BEGIN;
#事務(wù)1,查詢id = 5的數(shù)據(jù)并加鎖
SELECT * FROM t_num_test WHERE id = 5 FOR UPDATE;
#開啟事務(wù)2,插入?yún)^(qū)間(1,5]
INSERT INTO t_num_test VALUES(2,201);
#正常執(zhí)行
#開啟事務(wù)3,插入?yún)^(qū)間(5,9]
INSERT INTO t_num_test VALUES(6,600);
#正常執(zhí)行
#開啟事務(wù)4,更新鎖定行
UPDATE t_num_test SET num = 10000 WHERE id = 5;
#阻塞
#提交事務(wù)
COMMIT;
根據(jù)上述案例可知,在使用主鍵索引id進(jìn)行精準(zhǔn)查詢,只鎖定一條記錄時,MySQL加的是記錄鎖,不會產(chǎn)生間隙鎖。
注:唯一索引在此場景下作用效果與主鍵索引相同。
??2)普通索引
#開啟事務(wù)1
BEGIN;
#事務(wù)1,查詢id = 5的數(shù)據(jù)并加鎖
SELECT * FROM t_num_normal_test WHERE num = 5 FOR UPDATE;
#開啟事務(wù)2,插入?yún)^(qū)間(1,5]
INSERT INTO t_num_normal_test VALUES(2,2);
#阻塞
#開啟事務(wù)3,插入?yún)^(qū)間(5,9]
INSERT INTO t_num_normal_test VALUES(6,6);
#阻塞
#開啟事務(wù)4,插入?yún)^(qū)間(9,13]
INSERT INTO t_num_normal_test VALUES(10,10);
#正常執(zhí)行
#開啟事務(wù)4,更新鎖定行
UPDATE t_num_normal_test SET num = 10000 WHERE id = 5;
#阻塞
#提交事務(wù)
COMMIT;
不難發(fā)現(xiàn),當(dāng)插入數(shù)據(jù)至區(qū)間(1,5]和區(qū)間(5,9]時事務(wù)發(fā)生阻塞,插入數(shù)據(jù)至區(qū)間(9,13]時事務(wù)正常執(zhí)行了,也就是說在區(qū)間(1,5]和區(qū)間(5,9]產(chǎn)生了間隙鎖;在更新鎖定行時事務(wù)發(fā)生阻塞,在該行產(chǎn)生了記錄鎖。
根據(jù)上述案例可知,在使用普通索引指定num的值查詢時,MySQL在行上加記錄鎖,在該行相鄰區(qū)間加間隙鎖,而記錄鎖與間隙鎖的組合組成了臨鍵鎖,即使用了臨鍵鎖。
2.間隙鎖測試
??1)主鍵索引
#開啟事務(wù)1
BEGIN;
#事務(wù)1,鎖定區(qū)間(5,9]
SELECT * FROM t_num_test WHERE id BETWEEN 5 AND 9 FOR UPDATE;
#開啟事務(wù)2,插入?yún)^(qū)間(1,5]
INSERT INTO t_num_test VALUES(2,201);
#正常執(zhí)行
#開啟事務(wù)3,插入?yún)^(qū)間(5,9]
INSERT INTO t_num_test VALUES(6,600);
#阻塞
#開啟事務(wù)4,插入?yún)^(qū)間(9,13]
INSERT INTO t_num_test VALUES(10,1000);
#正常執(zhí)行
#開啟事務(wù)5,更新id = 5的記錄
UPDATE t_num_test SET num = 10000 WHERE id = 5;
#阻塞
#開啟事務(wù)6,更新id = 9的記錄
UPDATE t_num_test SET num = 10000 WHERE id = 9;
#阻塞
#提交事務(wù)
COMMIT;
測試結(jié)果:當(dāng)給區(qū)間(5,9]加鎖時,向區(qū)間(5,9]插入數(shù)據(jù)的事務(wù)被阻塞,向區(qū)間(1,5],(9,13]插入數(shù)據(jù)的事務(wù)正常執(zhí)行了,所以MySQL在區(qū)間(5,9]上產(chǎn)生了間隙鎖。在向id為5和9的兩條記錄執(zhí)行更新操作時,事務(wù)被阻塞了,說明在兩條數(shù)據(jù)記錄上添加了記錄鎖。
由上可知,根據(jù)主鍵索引鎖定一個區(qū)間時,MySQL會在該區(qū)間添加間隙鎖,在區(qū)間邊界處添加記錄鎖。
注:唯一索引在此場景下作用效果與主鍵索引相同。
如果鎖定不存在的行會發(fā)生什么情況?
#開啟事務(wù)1
BEGIN;
#事務(wù)1,鎖定不存在的數(shù)據(jù)
SELECT * FROM t_num_test WHERE id = 7 FOR UPDATE;
#開啟事務(wù)2,插入?yún)^(qū)間(1,5]
INSERT INTO t_num_test VALUES(2,201);
#正常執(zhí)行
#開啟事務(wù)3,插入?yún)^(qū)間(5,9]
INSERT INTO t_num_test VALUES(6,600);
#阻塞
#開啟事務(wù)4,插入?yún)^(qū)間(9,13]
INSERT INTO t_num_test VALUES(10,1000);
#正常執(zhí)行
#開啟事務(wù)5,更新id = 5的記錄
UPDATE t_num_test SET num = 10000 WHERE id = 5;
#正常執(zhí)行
#開啟事務(wù)6,更新id = 9的記錄
UPDATE t_num_test SET num = 10000 WHERE id = 9;
#正常執(zhí)行
#提交事務(wù)
COMMIT;
測試結(jié)果:在區(qū)間(5,9]產(chǎn)生間隙鎖。
當(dāng)鎖定不存在的數(shù)據(jù)時,會在該數(shù)據(jù)所在區(qū)間產(chǎn)生間隙鎖。
??2)普通索引
?????同記錄鎖普通索引
3.無索引列測試
#開啟事務(wù)1
BEGIN;
#事務(wù)1,無索引列加鎖
SELECT * FROM t_num_test WHERE num = 300 FOR UPDATE;
#開啟事務(wù)2,插入?yún)^(qū)間(1,5]
INSERT INTO t_num_test VALUES(2,201);
#阻塞
#開啟事務(wù)3,插入?yún)^(qū)間(5,9]
INSERT INTO t_num_test VALUES(6,600);
#阻塞
#開啟事務(wù)4,插入?yún)^(qū)間(9,13]
INSERT INTO t_num_test VALUES(10,1000);
#阻塞
#開啟事務(wù)5,插入?yún)^(qū)間(13,+∞)
INSERT INTO t_num_test VALUES(14,2000,'kkk');
#阻塞
#提交事務(wù)
COMMIT;
表鎖。
總結(jié)
對主鍵索引或唯一索引來說,當(dāng)鎖定一條記錄時,會產(chǎn)生記錄鎖;當(dāng)鎖定一個區(qū)間時,會產(chǎn)生間隙鎖和記錄鎖,即臨鍵鎖。
對普通索引來說,會產(chǎn)生臨鍵鎖。
對無索引列來說,會鎖住整張數(shù)據(jù)表。