MySQL 鎖機(jī)制存在的價(jià)值是什么?
我們都知道 MySQL 中有各種各樣的鎖,例如:表鎖、間隙鎖、意向鎖、行鎖等等。但你是否想過:為啥 MySQL 要有鎖機(jī)制的存在,它的存在是為了解決什么問題?今天我們就來聊聊這個(gè)問題。
沒有鎖的串行世界
我們先假設(shè)這樣一個(gè)場(chǎng)景:王五現(xiàn)在賬戶里沒有錢,于是向張三、李四各借 100 元,張三、李四很爽快地答應(yīng)了。如果數(shù)據(jù)庫這時(shí)候是串行的,沒有并發(fā)執(zhí)行的線程,那么其轉(zhuǎn)賬示意圖如下所示。
王五借款串行執(zhí)行 - 示意圖
從上圖可以看到:
- 時(shí)間點(diǎn) 1 - 2 的時(shí)候,數(shù)據(jù)庫處理了張三的轉(zhuǎn)賬請(qǐng)求,讀取到王五的賬戶余額為 0,并將其余額加 100,此時(shí)王五賬戶余額為 100。
- 時(shí)間點(diǎn) 3 - 4 的時(shí)候,數(shù)據(jù)庫處理了李四的轉(zhuǎn)賬請(qǐng)求,讀取到王五的賬戶余額為 100,并將其余額加 100,此時(shí)王五賬戶余額為 200。
- 最后,在時(shí)間點(diǎn) 5 的時(shí)候,王五賬戶余額為 200 元。
可以看到最終王五的賬戶余額是 200 元,轉(zhuǎn)賬是沒問題的。這種數(shù)據(jù)庫訪問方式雖然能保證數(shù)據(jù)一致性,但是每次只能執(zhí)行一個(gè)請(qǐng)求,并發(fā)訪問性能太差。
沒有鎖的并行世界
為了提高數(shù)據(jù)庫的并發(fā)訪問性能,MySQL 其實(shí)是支持多線程并發(fā)執(zhí)行的。我們上面的例子,如果使用多線程并發(fā)處理,其可能存在的一種情況如下圖所示。
這種情況的處理流程可能是這樣的:
- 在時(shí)間點(diǎn) 1 的時(shí)候,線程 A 讀取到王五的賬戶余額為 0。
- 在時(shí)間點(diǎn) 2 的時(shí)候,線程 B 讀取到王五的賬戶余額為 0。
- 在時(shí)間點(diǎn) 3 的時(shí)候,線程 A 將王五賬戶余額加 100,此時(shí)王五賬戶余額為 100。
- 在時(shí)間點(diǎn) 4 的時(shí)候,線程 B 將王五賬戶余額加 100,此時(shí)王五賬戶余額為 100。
- 在時(shí)間點(diǎn) 5/6 的時(shí)候,線程 A、B 都將王五的余額回寫回去,王五賬戶余額為 100。
正常來說,王五最終的賬戶余額應(yīng)該是 200 元,但實(shí)際上王五賬戶余額卻只有 100 元。通過分析上面的轉(zhuǎn)賬示意圖,我們會(huì)發(fā)現(xiàn)問題的關(guān)鍵點(diǎn)在于時(shí)間 4。
在這個(gè)時(shí)間點(diǎn)時(shí),王五的賬戶余額應(yīng)該是 100 元了,但是數(shù)據(jù)庫線程 B 還是以為王五的賬戶余額是 0 元,所以導(dǎo)致了最后的數(shù)據(jù)不一致。那么如何解決數(shù)據(jù)不一致的問題呢?答案就是:鎖機(jī)制。
有鎖的并行世界
實(shí)際上,對(duì)于上述的轉(zhuǎn)賬例子,在 InnoDB 中的處理流程如下圖所示。
- 在時(shí)間點(diǎn) 2 的時(shí)候,線程 A 讀取到王五的賬戶余額為 0。
- 在時(shí)間點(diǎn) 3 的時(shí)候,線程 B 讀取到王五的賬戶余額為 0。
- 在時(shí)間點(diǎn) 4 的時(shí)候,線程 A 將王五賬戶余額加 100,并獲取到鎖,此時(shí)王五賬戶余額為 100。
- 在時(shí)間點(diǎn) 5 的時(shí)候,線程 B 準(zhǔn)備將王五賬戶余額加 100,但此時(shí)發(fā)現(xiàn)王五賬戶被鎖了,于是阻塞等待。
- 在時(shí)間點(diǎn) 6 的時(shí)候,線程 A 提交事務(wù)。
- 在時(shí)間點(diǎn) 7 的時(shí)候,線程 B 重新讀取王五最新的余額為 100 元,并加 100 元,最終在時(shí)間點(diǎn) 8 提交事務(wù)。
在時(shí)間點(diǎn) 4 的時(shí)候,數(shù)據(jù)庫線程 A 對(duì)王五賬號(hào)余額加鎖,告訴其他線程:我正在更新這條數(shù)據(jù),你們其他人不要?jiǎng)印?在時(shí)間點(diǎn) 5 的時(shí)候,當(dāng)數(shù)據(jù)庫線程 B 準(zhǔn)備將對(duì)王五賬號(hào)余額做加 100 操作時(shí),其發(fā)現(xiàn)已經(jīng)有數(shù)據(jù)庫線程 A 在操作了,所以其將線程阻塞了。
等到數(shù)據(jù)庫線程 A 提交事務(wù),釋放鎖之后,數(shù)據(jù)庫線程 B 獲取到對(duì)應(yīng)的鎖。這時(shí)候數(shù)據(jù)庫線程 B 發(fā)現(xiàn)王五賬號(hào)的余額是 100 了,所以就在 100 余額的基礎(chǔ)上做更新,之后提交事務(wù),最終王五賬號(hào)的余額就是 200 元。
提示:該例子只是為了粗略說明 InnoDB 是如何通過鎖解決數(shù)據(jù)一致性問題的,在一些細(xì)節(jié)上大家不必對(duì)于糾結(jié)。例如這個(gè)例子在事務(wù)隔離級(jí)別為 READ COMMIT 的時(shí)候適用,但是在 REPEATABLE READ 隔離級(jí)別時(shí)存在問題。
看到這里,相信大家已經(jīng)明白:鎖的存在就是為了解決并發(fā)訪問下數(shù)據(jù)的不一致問題。而數(shù)據(jù)庫之所以要提供并發(fā)訪問,是為了提高數(shù)據(jù)庫的運(yùn)行效率。