悲觀鎖是 Pessimistic Lock的直譯,它的意思可以從2個方面來記憶:
- Pessimistic:根據劍橋字典,悲觀即「認為壞事比較可能發生」。所謂的「壞事」是指資料的衝突與不一致,特別是在多人或多個程序同時操作同個資源時。
Pessimistic: thinking that bad things are more likely to happen or emphasizing the bad part of a situation:
- Lock:為了避免資料衝突,某一使用者或程序A可以在讀取或寫入資源前,先將資源上鎖。
而「悲觀鎖」的意思,是讓這個鎖具有排他性,也就是在A完成操作並釋放鎖前,其他使用者或程序無法修改被鎖定的資源。
特性
悲觀鎖是透過事務(Transaction)機制實作,並衍生出對應的特性。所以在了解悲觀鎖之前,我們先複習一下事務是什麼。
事務
進行一個業務功能時,可能涉及多資料表的操作,例如訂單成立時,庫存應同步減少對應的數量。為了達到這個效果,可以使用事務來確保兩者同步。
事務是資料庫操作的邏輯單位,如果同一個事務內的操作不是全部成功,就會全部失敗。如果操作中已修改A表跟B表,但操作C表未成功,資料庫也會將A表跟B表恢復到未修改前的狀態,這個特性稱為「回滾(rollback)」。
悲觀鎖即是確保資料在同一時間只能被一個事務內的操作修改。
優點
- 保證資料的一致性:因為同一時間不會有兩個事務同時修改一筆資料。
- 可搭配公平鎖、Queue等機制控制執行順序:有些資料庫預設是FIFO,即先請求的先處理,但並非絕對,所以可進一步搭配公平鎖與Queue 確保FIFO。
缺點
- 可能造成不良的使用體驗:在事務的機制下,除了讀取外,一個事務內的操作必定要等當前獲得鎖的事務結束後才能進行,所以可能會增加等待時間,並造成系統吞吐量下降。
適用情境
事務高衝突時:如銀行的大量交易,用悲觀鎖可以避免資源同時被多個事務修改,維持資料的一致性;又例如電商搶購活動競爭少量庫存,悲觀鎖可以避免超賣,若DB本身採用FIFO,也確保了公平性。
其他補充
- 行鎖、表鎖、讀鎖、寫鎖等用 Synchronized 實現的鎖皆為悲觀鎖。行鎖在電商庫存管理的應用可以詳閱電商庫存管理的處理架構 - 行鎖
- 悲觀鎖的鎖頭欄位需務必加上索引,因為在沒有索引的時候,sql的查詢會一行行掃描,直到找到對應的鎖頭值,而被掃描過的資料行都會在過程中被加上悲觀鎖。
資料庫實作
這篇是用MySQL的Workbench來演示扣庫過程悲觀鎖的應用。假設現在有一張資料表book,欄位stock記錄著書籍的庫存。
按照以下步驟模擬多個事務同時扣庫的情形,而在這個工具中,多個事務需透過多個DB Session開啟:
- 開啟一個Session,使用begin字符開啟事務,避免MySQL自動提交。這個Session下文稱為Session A。
begin;
- 假設要購買ID為1的書,先將此行資料上悲觀鎖。
SELECT * FROM bookstore.book where book_id = 1 for update;
- 開啟另一個Session,重複步驟1跟2,開啟事務並上悲觀鎖。
begin;
SELECT * FROM bookstore.book where book_id = 1 for update;
此時會發現無法順利查詢,訊息顯示Running。這是由於Session A已經把這行資料鎖住了。
- 在Session A 的 console執行扣庫,並在查詢結果符合預期,提交事務。
update bookstore.book set stock = stock - 1 where book_id = 1 and stock > 0;
SELECT * FROM bookstore.book where book_id = 1;
commit;
- 切回Session B,可以發現查詢訊息由Running改為1 row(s) returned,且結果也順利回傳了,代表此時由Session B取得悲觀鎖。