[Database][MySQL] 樂觀鎖 & 悲觀鎖指南 – OCC & PCC (FOR SHARE|FOR UPDATE)

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

Leave a Reply

Your email address will not be published. Required fields are marked *