會員表 vip_member(InnoDB),字段:uid、start_at、end_at、active_status、updated_at。
續費規則:
end_at < NOW()(已過期)→ start_at = NOW(),end_at = NOW() + 月數end_at >= NOW()(未過期)→ end_at = end_at + 月數active_status = 1-- 線程 A、B 同時讀取 end_at = '2024-01-01'
-- A 續 1 個月,B 續 2 個月
-- 可能最終 end_at 只增加 2 個月,而非 3 個月
UPDATE vip_member
SET
start_at = CASE
WHEN end_at < NOW() THEN NOW()
ELSE start_at
END,
end_at = CASE
WHEN end_at < NOW() THEN DATE_ADD(NOW(), INTERVAL ? MONTH)
ELSE DATE_ADD(end_at, INTERVAL ? MONTH)
END,
active_status = 1,
updated_at = NOW()
WHERE uid = ?
LIMIT 1;
優點: 單語句原子執行,無並發衝突,無額外鎖開銷,SQL 注入安全(參數化)。
缺點: 業務邏輯耦合在 SQL,複雜場景維護困難。
START TRANSACTION;
SELECT * FROM vip_member WHERE uid = ? FOR UPDATE; -- 加排他鎖
-- 應用層判斷邏輯
UPDATE vip_member SET ... WHERE uid = ?;
COMMIT;
關鍵點:
FOR UPDATE:排他鎖,阻塞其他事務的 SELECT ... FOR UPDATE、UPDATE、DELETE。LOCK IN SHARE MODE:共享鎖,允許並發讀,阻塞寫 —— 不適用於此場景(仍會丟失更新)。優點: 邏輯清晰,應用層可控。
缺點: 鎖持有時間 = 事務持續時間,高並發下吞吐率下降,易死鎖。
-- 1. 讀取當前版本/關鍵字段
SELECT end_at, version FROM vip_member WHERE uid = ?;
-- 2. 業務計算新 end_at
-- 3. 帶版本條件更新
UPDATE vip_member
SET end_at = ?, active_status = 1, updated_at = NOW(), version = version + 1
WHERE uid = ? AND end_at = ? AND version = ?; -- 關鍵:雙重檢查
-- 4. 判斷 affected_rows
-- = 1 成功
-- = 0 衝突 → 重試(通常 1-2 次即成功)
實現變體: 無 version 列時,直接用業務字段 end_at 作為檢查條件(如上例)。
優點: 無鎖、無阻塞、吞吐極高、可選無事務(單語句原子)。
缺點: 衝突率高時重試開銷大,需應用層實現重試邏輯。
| 場景特徵 | 推薦方案 | 理由 |
|---|---|---|
| 邏輯簡單、可下推 SQL、追求極致性能 | A:單條 CASE UPDATE | 零鎖、零網絡往返、原子性最強 |
| 高並發、讀多寫少、衝突率低(<5%) | C:樂觀鎖 | 無阻塞,重試成本低,擴展性最佳 |
| 寫多、衝突率高、需強一致性 | B:悲觀鎖 FOR UPDATE | 隊列化衝突操作,避免無限重試 |
| 證券/交易系統、讀響應極敏感 | C:樂觀鎖 | 不阻塞讀請求 |
version INT DEFAULT 0,所有更新 version = version + 1,防止 ABA 問題。WHERE uid = ? 必須走主鍵/唯一索引,避免鎖表/全表掃描。Com_update、Innodb_row_lock_waits、重試次數分佈。