【轉載】MySQL:多個事務更新同一行數據時,通過加行鎖避免髒寫的
引入
-
多個事務併發運行的時候,如果同時要讀寫一批數據,此時讀和寫事件的關係需要協調好,否則可能會有髒讀、不可重複讀、幻讀等一系列問題
-
簡單來說,髒讀、不可重複讀、幻讀,都是別人在更新數據的時候,你怎麼讀的問題,讀的不對,那就有問題 ,讀的方法對了,那就不存在問題了。
-
那怎麼協調呢?這就要靠基於undo log版本鏈條以及ReadView實現的MVCC機制了
-
如果有多個事務同時併發更新一行數據的時候,會有髒寫的問題,而髒寫是絕對不允許的,那麼這個髒寫要靠怎麼防止呢?
解決方法
說白了,就是靠鎖機制,依靠鎖機制讓多個事務更新一行數據的時候串行化,避免同時更新一行數據
- 在MySQL裏,假設有一行數據在那兒不動,此時有一個事務來了要更新這行數據,這個時候它會先看一下,看看這行數據此時有沒有人加鎖
- 一看沒人加鎖,說明它是第一個,這個時候事務就會創建一個鎖,裏面包含了自己的trx_id和等待狀態,然後把鎖跟這行數據關聯在一起。
- 必須明確的是,更新一行數據必須把它所在的數據頁從磁盤文件裏讀取到緩存頁裏來才能更新的。所以說,此時這行數據和關聯的鎖數據結構,都是在內存裏的。如下圖:
- 如上圖,因爲事務A給那行數據加了鎖,所以此時就可以說那行數據已經被加鎖了。
- 那麼既然被加鎖了,此時就不能再讓別人訪問了。
- 假如此時另外一個事務B過來了,這個事務B也想更新那行數據,此時就會檢查一下,當前這行數據有沒有別人加鎖。
- 事務B就會發現有別的事務搶先給這行數據加鎖了,那麼怎麼辦呢?
- 事務B這個時候就會也生成一個鎖,然後等着排隊。這個鎖數據結構,裏面有事務B的trx_id,還有自己的等待狀態,但是因爲它是在排隊等待,所以它的等待狀態就是true了,意思是當前正在等待這個鎖。
- 接着事務A這個時候也更新完了數據,就會把自己的鎖給釋放調了。鎖一旦釋放了,它就會去找,此時還有沒有別人也對這行數據加鎖了呢?它會發現事務B也加鎖了
- 於是這個時候,就會把事務B的鎖的等待狀態修改爲false,然後喚醒事務B繼續執行,此時事務B就獲取到鎖了。如下圖:
上述就是MySQL中鎖機制的一個最基本的原理
如何理解?
從上面可以看出,它鎖住了一行。所以也叫做行鎖。
什麼是行鎖
- 行鎖,就是針對數據表中行記錄的鎖。比如事務A更新了一行,而這時候事務B也要更新同一行,則必須等待事務A的操作完成之後才能進行更新
- 行鎖是鎖住單個記錄的鎖,防止其他事務對其update、delete的操作,在RR和RC隔離級別中都支持。
- 行鎖是通過鎖住索引來實現的
- 在多個事務併發更新數據的時候,都是要在行級別加獨佔鎖的,這就是行鎖。
特點
- 對一行數據加鎖
- 開銷大
- 加鎖慢
- 會出現死鎖
- 鎖粒度小,發生鎖衝突概率最低,併發性高
- 行鎖是通過對索引上的索引項加鎖來實現的
分類
行鎖可以分爲兩種:
共享鎖
- 讀鎖(
read lock
),也叫共享鎖(shared lock
)- 允許一個事務去讀一行,阻止其他事務獲得相同數據集的排他鎖
- 一個事務給一個數據行加共享鎖時,必須先獲得表的 意向共享鎖(
IS
)
排他鎖
- 寫鎖(
write lock
),也叫排他鎖(exclusive lock
)- 允許獲得排他鎖的事務更新數據,阻止其他事務取得相同數據集的共享鎖和排他鎖(只允許獲取寫鎖的線程操作,其他線程的任何操作都不能進行)
- 一個事務給一個數據行加排他鎖時,必須先獲得該表的意向排它鎖(
IX
)
注意:
MySQL
InnoDB
引擎默認修改語句:update
、delete
、insert
都會自動給涉及到的數據加上排他鎖,select
語句默認不會加任何鎖類型,如果加排他鎖可以使用select …for update
語句,加共享鎖可以使用select … lock in share mode
語句。- 所以通過加排他鎖的數據行在其他事務中是不能修改數據的,也不能通過
for update
和lock in shared mode
鎖的方式查詢數據,但是可以直接通過select … from …
查詢數據,因爲普通查詢沒有任何鎖數據 - InnoDB的行鎖是通過對索引加的鎖,如果不通過索引條件檢索數據,那麼
InnoDB
將對錶中的所有記錄加鎖,此時就會升級爲表鎖
理解
多個事務併發更新同一行數據時,它加的是什麼鎖呢?
- 加的是獨佔鎖:當有一個事務加了獨佔鎖之後,此時其他事務再要更新這行數據,都是要加獨佔鎖的,但是隻能生成獨佔鎖在後面等待。
已經加了行鎖了(正在更新中),如果有另一個事務此時要求寫,會是什麼樣呢?
- 會阻塞,直到被喚醒
已經加了行鎖了(正在更新中),如果有另一個事務此時要求讀,會是什麼樣呢?
如果數據更新的時候,別的事務去讀取這行數據,有兩種可能:
- 第一種可能是基於MVCC機制進行事務隔離,讀取快照版本,這是比較常見的
- 第二種可能是查詢的同時基於特殊語法去加獨佔鎖或者共享鎖
- 如果你查詢的時候加獨佔鎖,那麼跟其他更新數據的事務加的獨佔鎖都是互斥的;
- 如果你查詢的時候加共享鎖,那麼跟其他查詢加的共享鎖不是互斥的,但是跟其他事務更新數據就加的獨佔鎖是互斥的,跟其他查詢加的獨佔鎖也是互斥的。
也就是說,如果已經加了獨佔鎖了,如果有另一個事務要求讀,這個讀的事務:
- 不能顯式加獨佔鎖
- 不能顯式加共享鎖
如果顯示加了,那麼這個讀事務就只能阻塞了
如果你先加了共享鎖,然後別人來更新要加獨佔鎖行嗎?
- 不行
- 共享鎖和獨佔鎖是互斥的。那個事務只能等待
如果你先加了共享鎖,然後別人也能加共享鎖嗎?
- 可以
- 共享鎖和共享鎖之間不是互斥的
問題是:當有人在更新數據的時候,其他的事務可以讀取這行數據嗎?默認情況下需要加鎖嗎?
答案是:可以讀取,而且不需要加鎖。
- 因爲默認情況下,有人在更新數據,然後其他事務去讀取這行數據,直接默認是開啓
MVCC
機制的 - 也就是說,此時對一行數據的讀和寫兩個操作默認是不會加鎖互斥的,因爲
MySQL
設計MVCC
機制就是爲了解決這個問題,避免頻繁加鎖互斥 - 此時你讀取數據,完全可以根據你的
ReadView
,去在undo log
版本鏈條中找一個你能讀取的版本,完全不用去顧慮別人在不在更新 - 就算你真的等他更新完畢了還提交了,基於
MVCC
機制你也讀不到它更新的值。因爲ReadView
機制是不允許的。所以默認情況下的讀,完全不需要加鎖,不需要去關心其他事務的更新加鎖問題,直接基於MVCC
機制讀某個快照就可以了
如何上鎖
(1)隱式上鎖(默認,自動加鎖自動釋放)
select //不會上鎖
insert、update、delete //上寫鎖
12
(2)顯式上鎖(手動)
select * from tableName lock in share mode;//讀鎖
select * from tableName for update;//寫鎖
12
(3)解鎖(手動)
1. 提交事務(commit)
2. 回滾事務(rollback)
3. kill 阻塞進程
123
爲什麼上了寫鎖,別的事務還可以讀操作?
因爲InnoDB
有MVCC
機制(多版本併發控制),可以使用快照讀,而不會被阻塞。
使用建議
不過一般在開發業務系統的時候,查詢主動加共享鎖很少見。一般不會在數據庫層面做複雜的手動加鎖操作,而是會基於redis
的分佈式鎖來控制業務系統的加鎖邏輯。
另外,查詢的時候也能加互斥鎖,語法是:select * from table for update
。
- 意思是,我查出來數據以後還要更新,此時我加獨佔鎖了,其他事務都不得更新這個數據了
- 一旦你查詢的時候加了獨佔鎖,此時在你事務提交之前,任何人都不能更新數據了,只能你在本事務裏更新數據,等你提交了,別人再更新數據
行鎖的注意點
- 只有通過索引條件檢索數據時,
InnoDB
纔會使用行鎖,否則會使用表鎖(索引失效,行鎖變表鎖) - 即使是訪問不同行的記錄,如果使用的是相同的索引鍵,會發生鎖衝突
- 如果數據表建有多個索引時,可以通過不同的索引鎖定不同的行
行鎖的三種算法
MySQL
的InnoDB
存儲引擎支持三種行鎖算法:
record lock
(記錄鎖):單個記錄上的鎖gap lock
(間隙鎖):鎖定一個範圍,但是不包含記錄本身next-key lock
(record lock + gap lock
):鎖定一個範圍,並且鎖定記錄本身
record lock(記錄鎖)
record lock
:單個行記錄上的鎖- 如果
InnoDB
在建表的時候沒有設置索引,那麼會使用隱式的主鍵來進行鎖定 InnoDB
的行鎖是通過對索引加的鎖,如果不通過索引條件檢索數據,那麼InnoDB
將對錶中的所有記錄加鎖,此時就會升級爲表鎖。
gap lock(間隙鎖)
爲什麼要間隙鎖?
- 我們說
MySQL
在REPEATABLE READ
隔離級別下是可以解決幻讀問題的,解決方案有兩種,可以使用MVCC
方案解決,也可以採用 加鎖 方案解決。 - 但是加鎖時有個大問題,那就是事務在第一次執行讀取操作時,哪些幻影記錄尚不存在,我們無法給這些幻影記錄加上記錄鎖。因此,引入了間隙鎖,
比如我們需要把number
值爲 8
的那條記錄加一個 gap
鎖 的示意圖如下:
如圖中爲 number
值爲 8 的記錄加了 gap
鎖 ,意味着不允許別的事務在 number
值爲 8
的記錄前邊的 間隙插入新記錄,其實就是 number
列的值 (3, 8) 這個區間的新記錄是不允許立即插入的。比方說有另外一個事務再想插入一條 number
值爲 4 的新記錄,它定位到該條新記錄的下一條記錄的 number
值爲8,而這條記錄上又有一個 gap
鎖 ,所以就會阻塞插入操作,直到擁有這個 gap
鎖 的事務提交了之後, number
列的值在區間 (3, 8) 中的新記錄纔可以被插入。
這個 gap
鎖 的提出僅僅是爲了防止插入幻影記錄而提出的,雖然有 共享gap
鎖 和 獨佔gap
鎖 這樣的說法,但是它們起到的作用都是相同的。而且如果你對一條記錄加了 gap
鎖 (不論是 共享gap
鎖 還是 獨佔gap
鎖 ),並不會限制其他事務對這條記錄加 正經記錄鎖 或者繼續加 gap
鎖 ,gap
鎖 的作用僅僅是爲了防止插入幻影記錄的而已。
但是此時有一個問題,我們如何給最後一條記錄的後面的間隙加鎖了,這就要用到 數據頁 的知識了。數據頁其實是有兩條僞記錄的:
Infimum
記錄,表示該頁面中最小的記錄。Supremum
記錄,表示該頁面中最大的記錄
爲了實現阻止其他事務插入 number
值在 (20, +∞) 這個區間的新記錄,我們可以給索引中的最後一條記錄,也就是 number
值爲 20
的那條記錄所在頁面的 Supremum
記錄加上一個 gap
鎖 ,畫個圖就是這樣:
這樣就可以阻止其他事務插入 number
值在 (20, +∞
) 這個區間的新記錄
小結:
- 當我們用範圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,
InnoDB
會給符合條件的已有數據記錄的索引加鎖,對於鍵值在條件範圍內但並不存在的記錄。- 正常等值條件 並且值存在的情況下加的是行鎖
- 如果等值條件 值不存在的情況下加的是間隙鎖,或者範圍查詢,加的也是間隙鎖
- 優點:解決了事務併發的幻讀問題
- 不足:
- 因爲
query
執行過程中通過範圍查找的話,他會鎖定爭個範圍內所有的索引鍵值,即使這個鍵值並不存在。 - 間隙鎖有一個致命的弱點,就是當鎖定一個範圍鍵值之後,即使某些不存在的鍵值也會被無辜的鎖定,而造成鎖定的時候無法插入鎖定鍵值範圍內任何數據。在某些場景下這可能會對性能造成很大的危害。
- 因爲
小結:間隙鎖是鎖住記錄之間的間隙,防止其他事務在某個間隙進行insert
的操作,產生幻讀。在RR
隔離級別中都支持。如圖所示:
Next-key Lock (臨鍵鎖)
- 臨鍵鎖是行鎖和間隙鎖的組合,同時鎖住數據和數據之間的間隙,在
RR
的隔離級別中支持。如圖所示:
- 默認情況下,
InnoDB
在RR
的事務隔離級別運行,InnoDB
會使用next-key lock
鎖進行搜索和索引掃描,以防止幻讀。- 索引上的等值查詢(唯一索引),給不存在的記錄加鎖時,優化爲間隙鎖。
- 索引上的等值查詢(普通索引),向右遍歷時最後一個值不滿足查詢需求時,next-key lock退化爲間隙鎖。
- 索引上的範圍查詢(唯一索引)–會訪問到不滿足條件的第一個值爲止。
注意:間隙鎖唯一目的是防止其他事務插入間隙。間隙鎖可以共存,一個事務採用的間隙鎖不會阻止另一個事務在同一間隙上採用間隙鎖。
小結
- 一個事務更新,多個事務讀取,是使用
MVCC
機制實現的,避免了髒讀 - 多個事務更新同一行數據,是通過加行記錄鎖實現的,避免了髒寫
- 多個事務更新不同行數據,是通過加臨鍵鎖來解決幻讀問題的