framework
framework/mysql/transaction/mvcc-and-locking-read.md
high
mysql-mvcc-and-locking-read
MVCC 一致性视图与当前读加锁 — 普通 SELECT 走 undo 多版本不加锁,需读最新并锁定时用 SELECT FOR UPDATE(写锁)或 LOCK IN SHARE MODE(读锁)。Use when 防超卖/防并发改 / 纠结要不要 FOR UPDATE / 排查读到旧值时。
mvcc多版本并发控制for updatelock in share mode当前读一致性视图undo log防超卖
paths
*.sql*.java*.xml
MySQL · MVCC 与当前读加锁
本条只管「何时该用加锁读、MVCC 为什么让你读到旧值」。RR/RC 级别本身怎么选见
isolation-level.md;加锁带来的间隙锁/死锁见gap-lock-deadlock.md。
MVCC 机制(为什么普通读看不到最新值)
每行有隐藏的事务版本号 + 回滚指针,旧版本存在 undo log。普通 SELECT 按事务的一致性视图沿 undo 找到「对本事务可见」的版本 → 不加锁、不阻塞写,但读到的可能不是最新已提交值。
这是高并发读的基石,但意味着:靠普通 SELECT 读出来再做判断、再 UPDATE,存在并发覆盖风险(如超卖)。
当前读:读最新 + 加锁
| 写法 | 锁类型 | 用途 |
|---|---|---|
SELECT ... FOR UPDATE |
排他锁(X) | 读出最新值并锁定,准备改它(防超卖、防并发改) |
SELECT ... LOCK IN SHARE MODE |
共享锁(S) | 读出最新值并保证读期间别人不能改(弱于上者) |
UPDATE / DELETE |
X 锁 | 本身就是当前读 |
正例:防超卖
-- ✅ 当前读锁定库存行,串行化扣减
START TRANSACTION;
SELECT stock FROM goods WHERE id = 1 FOR UPDATE; -- 锁住,别的事务在此等待
-- 应用层判断 stock > 0
UPDATE goods SET stock = stock - 1 WHERE id = 1;
COMMIT;
-- ✅ 或直接用原子 UPDATE + WHERE 条件,连 SELECT 都省(更轻)
UPDATE goods SET stock = stock - 1 WHERE id = 1 AND stock > 0;
-- 看 affected rows 是否为 1 判断是否扣成功
反例
-- ❌ 快照读判断 + 后续更新:两步之间别人已扣,导致超卖
SELECT stock FROM goods WHERE id = 1; -- 普通读,可能是旧值
if (stock > 0) UPDATE goods SET stock = stock - 1 WHERE id = 1;
选用原则
- 能用原子
UPDATE ... WHERE 条件解决就别加FOR UPDATE(锁范围更小、更快)。 - 必须「读出值→应用层复杂判断→再写」才用
FOR UPDATE。 - 跨进程/跨服务的互斥不归行锁管,见
../../redis/distributed-lock.md。
自检
- [ ] 需要基于「读到的值」做判断再更新时,是否用了当前读(FOR UPDATE)或原子 UPDATE,而非快照读?
- [ ] 能用原子
UPDATE...WHERE就没多加一道FOR UPDATE? - [ ] 加锁读是否在事务内、且事务尽快提交(见 transaction-scope)?
- [ ] 没把行锁误当跨进程分布式锁用?