直接结论:别指望隔离级别自动兜底,必须显式加锁或原子校验——否则并发更新必然丢数据;update不带where会全表扫描更新导致覆盖、性能崩溃和锁表,select for update未走索引则升级为表锁,乐观锁须严格校验影响行数为1且禁用事务内ddl。

直接结论:别指望隔离级别自动兜底,必须显式加锁或原子校验——否则并发更新必然丢数据。
UPDATE 不带 WHERE 条件就是定时炸弹
很多人以为 UPDATE 天然锁行,其实它只锁**实际修改的行**,且前提是 WHERE 走索引。漏写 WHERE 会触发全表扫描+全表更新,不仅性能崩,还会让并发请求互相覆盖。
- 检查所有业务中的
UPDATE,确保每个都带主键或唯一索引条件,例如UPDATE account SET balance = balance + 100 WHERE id = 123 - 禁止用模糊条件如
WHERE status = 'pending'更新关键字段——可能锁多行,且易因数据倾斜引发死锁 - 上线前用
EXPLAIN看执行计划,确认type是const或eq_ref,不是ALL或range
SELECT FOR UPDATE 必须走索引,否则锁表
SELECT ... FOR UPDATE 是常用手段,但它只在查询命中索引时才锁对应行;如果走全表扫描,InnoDB 会升级为表锁(尤其 MySQL 5.7 及以前),并发一高就卡死。
-
FOR UPDATE的WHERE必须使用主键或唯一索引字段,例如SELECT * FROM account WHERE user_id = 123 FOR UPDATE(前提是user_id有唯一索引) - 别在
FOR UPDATE后加LIMIT 1期望“只锁一行”——MySQL 不保证锁哪行,且可能因间隙锁(gap lock)锁住更多范围 - 注意
next-key lock行为:即使查等值,也会锁住索引间隙,防止幻读;这在高并发INSERT场景下容易引发莫名阻塞
乐观锁 version 字段必须校验影响行数
用 version 字段做 CAS 更新看似简单,但重试逻辑缺失或判断松散,会导致“以为更新成功,实则被跳过”。
- 每次更新必须检查
UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?的影响行数,**只认mysql_affected_rows()或 ORM 返回值是否为 1**,不能只看 SQL 是否执行成功 - 重试次数建议 ≤3 次,超时直接报错,避免长事务拖垮连接池
-
version字段类型用INT UNSIGNED,别用TIMESTAMP——时钟回拨或毫秒精度不足会导致冲突误判
事务里混 DDL 会隐式提交,锁提前释放
MySQL 遇到某些语句会自动提交当前事务,导致 FOR UPDATE 锁提前释放,后续更新彻底裸奔。
- 典型触发点包括
ALTER TABLE、CREATE INDEX、DROP TABLE等 DDL 操作 - DDL 前务必确认当前事务已提交或回滚,严禁在事务中穿插非事务性操作
- 监控日志里出现
Implicit commit提示,基本就是这个原因
真正难的不是选悲观锁还是乐观锁,而是锁的边界是否和业务意图完全对齐——比如扣库存时锁的是“商品ID”,但业务实际要保障的是“用户+商品”的组合唯一性,这时候单靠主键锁就失效了。











