Intro
Database 中,鎖的實現分類上目前可分為兩種:
- 樂觀鎖(Optimistic Locking)
- 悲觀鎖(Pessimistic Locking)
樂觀鎖 - OCC
樂觀並行控制 - Optimistic Concurrency Control,縮寫「OCC」
- 不利用內建鎖機制,而是利用應用層邏輯解決衝突
- 可透過讀取資料時分派一個版本或時間戳,通過校驗後寫入否則中斷(實作上可利用寫入連帶驗證版本達成)
- 適合交易之間資料競爭(data race)概率較小的應用情境
MySQL 實現範例
CAS (Compare And Swap)
-- 確保「我現在改的」是我剛才「看到的」那一版
UPDATE page
SET content = '第六版...', version = version + 1
WHERE id = 3547 AND version = 5;
Conditional Update
-- 利用資料庫原子操作,不需要 version 也能保證不超賣
UPDATE `DB`.`table`
SET quantity = quantity - 1
WHERE id = 1 AND quantity >= 1;
悲觀鎖 - PCC
悲觀並行控制 - Pessimistic Concurrency Control,縮寫「PCC」
- 利用內建鎖機制來解決衝突
- 呈上,效率不佳且有增加產生死鎖(deadlock)機會
- 鎖的分類上可以簡單分為兩大類:讀鎖 (Read Lock) 和寫鎖/排他鎖 (Exclusive Lock)
| 事務 A & B 持有的鎖 | 無鎖 (Normal Select) | FOR SHARE (S-Lock) | FOR UPDATE (X-Lock) |
|---|---|---|---|
| Normal Select | 允許 (MVCC) | 允許 | 允許 |
| FOR SHARE | 允許 | 允許 | 阻塞 |
| FOR UPDATE | 允許 | 阻塞 | 阻塞 |
| UPDATE / DELETE | 允許 | 阻塞 | 阻塞 |
MySQL 實現案例 (InnoDB Transaction)
Row-level Lock for share (Read Lock)
START TRANSACTION;
-- 鎖定優惠券的「時間戳」與「狀態」 (S-Lock)
-- FOR SHARE 允許其他用戶同時執行此行(不互斥),但會阻塞管理員的 UPDATE 請求
SELECT * FROM coupons WHERE code = 'NEWYEAR' AND status = 'active' FOR SHARE;
-- 寫入訂單:因為鎖的保護,訂單的 created_at 必定早於(或等於)後續管理員修改優惠券的時間
INSERT INTO orders (user_id, coupon_code, created_at) VALUES (9527, 'NEWYEAR', NOW());
COMMIT;
如果管理員同時想停用此券,其 UPDATE 請求會進入排隊,直到所有結帳中的 FOR SHARE 交易完成。這確保了「正在結帳的人不受影響」,而「更新後的規則只會套用到未來的請求」。
Row-level Lock for update (Write Lock)
START TRANSACTION;
-- 1. 鎖定該行,阻塞其他事務同時進行「加鎖讀取 (FOR SHARE / FOR UPDATE)」或「修改 (Update/Delete)」
-- 注意:普通的 SELECT (快照讀) 依然可以讀取此行,不會被阻塞
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- 程式邏輯判斷餘額是否充足...
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
「注意:InnoDB 的行鎖(Row-level Lock)是基於 Index 實現的。如果查詢條件沒有命中索引,鎖會升級為表鎖(Table Lock)或間隙鎖(Gap Lock),導致大幅影響併發效能。」
MySQL - InnoDB Locking Postgresql Explicit Locking - Row-Level Locks