MySQL 事務
文章比較長,建議分段閱讀
后續如果有改動會在 Junebao.top
之前對事務的了解僅限于知道要么全部執行,要么全部不執行,能背出 ACID 和隔離級別,知其然但不知其所以然,現在覺得非常有必要系統學一下,關于事務,關于 LBCC,關于 MVCC,關于死鎖 ……
并發的問題
所謂 事務 是用戶定義的一個 數據庫操作序列, 這些操作要么全做,要么全不做,是一個不可分割的工作單位,在關系型數據庫中,一個事務可以是一條 SQL 語句,一組 SQL 語句或者是整個程序,事務的開始和結束由用戶顯示控制,如果用戶沒有顯式定義事務,則由 DBMS 按默認規定自動劃分事務,如在 MySQL 中默認 autocommit
為 ON
則開啟事務自動提交,每條沒有顯式定義事務的 SQL 語句都會被當作一個單獨的事務并自動提交:
mysql> show session variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set, 1 warning (0.00 sec)
事務有四個特性,即 ACID:
- A(Atomicity): 原子性 ,事務是數據庫的基本工作單位,事務中包含的諸操作要么都做,要么都不做。
- C(Consistency): 一致性,事務的執行結果必須使數據庫從一個一致性狀態轉換到另一個一致性狀態,所謂一致性狀態是指數據庫中因該只包含成功事務執行的結果,如果一個事務在執行過程中被迫中斷,但這個未完成的事務對數據庫的部分修改已經寫入物理數據庫,這時數據庫就處于一種 “不正確” 的狀態,或者稱處于 “不一致” 的狀態。
- I (Isolation): 隔離性,一個事務的執行不能被其他事務所干擾,即一個事務內部操作所使用的數據對其他并發的事務應該是隔離的,所有并發執行的事物之間不能相互干擾。
- D(Durability):持久性,一個事務一旦提交,他對數據的操作就應該是持久的,接下來的其他操作或故障不應該對他有任何影響。
只有保證了事務的 ACID 特性,對數據庫的操作才能是安全的,因此,不管是 LDCC 還是 MVCC,其核心目的都是保證事務的 ACID 特性。可能破壞事務 ACID 特性的因素包括:
- 多個事務并行運行時,不同事物的操作交叉執行。
- 事務執行過程中被強制停止。
上面兩個因素分別對應事務處理的兩類技術:并發控制和數據庫恢復技術,事務被強制終止一般由事物內部故障,系統故障等造成,發生類似故障時,一般采用日志文件恢復等方法使數據庫恢復到上一個一致性狀態,這里著重研究由并發導致的 ACID 被破壞的情況和解決方案。
并發導致的數據不一致包括:丟失修改,臟讀,不可重復讀,幻讀。
丟失修改
如上圖,兩個并發的事務 T1, T2 同時讀表中的某條記錄 total 得到 16, 并且對其進行修改,最終造成后提交的事務 T2 的修改結果覆蓋了先提交的事務 T1 的修改結果,這種現象叫做丟失修改。
針對丟失修改,還有一種情況,就是如果 T2 在修改 total 之后發生異常進行了回滾,就會導致 total 值重置為 16,這種現象被稱為 回滾覆蓋,而第一種情況被稱為提交覆蓋
不可重復讀和幻讀
事務 T1 讀取數據后,T2 對該數據進行了更新操作, 導致 T1 無法再次讀到前一次讀取的結果,這種現象叫做不可重復讀,導致不可重復讀的原因包括下面三種情況:
- T1 讀取某一數據后,T2 對其進行了修改,導致 T1 再次讀該數據時,得到與之前不一致的值。
- T1 讀取某些數據后,T2 刪除了其中的某些數據,當 T1 再次讀這些數據時,發現之前的一些數據神秘消失了。
- T1 讀取某些數據后,T2 向其中插入了一些數據,當 T1 再次讀這些數據時,發現莫名其妙多了一些數據。
后兩種情況也被常常稱為 幻讀, 幻讀與第一種情況的不同在于:
- 幻讀往往在讀取某一范圍的數據時產生。
- 幻讀是因為其他事務執行了插入或刪除語句導致的,但第一種情況一般是執行更新語句導致的。
為了方便,不可重復讀一般只指第一種情況,幻讀指后兩種情況。
臟讀
事務 T1 修改了某條記錄,T2 讀取到了 T1 未提交的這條記錄,但 T1 由于某些原因被回滾了,這就導致了 T2 讀取到的數據與數據庫中的數據不一致,即臟數據。
對比
一致性問題 | 產生過程 | 一致性問題 | 產生原因 |
---|---|---|---|
臟讀 | ![]() | 不可重讀 | ![]() |
幻讀 | ![]() | 丟失修改 | ![]() |
導致上述四種不一致的原因就是破壞了事務的隔離性,進而導致一致性被破壞,而保證數據隔離性的方法就是使用正確的方式調度并發操作, 但有的時候,為了性能,我們有允許犧牲一部分隔離性,比如對有些數據量很大,少量臟數據對結果影響很小或影響可以接收時,我們可以降低一致性要求以減少系統開銷,這就是數據庫的隔離級別。
隔離級別 | 臟讀 | 不可重復讀 | 幻讀 |
---|---|---|---|
讀未提交 Read uncommitted | 可能 | 可能 | 可能 |
讀已提交 Read committed | 可能 | 可能 | |
可重復讀 Repeatable read | 可能 | ||
串行化 Serializable |
并發控制技術
隔離級別只是定義了在不同的級別下應該保證哪些一致性,具體實現這些隔離級別的方法有很多,如傳統的基于鎖的并發控制(LBCC),還有一些無鎖并發控制方案,如時間戳(timestamp), 樂觀控制法(scheduler),多版本并發控制(MVCC)等,我們主要探索基于鎖的并發控制(LBCC)和多版本并發控制(MVCC)
基于鎖的并發控制(LBCC)
所謂封鎖就是事務在某個數據對象進行操作之前先申請鎖,對該對象加鎖后,該事務就擁有了一定的對該對象的控制,在該事務釋放該鎖前,其他事務不能操作此數據對象。
從鎖的模式來看,鎖可以分為共享鎖和排它鎖,共享鎖又稱為讀鎖(S 鎖),排它鎖又稱為寫鎖(X鎖)。
- X 鎖:若事務 T 對數據對象 A 加上了 X 鎖,則只允許 T 讀取和修改 A, 其他任何事務不得再對 A加任何類型的鎖,直到 T 釋放鎖,。
- S 鎖:若事務 T 對數據對象 A 加上了 S 鎖,則 T 和其他事務都可以可以讀 A,同時其他事務可以繼續申請 A 的 S 鎖,但是直到所有事務都釋放 A 的 S 鎖為止(所有事務并不包括自己),A 是不允許修改的。這就意味著如果只有一個事務對 A 添加了 S 鎖,那他自己是可以修改數據的。
封鎖協議
- 一級封鎖協議: 事務 T 在修改數據 R之前必須對其加 X 鎖,直到事務提交或回滾才釋放鎖,一級封鎖協議可以防止丟失修改,一級封鎖協議只在寫數據時加鎖,讀數據時并不需要獲取鎖,所以它無法解決臟讀,幻讀,不可重復讀。
- **二級封鎖協議:**要求在一級封鎖協議的基礎上,讀數據前必須加 S 鎖,讀完即可立刻釋放 S 鎖。因為對數據 A 加 X 鎖后,不允許其他事務再申請其他鎖,所以事務要想獲得 S 鎖讀,就必須等持有 X 鎖的事務寫完提交或回滾,這樣就可以避免臟讀,但由于二級封鎖協議允許讀完后立刻釋放 S 鎖,無法保證下一次讀時數據不被修改,所以所以它不能保證可重復讀(包括幻讀)。
- **三級封鎖協議:**要求在一級封鎖協議的基礎上,讀數據前必須加 S 鎖,直到事務結束才釋放。該協議可以解決不可重復讀(包括幻讀)的問題。
死鎖,活鎖
死鎖和活鎖是使用 LBCC 解決一致性問題時必須考慮的問題:
- 活鎖:如果 T1 封鎖了 R,T2 請求 R 的鎖,這時 T2 應該等待,然后 T3 也請求 R 并等待,這時 T1 釋放了 R 的鎖,但該鎖被 T3 獲得,在這過程中, T4 也請求 R, T3 釋放鎖后鎖又被 T4 獲得…… 這導致 T2 一直無法獲得鎖,這種某個事務陷入饑餓的狀態現象叫做活鎖,避免活鎖的簡單策略是先來先服務。
- 死鎖:如果 T1 封鎖了 R1, T2 封鎖了 R2, 然后 T1 又來申請 R2, T2 又來申請 R1, 這會造成 T1 T2 相互等待永遠無法結束的局面,形成死鎖。
預防死鎖
死鎖出現的原因是兩個事務都已經封鎖了一些數據對象,然后都去請求已經被對方鎖定的數據對象,預防死鎖就是要想辦法破壞死鎖產生的條件,通常使用一次封鎖法和順序封鎖法解決:
- 一次封鎖法:一個事務必須一次性將所有要使用的數據全部加鎖,否則就不能繼續執行。但由于數據庫中的數據并不是一塵不變的,我們往往很難準確知道要用哪寫數據,為了實現一次封鎖,就不得不擴大封鎖范圍,將可能的數據全部加鎖,這會造成系統并發度降低,影響性能。
- 順序封鎖法:預先對數據對象規定一個封鎖順序,所有事物按這個順序實施封鎖,但這樣同樣存在問題:
- 數據庫中的數據對象很多,要維護這么多對象的封鎖順序并不容易。
- 誰應該先被封鎖,誰應該后被封鎖,關于這個順序很難得到一個最優解。
解除死鎖
上面個兩種預防死鎖的辦法在現實中可行性很低,所以DBS通常采用診斷并解除死鎖的辦法解決死鎖問題。
診斷死鎖:
- 超時法:如果一個事務的等待時間超過特定期限,就認為其發生了死鎖。
- 等待圖法:將事務和其等待的數據對象以有向圖的形式組織,檢測圖中是否有回路,發現回路即為死鎖。
解除死鎖:
- 發現死鎖后,我們往往會選擇一個處理死鎖代價最小的事務,將其撤銷以釋放他持有的所有鎖,當然,對撤銷事務的數據修改操作必須加以修復。
可串行化調度和兩段鎖協議
可串行化調度:
- 如果多個事務并發執行的結果和按某一次序串行執行這些事務的結果是一樣的,就說這種調度是可串行化調度,他是并發事務正確調度的準則。
兩段鎖協議(2PL):
- 兩段鎖協議用來保證調度是可串行化的
- 兩段鎖協議是指所有事務必須分兩個階段對數據項進行加鎖和解鎖操作。
- 在對一個數據項進行讀寫操作前,必須申請并獲得該數據項的封鎖。(拓展階段)
- 釋放完一個封鎖后,事務不再去申請或獲取任何其他鎖。(收縮階段)
封鎖粒度
加鎖時,你可以選擇對一個屬性值,關系,索引項,整個索引甚至整個數據庫加鎖,加鎖對象的大小叫做鎖的粒度,一般來說,粒度越大,并發度越小,系統開銷也越小,封鎖粒度越小,并發度越高,系統開銷也就越大。一個 DBS 應該盡可能兼顧并發度和系統開銷,這樣顯然不能只支持某一粒度的封鎖,這種提供多種封鎖粒度供不同事務選擇的封鎖方法叫做多粒度封鎖。
多粒度封鎖可以由多粒度樹描述,如下:
多粒度樹的根節點是整個數據庫,表示最大的數據粒度,葉子節點表示最小的封鎖粒度,多粒度封鎖協議 允許粒度樹的每一個節點可以被單獨加鎖,某一個節點加鎖意味著該節點的所有子節點也被加同樣的鎖,對該節點來說,這個鎖屬于顯式加鎖,對于其子節點來說,屬于隱式加鎖,他們的效果是一樣的。
顯式和隱式加鎖看起來是理所當然的,但這會導致一個問題,在對某一數據項加鎖時,我們必須保證當前要加的這把鎖與其顯示假的鎖不沖突,同時還要保證與其隱式假的鎖不沖突,為此,在加鎖前,我們必須要:
- 檢查數據項有無顯示加鎖,保證不與其沖突。
- 檢查數據項的所有父節點,保證不與其隱式加鎖沖突。
- 檢查數據項的所有子節點,保證加鎖后由于本次加鎖獲得隱式鎖的數據項不與其原來的鎖沖突。
這樣一來,每次加鎖我們不得不遍歷整個粒度樹,這種效率是非常低下的,為此,我們引入了一種新鎖:意向鎖
意向鎖用于提升加鎖效率,無法手動創建,它的含義是如果對一個節點加意向鎖,則說明該節點的下層節點正在被加鎖,對任意節點加鎖時,必須先對它的上層節點加意向鎖。有三種常用的意向鎖,他們分別是:意向共享鎖(IS 鎖),意向排它鎖(IX鎖),共享意向排它鎖(SIX鎖):
- IS 鎖:表示其子節點準備加 S 鎖
- IX 鎖:表示其子節點準備加 X 鎖
- SIX 鎖:如果對一個數據對象加 SIX 鎖,表示對他加 S 鎖,在家 IX 鎖,例如對某表加 SIX 鎖,表示該事務要通過 S 鎖讀整個表,同時還要更新個別元組(IX鎖)。
根據上面的描述,我們可以得出以下的鎖強度偏序關系圖和數據鎖的相容矩陣:
鎖強度表示一個鎖對其他鎖的排斥程度
X | SIX | IX | S | IS | - | |
---|---|---|---|---|---|---|
X | F | F | F | F | F | T |
SIX | F | T | T | F | T | T |
IX | F | T | T | F | F | T |
S | F | F | F | T | T | T |
IS | F | T | T | T | T | T |
- | T | T | T | T | T | T |
規律:對于意向鎖來說使用強度更高的鎖來替換強度低的鎖是安全的。
意向鎖如何提高加鎖效率?
如果某一時刻,數據庫中數據對象的鎖持有情況如下圖粒度樹所示,對表 T1 加了 S 鎖,其父節點對應加了 IS 鎖,T1 的子節點也隱式獲得了 S 鎖(當然所有節點也隱式獲得了 IS 鎖,所有隱式鎖都未畫出)
如果現在我們希望更新記錄 R1,根據封鎖協議,就必須對 R1 添加 X 鎖,對其父節點加 IX 鎖,這時只需要檢查 T1 和 DB 的鎖是否與之不相容,T1 持有 S 鎖,與 X 鎖不相容,調度器會阻止加鎖。
相反,如果我們只是想對 R1 加 S 鎖進行讀操作,就需要先對父節點加 IS 鎖,T1 持有 S 鎖,與 IS 鎖相容,再檢查 R1 持有的鎖是否與 S 鎖相容,相容,允許加鎖。
加入意向鎖后,我們不需要再去遍歷所有子節點便可以判斷能否未數據項加鎖,可以提高系統并發度,減少加鎖解鎖開銷。
多版本并發控制(MVCC)
通過 LBCC, 我們可以解決所有的并發不一致問題,那為什么還會有其他并發控制方案呢?歸根結底還是基于性能的考慮, LBCC 只是實現了允許并發讀,但對于并發讀寫,寫寫操作只能串行執行,在讀寫都很頻繁的場景下,并發性能將大大降低,因此,人們才提出各種無鎖并發控制方案,MVCC 就屬于其中一種。
MVCC 的大概思路是每一個事務都有一個唯一的ID,當某一個事務要修改某行數據時,先將這一行原來的數據做一個快照保存下來,當有其他并發事務也要操作這個事務時,可以操作之前的版本,這樣,最新的版本只被寫事務維持,不會干擾到讀事務,以此實現隔離,MVCC 并沒有一個統一的標準,不同 DBS 的實現也不盡相同,下面以 MySQL InnoDB 引擎為例說一下 MVCC 的一個具體流程。
InnoDB 中的 MVCC
既然是 MVCC, 那最重要的就是舊版本的數據要存在,在 MySQL InnoDB 中,這些數據會以 回滾段 (rollback segment)的形式保存在表空間中,更具體來說,他們會被保存在 undo log 中。
其次,InnoDB 會在所有表中加兩個隱藏列 DB_TRX_ID 和 DB_ROLL_PTR,前者占 6 字節,表示插入或更新該行的最后一個事務的事務標識符;后者占 7 個字節,稱為回滾指針,指向回滾段的 undo log 記錄。
InnoDB 插入的隱藏列還有一個 DB_ROW_ID,會隨著新行的插入會單調遞增,如果使用了默認自增ID的聚簇索引,索引中就會包含這個列。
每當我們插入或更新一行數據(刪除被認為是更新的一種),InnoDB 會為這個事務分配一個唯一單調遞增的事務ID,這個 ID會記錄在這一行的 DB_TRX_ID 中,表示這一行數據的最新版本。
如下圖,設有一張表 t, 包含兩個字段 id 和 name, 它的初始狀態如下:
表示最后插入或修改這條記錄的事務 ID 是 100,現在如果有一個新的事務要修改這條記錄,設其 ID 為 200,則現在表的狀態就會變成下面這樣:
灰色行表示歷史版本,被記錄在 undo log 中,從最新版本的回滾指針可以找到這條記錄的歷史版本,這條鏈表被叫做版本鏈,
除此之外,當一個事務第一次執行讀操作時,會為該事務生成一個一致性視圖 read-view, 這個數據結構包含此刻所有活躍著的(未提交的)寫事務的事務ID列表和此時DBS分配出去的最大事務號,有了 read-view 就可以判斷出哪寫事務是已經提交了的,哪寫事務是未提交的,具體判斷邏輯如下:
從活躍事務列表中找到最小的事務ID,記為 min_t_id, 讀取到某一版本的事務ID記為 row_t_id, 系統分配的最大事務ID 記為 max_t_id
- 如果 row_t_id < min_t_id, 說明這一版本的事務一定被提交了,這一版本可見
- 如果 row_t_id > max_t_id, 說明這一版本的事務還沒有開始,一般不存在這種情況,不可見
- 如果 row_t_id >= min_t_id && row_t_id <= max_t_id, 還需要分兩種情況討論:
- 如果 row_t_id 在活躍事務列表中,則說明該版本是由未提交的事務創建的,不可見,但對于自身事務是可見的。
- 如果 row_t_id 不在活躍事務列表中,說明創建該版本的事務已提交,該版本是可見的。
有了版本鏈和 read-view,一個事務就可以根據 read-view 順著版本鏈依照上面的規則一直往下直到找到一個可見的版本,
以上面的例子為例,如果事務的并發時序圖如下:
當事務二更新操作執行后,版本鏈變為:

事務三第一次執行查詢語句時,生成的 read-view 為:
| active list | max |
+-------------------+--------+
| 200, 300 | 300 |
+-------------------+--------+read view
根據 read-view 得到 max_t_id = 300, min_t_id = 200, 從最新版本開始遍歷,
- row_t_id = 300, 滿足條件3,同時 row_t_id 在 active list 中,說明這一版本未提交,不可見,根據回滾指針檢查下一個版本。
- row_t_id = 200,同樣滿足條件三,在 active list 中,不可見,看下一個版本。
- row_t_id = 100, 滿足條件一,可見,則這個讀事務會使用這一版本的信息。
如下圖,如果在第五時刻,事務 2 提交,第六時刻事務三再次讀取:
版本鏈并沒有發生改變,但如果在 RC 的隔離界別下,事務三的第二條查詢語句會重新生成 read-view, 這時活躍事務只有 200,最大事務為 300,按上面的規則,row_t_id = 300 時,滿足條件三但不在活躍事務列表中,所以這條記錄是可見的,這就會讀出 wangwu, 導致不可重復讀。
但如果使用 InnoDB 默認的 RR 隔離界別,read-view 只會在事務執行第一條查詢語句時生成,后續所有查詢語句使用同一個 read-view, 由此避免不可重復讀。
一些其他問題
InnoDB 中的鎖
在上面我們介紹了 LBCC,這里再簡單介紹一下 InnoDB 中幾個具體的鎖,他們以鎖粒度劃分。
記錄鎖
Record Locks,也叫行鎖,加在索引記錄上的鎖。例如,SELECT c1 FROM t WHERE c1 = 10 For UPDATE
, 防止任何其他事務插入、更新或刪除 t.c1 值為10的行, 行鎖加在索引上而不是記錄上,因為innodb一定會有一個聚簇索引,因此最終的行鎖都會落到聚簇索引上。
間隙鎖
gap Locks, 是對索引記錄之間間隙加的鎖,或者是對第一個索引記錄之前或最后一個索引記錄之后的間隙的鎖。例如:
SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE
這樣可以防止其他事務插入 c1 值在 10 - 20 之間的數據。
這里的間隙可以是兩個索引值之間的間隙,也可以是從無窮到單個索引值之間的間隙
使用唯一索引檢索唯一行時不會使用間隙鎖,但是如果檢索條件僅包含多列唯一索引的一些列時,仍然會使用間隙鎖鎖定,例如一個簡單的 SELECT 語句:
SELECT * FROM child WHERE id = 100;
如果 id 使用唯一索引,則該語句只會加記錄鎖。
但如果 id 未建立索引或建立了非唯一索引,那么從負無窮到100的間隙將會被間隙鎖鎖定。
還有一個需要值得注意的問題,在一個間隙上,不同的事務可以持有相互沖突的鎖,這是因為如果某條記錄被從索引中清除,那我們必須合并由不同事務保存在記錄上的間隙鎖。所以說間隙鎖的唯一目的是為了防止記錄被插入間隙, 一個事務進行的間隙鎖定不會阻止另一事務對相同的間隙進行間隙鎖定。共享和專用間隙鎖之間沒有區別。 它們彼此不沖突,并且執行相同的功能。
間隙鎖定可以顯式禁用。 如果將事務隔離級別更改為READ COMMITTED或啟用innodb_locks_unsafe_for_binlog系統變量(現已棄用),則會發生這種情況。 在這種情況下,將禁用間隙鎖定來進行搜索和索引掃描,并且間隙鎖定僅用于外鍵約束檢查和重復鍵檢查。
使用 READ COMMITTED 隔離級別或啟用innodb_locks_unsafe_for_binlog還有其他效果。MySQL 在計算完 WHERE 條件后,將釋放不匹配行的記錄鎖。 對于 UPDATE 語句,InnoDB 進行“半一致”讀取,以便將最新的提交版本返回給 MySQL,以便MySQL可以確定該行是否與UPDATE的WHERE條件匹配。
間隙鎖總結
- 進行范圍查詢或使用非唯一索引作為檢索條件時會使用間隙鎖。
- 間隙鎖只用于阻止別的事務插入間隙,他不阻止別的間隙鎖鎖定相同內容,在一個間隙上,不同的事務可以持有相互沖突的鎖。
- 間隙鎖只在 RR 隔離界別下起作用,可以手動關閉。
臨鍵鎖
臨鍵鎖是間隙鎖和記錄鎖的結合,臨鍵鎖使得一個會話如果在某個索引記錄上建立了共享或排它鎖,其他會話不能在該索引記錄前面的間隙插入數據。
假設某個表的索引包含值10,11,13,20,則其臨鍵鎖可能包含以下間隔:
其實只需要記住臨鍵鎖鎖定的是一個左開右閉的區間即可
臨鍵鎖總結
- 臨鍵鎖是行鎖和間隙鎖的組合。
- 臨鍵鎖鎖定的是一個左開右閉的區間。
- InnoDB RR 隔離級別下,臨鍵鎖用來解決幻讀。
插入意向鎖
insert intention lock, 是在插入新的記錄之前通過 INSERT 操作設置的一種間隙鎖,該鎖以這樣一種方式發出插入意圖的信號,即如果多個事務要插入的數據在同一間隙內但不是相同的位置,那這些事務就不需要相互等待。比如假設有值為4和7的索引記錄。嘗試分別插入值5和6的單獨事務在獲得插入行上的獨占鎖之前,每個事務都使用插入意圖鎖鎖定4和7之間的間隙,但不會互相阻塞,因為行不沖突。
自增鎖
AUTO-INC鎖是一種特殊的表級鎖,如果一個表中有 AUTO_INCREMENT列,則要插入該表的事務在插入之前會先獲取該鎖,該鎖是表級鎖,但不是事務級鎖,插入語句執行完后就會立刻釋放,不會等待事務提交才釋放。自增鎖也具有不同的模式,可以使用 innodb_autoinc_lock_mode
選項(0, 1, 2)控制自增鎖遞增算法,以謀求效率和安全性的要求.

在默認狀態下,該選項設置為 1,在該模式下:
- “批量插入”使用特殊的AUTO-INC表級鎖并將其保持到語句結束。 這適用于所有INSERT … SELECT,REPLACE … SELECT和LOAD DATA語句。 一次只能執行一條持有AUTO-INC鎖的語句。 如果批量插入操作的源表與目標表不同,則在對源表中選擇的第一行進行共享鎖之后,將對目標表執行AUTO-INC鎖。 如果批量插入操作的源和目標是同一表,則在對所有選定行進行共享鎖之后,將獲取AUTO-INC鎖。
- “簡單插入”(預先知道要插入的行數)通過在互斥量(輕型鎖)的控制下獲得所需數量的自動增量值來避免表級AUTO-INC鎖定 僅在分配過程的整個過程中才保留,直到語句完成為止。 除非另一個事務持有AUTO-INC鎖,否則不使用表級AUTO-INC鎖。 如果另一個事務持有AUTO-INC鎖,則“簡單插入”將等待AUTO-INC鎖,就好像它是“批量插入”一樣。
- 混合模式插入”,如果用戶為多行“簡單插入”中的某些行 (但不是所有行) 的AUTO_INCREMENT列提供顯式值,InnoDB分配的自動增量值會多于要插入的行數。但是,自動分配的所有值都是由最近執行的上一條語句生成的自動增量值連續生成的,因此“多余的”號碼就會丟失。
其他模式可以參考 官方文檔
關于幻讀
網上對幻讀的定義各種各樣,有人把幻讀囊括在不可重復中(比如我們的教材),有人說對某一范圍的數據執行刪除或插入會導致幻讀,有人說只有插入導致的才叫幻讀,實際上在 SQL 92 標準里明確定義了什么是幻讀:
P3 ("Phantom"): SQL-transaction T1 reads the set of rows N that satisfy some <search condition>. SQL-transaction T2 then executes SQL-statements that generate one or more rows that satisfy the <search condition> used by SQL-transaction T1. If SQL-transaction T1 then repeats the initial read with the same <search condition>, it obtains a different collection of rows.
當一個事務 T1 讀到滿足某些條件的行集合后,事務 T2 向表中插入了滿足這些條件的一行或多行數據,如果 T1 使用相同的條件重復讀取,它將得到不同的結果,這叫幻讀,而對于刪除的情況,92 標準也明確說了這屬于不可重復讀。
P2 ("Non-repeatable read"): SQL-transaction T1 reads a row. SQL-transaction T2 then modifies or deletes that row and performs a COMMIT. If T1 then attempts to reread the row, it may receive the modified value or discover that the row has been deleted.
P2(“不可重復讀取”):SQL-Transaction T1讀取一行。然后,SQL-Transaction T2修改或刪除該行并執行提交。如果T1隨后嘗試重新讀取該行,它可能會收到修改后的值或發現該行已被刪除。
在 InnoDB 的文檔中,也可以看見:
Also, a deletion is treated internally as an update where a special bit in the row is set to mark it as deleted.
所以對于 MVCC 解決不可重復讀中刪除情況的問題和修改其實是一樣的。
還有一個問題是 InnoDB 的 RR 隔離級別有沒有解決幻讀的問題,這在文檔里面也清晰的寫了:解決了,解決方式就是上面的臨鍵鎖。
關于丟失修改
上面說到并發導致的問題時提到了兩類丟失修改的問題:提交覆蓋和回滾覆蓋,但似乎在平時說到并發問題時大家只提臟讀幻讀不可重復讀,在 SQL 92 標準里也沒有發現對所謂丟失修改問題的描述,事實上,這兩種情況都是在讀的結果上進行了修改,對于回滾覆蓋,InnoDB 的任何隔離級別下他都不會發生,因為回滾到的是針對數據庫的上一個已提交的版本,而不是針對該事務的,而對于提交覆蓋,我們完全可以在應用程序層面使用諸如 CAS 等技術手段避免這類問題,網上也有使用悲觀鎖或樂觀鎖避免的方案,可以參考:事務的隔離級別以及Mysql事務的使用
關于 InnoDB 中的死鎖
在 LBCC 那一節也簡單說了一下死鎖和活鎖,這里再詳細了解一下 MySQL InnoDB 的死鎖問題。先看一個官方文檔給出的死鎖的例子:
CREATE TABLE t (i INT) ENGINE = InnoDB;
INSERT INTO t (i) VALUES(1);
事務一先對數據加 S 鎖執行讀操作(未提交):
START TRANSACTION;
SELECT * FROM t WHERE i = 1 LOCK IN SHARE MODE;
然后再開啟一個事務二嘗試刪除該記錄:
START TRANSACTION;
DELETE FROM t WHERE i = 1;
由于刪除會對數據加 X 鎖,這與事務一加的 S 鎖并不相容,所以事務二會等待事務一釋放 S 鎖,像下面這樣:
而等待一段時間后,會提示獲取鎖超時并終止事務:
DELETE FROM t WHERE i = 1
> 1205 - Lock wait timeout exceeded; try restarting transaction
> 時間: 51.473s
這樣并不存在死鎖的問題,但如果在事務二等待的期間,事務一執行了對這條記錄的刪除操作:
DELETE FROM t WHERE i = 1;
就會發生死鎖:
DELETE FROM t WHERE i = 1
> 1213 - Deadlock found when trying to get lock; try restarting transaction
> 時間: 12.216s
原因是事務一如果想要刪除就必須要一個 X 鎖,但 X 鎖已經被事務二請求了,不能授予事務一,且由于事務二事先請求 X 鎖,事務一持有的 S 鎖也不能升級為 X 鎖,這就會導致死鎖,出現這個問題時,InnoDB 會回滾一個小事務(事務的大小由插入、更新或刪除的行數決定)并拋出上面的錯誤來解除死鎖。
在高并發系統上,當多個線程等待相同的鎖時,死鎖檢測會導致速度變慢。 所以有時當發生死鎖時,禁用死鎖檢測而依靠innodb_lock_wait_timeout
設置進行事務回滾可能會更有效。 可以使用 innodb_deadlock_detect
配置選項禁用死鎖檢測。
InnoDB
使用自動行級鎖定。即使在僅插入或刪除單行的事務中,也可能會遇到死鎖。這是因為這些操作并不是真正的“原子”操作;它們會自動對插入或刪除的行的(可能是多個)索引記錄設置鎖定。您可以使用以下技術來處理死鎖并減少發生死鎖的可能性:
使用
SHOW ENGINE INNODB STATUS
命令以確定最近死鎖的原因。這可以幫助您調整應用程序以避免死鎖。如果頻繁出現死鎖警告引,請通過啟用
innodb_print_all_deadlocks
配置選項來收集更廣泛的調試信息 。有關每個死鎖的信息,而不僅僅是最新的死鎖,都記錄在MySQL error log 中。完成調試后,請禁用此選項。如果事務由于死鎖而失敗,在任何時候,請重試一遍,死鎖并不可怕。
請保持插入或更新事務足夠小,避免鎖被一個事務長時間占用,以此減少沖突概率。
進行一系列相關更改后立即提交事務,以減少沖突的發生。特別是,不要長時間未提交事務而使交互式 mysql會話保持打開狀態。
如果您使用鎖定讀取(
SELECT ... FOR UPDATE
或SELECT ... LOCK IN SHARE MODE
),請嘗試使用較低的隔離級別,例如READ COMMITTED
。在事務中修改處于多個表或同一表中的不同行集時,每次都要以一致的順序去執行這些操作。這樣事務會形成定義明確的隊列而不會導致死鎖。例如,將數據庫操作組織到應用程序內的函數中,而不是在不同位置編碼多個類似的INSERT,UPDATE和DELETE語句序列。
對表中的數據建立合適索引,這樣您的查詢將會使用更少的索引記錄,同樣也會使用更少的鎖。使用
EXPLAIN SELECT
以確定MySQL認為哪些索引最適合您的查詢。如果可以,請盡量少的使用鎖,以允許
SELECT
從一個舊的快照返回數據,不要添加條款FOR UPDATE
或LOCK IN SHARE MODE
給它。在READ COMMITTED
這里使用隔離級別是件好事,因為同一事務中的每個一致性讀取均從其自己的新快照讀取。如果沒有其他辦法,可以使用表級鎖序列化事務。對事務表(例如InnoDB表)使用LOCK TABLES的正確方法是:
SET autocommit = 0
(notSTART TRANSACTION
)后跟來開始事務,直到明確提交事務后才對LOCK TABLES
調用UNLOCK TABLES
。例如,如果您需要寫表t1
和從表中讀取數據t2
,則可以執行以下操作:SET autocommit=0; LOCK TABLES t1 WRITE, t2 READ, ...; ... do something with tables t1 and t2 here ... COMMIT; UNLOCK TABLES;
表級鎖可防止對表的并發更新,從而避免死鎖,但代價是對繁忙系統的響應速度較慢。
序列化事務的另一種方法是創建一個僅包含一行的輔助“信號量”表。 在訪問其他表之前,讓每個事務更新該行。 這樣,所有事務都以串行方式發生。 注意,在這種情況下,InnoDB即時死鎖檢測算法也適用,因為序列化鎖是行級鎖。 對于MySQL表級鎖,必須使用超時方法來解決死鎖。
參考
MySQL 官方文檔 innodb-multi-versioning
美團技術文章 - Innodb中的事務隔離級別和鎖的關系
解決死鎖之路 - 學習事務與隔離級別