必须用原子SQL(如UPDATE SET col=col+?)或配合锁/版本校验实现“读-改-写”,否则必丢数据;字段不可为NULL,WHERE须精准命中单行索引,且所有方案都依赖ROW_COUNT()校验。

直接用 UPDATE 语句做“读-改-写”三步操作,一定会丢数据。必须把“读取旧值→计算新值→写入”这三步压缩成一条原子 SQL,或配合锁/版本校验来兜底。
用 UPDATE ... SET col = col + ? 做原子增量
这是最轻量、最可靠的方式,适用于计数器、余额、积分等场景。MySQL 的 UPDATE 本身是原子的,col = col + 1 这类表达式由引擎在行锁保护下完成计算和写入,天然避免丢失。
- ✅ 正确写法:
UPDATE accounts SET balance = balance - 100 WHERE user_id = 123 - ❌ 错误写法:
SELECT balance FROM accounts WHERE user_id = 123→ 应用层算出新值 →UPDATE accounts SET balance = 900 WHERE user_id = 123(中间有竞态窗口) - ⚠️ 注意:字段不能为
NULL,否则NULL + 100结果仍是NULL;建议设默认值或加COALESCE(balance, 0)
用 WHERE 加乐观锁条件(如 version)
当必须先读再更新(比如要校验业务状态),就得靠 version 或 updated_at 字段做 CAS 校验。关键不是加了字段,而是应用层必须检查 ROW_COUNT() 是否为 1。
- ✅ 正确流程:查出
id=123, version=5→ 执行UPDATE orders SET status='shipped', version=6 WHERE id=123 AND version=5→ 检查影响行数 - ❌ 常见错误:执行完 UPDATE 就认为成功,不判断返回值;或重试时没重新 SELECT 最新
version,导致无限失败 - ⚠️
version字段必须是INT或BIGINT,不能是TIMESTAMP(毫秒级可能重复);且该列要建索引,否则WHERE条件走不了索引,锁范围扩大
SELECT ... FOR UPDATE 必须走索引,否则锁表
显式加排他锁看似稳妥,但 MySQL 在 FOR UPDATE 时是否只锁一行,完全取决于执行计划。没索引 = 全表扫描 = 表锁,高并发下直接卡死。
- ✅ 安全写法:
SELECT * FROM inventory WHERE sku = 'ABC123' FOR UPDATE(sku是唯一索引) - ❌ 危险写法:
SELECT * FROM inventory WHERE status = 'in_stock' FOR UPDATE(status无索引,或只有普通索引但区分度低) - ⚠️ 不要用
LIMIT 1试图“只锁一行”:SELECT * FROM t WHERE a > 100 FOR UPDATE LIMIT 1可能锁住整个范围,且无法预测锁哪行;InnoDB 的 next-key lock 会连带锁间隙
别碰 SERIALIZABLE 隔离级别,除非你真需要银行级串行
它确实能彻底消灭丢失更新,但代价是所有事务排队执行。实际压测中,吞吐量常跌到 RC 级别的 1/10,且死锁率飙升——这不是调优,是自废武功。
- ✅ 合理选择:
READ COMMITTED是默认且够用的起点;REPEATABLE READ对读一致性更强,但写-写冲突仍需自己处理 - ❌ 混淆概念:以为设成
REPEATABLE READ就自动给UPDATE加范围锁防覆盖——其实它只影响SELECT的快照行为,对并发UPDATE无额外保护 - ⚠️ 所有方案都依赖 WHERE 条件精准命中单行:主键、唯一索引缺一不可;模糊条件、函数包裹(如
WHERE DATE(created_at) = '2026-07-01')会让锁失效或扩大
真正容易被忽略的点是:**任何“先查后更”的逻辑,只要没做行数校验或显式加锁,就等于裸奔**。哪怕用了 version,漏判 ROW_COUNT() == 0 这一行代码,整套机制就形同虚设。

















