一、多個事務並發時可能遇到的問題
- Lost Update 更新丟失
a. 第一類更新丟失,回滾覆蓋:撤消一個事務時,在該事務內的寫操作要回滾,把其它已提交的事務寫入的數據覆蓋了。
b. 第二類更新丟失,提交覆蓋:提交一個事務時,寫操作依賴於事務內讀到的數據,讀發生在其他事務提交前,寫發生在其他事務提交后,把其他已提交的事務寫入的數據覆蓋了。這是不可重復讀的特例。 - Dirty Read 臟讀:一個事務讀到了另一個未提交的事務寫的數據。
- Non-Repeatable Read 不可重復讀:一個事務中兩次讀同一行數據,可是這兩次讀到的數據不一樣。
- Phantom Read 幻讀:一個事務中兩次查詢,但第二次查詢比第一次查詢多了或少了幾行或幾列數據。
兩類更新丟失的舉例:
| 時間 | 取款事務A | 轉賬事務B |
|---|---|---|
| T1 | 開始事務 | |
| T2 | 開始事務 | |
| T3 | 讀余額為1000 | |
| T4 | 取出100,余額改為900 | - |
| T5 | 讀余額為1000 | |
| T6 | 匯入100,余額改為1100 | |
| T7 | 提交事務,余額定為1100 | |
| T8 | 撤銷事務,余額改回1000 | - |
| T9 | 最終余額1000,更新丟失 | - |
寫操作沒加“持續-X鎖”,沒能阻止事務B寫,發生了回滾覆蓋。
| 時間 | 轉賬事務A | 取款事務B |
|---|---|---|
| T1 | 開始事務 | |
| T2 | 開始事務 | |
| T3 | 讀余額為1000 | |
| T4 | 讀余額為1000 | |
| T5 | 取出100,余額改為900 | |
| T6 | 提交事務,余額定為900 | |
| T7 | 匯入100,余額改為1100 | - |
| T8 | 提交事務,余額定為1100 | - |
| T9 | 最終余額1100,更新丟失 | - |
寫操作加了“持續-X鎖”,讀操作加了“臨時-S鎖”,沒能阻止事務B寫,發生了提交覆蓋。
二、事務隔離級別
為了解決多個事務並發會引發的問題,進行並發控制。數據庫系統提供了四種事務隔離級別供用戶選擇。
- Read Uncommitted 讀未提交:不允許第一類更新丟失。允許臟讀,不隔離事務。
- Read Committed 讀已提交:不允許臟讀,允許不可重復讀。
- Repeatable Read 可重復讀:不允許不可重復讀。但可能出現幻讀。
- Serializable 串行化:所有的增刪改查串行執行。
讀未提交
事務讀不阻塞其他事務讀和寫,事務寫阻塞其他事務寫但不阻塞讀。
可以通過寫操作加“持續-X鎖”實現。
讀已提交
事務讀不會阻塞其他事務讀和寫,事務寫會阻塞其他事務讀和寫。
可以通過寫操作加“持續-X”鎖,讀操作加“臨時-S鎖”實現。
可重復讀
事務讀會阻塞其他事務事務寫但不阻塞讀,事務寫會阻塞其他事務讀和寫。
可以通過寫操作加“持續-X”鎖,讀操作加“持續-S鎖”實現。
串行化
“行級鎖”做不到,需使用“表級鎖”。
可串行化
如果一個並行調度的結果等價於某一個串行調度的結果,那么這個並行調度是可串行化的。
區分事務隔離級別是為了解決臟讀、不可重復讀和幻讀三個問題的。
| 事務隔離級別 | 回滾覆蓋 | 臟讀 | 不可重復讀 | 提交覆蓋 | 幻讀 |
|---|---|---|---|---|---|
| 讀未提交 | x | 可能發生 | 可能發生 | 可能發生 | 可能發生 |
| 讀已提交 | x | x | 可能發生 | 可能發生 | 可能發生 |
| 可重復讀 | x | x | x | x | 可能發生 |
| 串行化 | x | x | x | x | x |
三、常用的解決方案
這里羅列的技術有些是數據庫系統已經實現,有些需要開發者自主完成。
1. 版本檢查
在數據庫中保留“版本”字段,跟隨數據同時讀寫,以此判斷數據版本。版本可能是時間戳或狀態字段。
下例中的 WHERE 子句就實現了簡單的版本檢查:
UPDATE table SET status = 1 WHERE id=1 AND status = 0;
版本檢查能夠作為“樂觀鎖”,解決更新丟失的問題。
2. 鎖
2.1 共享鎖與排它鎖
共享鎖(Shared locks, S-locks)
基本鎖類型之一。加共享鎖的對象只允許被當前事務和其他事務讀。也稱讀鎖。
能給未加鎖和添加了S鎖的對象添加S鎖。對象可以接受添加多把S鎖。
排它鎖(Exclusive locks, X-locks)
基本鎖類型之一。加排它鎖的對象只允許被當前事務讀和寫。也稱獨占鎖,寫鎖。
只能給未加鎖的對象添加X鎖。對象只能接受一把X鎖。加X鎖的對象不能再加任何鎖。
更新鎖(Update locks, U-locks)
鎖類型之一。引入它是因為多數數據庫在實現加X鎖時是執行了如下流程:先加S鎖,添加成功后嘗試更換為X鎖。這時如果有兩個事務同時加了S鎖,嘗試換X鎖,就會發生死鎖。因此增加U鎖,U鎖代表有更新意向,只允許有一個事務拿到U鎖,該事務在發生寫后U鎖變X鎖,未寫時看做S鎖。
目前好像只在 MySQL 里看到了U鎖。
2.2 臨時鎖與持續鎖
鎖的時效性。指明了加鎖生效期是到當前語句結束還是當前事務結束。
2.3 表級鎖與行級鎖
鎖的粒度。指明了加鎖的對象是當前表還是當前行。
2.4 悲觀鎖與樂觀鎖
這兩種鎖的說法,主要是對“是否真正在數據庫層面加鎖”進行討論。
悲觀鎖(Pessimistic Locking)
悲觀鎖假定當前事務操縱數據資源時,肯定還會有其他事務同時訪問該數據資源,為了避免當前事務的操作受到干擾,先鎖定資源。悲觀鎖需使用數據庫的鎖機制實現,如使用行級排他鎖或表級排它鎖。
盡管悲觀鎖能夠防止丟失更新和不可重復讀這類問題,但是它非常影響並發性能,因此應該謹慎使用。
樂觀鎖(Optimistic Locking)
樂觀鎖假定當前事務操縱數據資源時,不會有其他事務同時訪問該數據資源,因此不在數據庫層次上的鎖定。樂觀鎖使用由程序邏輯控制的技術來避免可能出現的並發問題。
唯一能夠同時保持高並發和高可伸縮性的方法就是使用帶版本檢查的樂觀鎖。
樂觀鎖不能解決臟讀的問題,因此仍需要數據庫至少啟用“讀已提交”的事務隔離級別。
3. 三級加鎖協議
稱之為協議,是指在使用它的時候,所有的事務都必須遵循該規則!!!
一級加鎖協議
事務在修改數據前必須加X鎖,直到事務結束(提交或終止)才可釋放;如果僅僅是讀數據,不需要加鎖。
如下例:
SELECT xxx FOR UPDATE; UPDATE xxx;
二級加鎖協議
滿足一級加鎖協議,且事務在讀取數據之前必須先加S鎖,讀完后即可釋放S鎖。
三級加鎖協議
滿足一級加鎖協議,且事務在讀取數據之前必須先加S鎖,直到事務結束才釋放。
4. 兩段鎖協議(2-phase locking)
加鎖階段:事務在讀數據前加S鎖,寫數據前加X鎖,加鎖不成功則等待。
解鎖階段:一旦開始釋放鎖,就不允許再加鎖了。
若並發執行的所有事務均遵守兩段鎖協議,則對這些事務的任何並發調度策略都是可串行化的。
遵循兩段鎖協議的事務調度處理的結果是可串行化的充分條件,但是可串行化並不一定遵循兩段鎖協議。
兩段鎖協議和防止死鎖的一次封鎖法的異同之處
一次封鎖法要求每個事務必須一次將所有要使用的數據全部加鎖,否則就不能繼續執行,因此一次封鎖法遵守兩段鎖協議;但是兩段鎖協議並不要求事務必須一次將所有要使用的數據全部加鎖,因此遵守兩段鎖協議的事務可能發生死鎖。
四、不同的事務隔離級別與其對應可選擇的加鎖協議
| 事務隔離級別 | 加鎖協議 |
|---|---|
| 讀未提交 | 一級加鎖協議 |
| 讀已提交 | 二級加鎖協議 |
| 可重復讀 | 三級加鎖協議 |
| 串行化 | 兩段鎖協議 |
封鎖協議和隔離級別並不是嚴格對應的。
作者:傅易君
鏈接:https://www.jianshu.com/p/71a79d838443
來源:簡書
