事務併發的可能問題與其解決方案 -----資料庫的隔離級別和 悲觀鎖、樂觀鎖
一、多個事務併發時可能遇到的問題
- 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鎖。
目前好像只在 MSSQL 裡看到了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鎖,加鎖不成功則等待。
解鎖階段:一旦開始釋放鎖,就不允許再加鎖了。
若併發執行的所有事務均遵守兩段鎖協議,則對這些事務的任何併發排程策略都是可序列化的。
遵循兩段鎖協議的事務排程處理的結果是可序列化的充分條件,但是可序列化並不一定遵循兩段鎖協議。
兩段鎖協議和防止死鎖的一次封鎖法的異同之處
一次封鎖法要求每個事務必須一次將所有要使用的資料全部加鎖,否則就不能繼續執行,因此一次封鎖法遵守兩段鎖協議;但是兩段鎖協議並不要求事務必須一次將所有要使用的資料全部加鎖,因此遵守兩段鎖協議的事務可能發生死鎖。
四、不同的事務隔離級別與其對應可選擇的加鎖協議
事務隔離級別 | 加鎖協議 |
---|---|
讀未提交 | 一級加鎖協議 |
讀已提交 | 二級加鎖協議 |
可重複讀 | 三級加鎖協議 |
序列化 | 兩段鎖協議 |
封鎖協議和隔離級別並不是嚴格對應的。
理解“事務隔離級別-加鎖的選擇-三級加鎖協議”之間的聯絡,著實花了不少功夫。
作者:傅易A
連結:https://www.jianshu.com/p/71a79d838443
來源:簡書