select for update会拖慢系统,因其在未走索引、主键不明确或扫描范围大时升级为表锁,且事务越长阻塞越严重,易引发死锁;而update where version=?靠原子性实现无锁更新,但需严格满足version字段约束与原子执行条件。

为什么 SELECT ... FOR UPDATE 会拖慢整个系统
因为它是真锁,且默认行级锁的生效前提很苛刻。一旦没走索引、主键不明确、或扫描范围过大,InnoDB 就会升级为表锁——整张表卡住,所有后续的 SELECT ... FOR UPDATE、UPDATE、DELETE 都得排队等。更麻烦的是,事务越长,锁持有时间越久,阻塞链越容易形成死锁,触发 innodb_deadlock_detect 回滚后还要重试,实际吞吐反而更低。
常见踩坑点:
-
SELECT * FROM orders WHERE status = 'pending' FOR UPDATE—— 如果status没索引,直接表锁 - 在事务里做 HTTP 调用、日志写入、复杂计算,导致锁持有几十毫秒甚至几百毫秒
- 多个
FOR UPDATE语句顺序不一致(比如 A 先锁 id=1 再锁 id=2,B 反过来),极易死锁
UPDATE ... WHERE version = ? 为什么能绕过锁竞争
它根本不加锁,纯靠数据库原子性 + 条件匹配完成冲突检测。执行时只做一次快照读(MVCC),然后尝试更新:若 version 值已变,UPDATE 影响行为 0,业务立刻感知失败;若匹配成功,一行内完成 stock = stock - 1 和 version = version + 1,无锁、无等待、无死锁风险。
但要注意几个硬约束:
- 必须把
version字段设为INT UNSIGNED NOT NULL DEFAULT 0,避免空值导致条件永远不成立 - 初始插入时要显式写
version = 0,不能依赖DEFAULT在某些 ORM 中可能不生效 - 不能在同一个事务里先
SELECT ... version再UPDATE—— 这样还是可能被其他事务抢先更新,必须保证“读-判-更”是原子的,即业务层拿到 version 后立即发 UPDATE
什么时候换乐观锁反而更危险
不是所有场景都适合砍掉悲观锁。如果业务逻辑中「读」和「更新」之间存在不可省略的中间判断(比如库存扣减前要查用户等级、校验优惠券、调第三方风控接口),那 version 机制就扛不住:别人可能在你调接口时改了数据,而你 UPDATE 还是会成功(因为 version 没变),导致超卖。
这种情况下,要么保留悲观锁(但缩短事务范围,把外部调用挪到事务外),要么改用带条件的乐观锁增强版:
- 把关键业务状态也纳入 WHERE 条件,例如
UPDATE goods SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 5 AND stock >= 1 - 或者用
SELECT ... FOR UPDATE NOWAIT(MySQL 8.0+)快速失败,避免无限等待 - 再或者,对热点商品 ID 做分段(如
shard_id = id % 16),把单行锁分散成 16 行锁,降低冲突概率
版本号字段本身带来的隐性成本
很多人忽略一点:version 字段会让每次 UPDATE 都变成“非覆盖写”——即使只改一个字段,也必须带上 version = version + 1,这会导致:
- 二级索引更新更频繁(因为主键索引改了,所有二级索引都要更新)
- binlog 日志体积增大(每行变更多记一个字段)
- 如果用了 MySQL Group Replication 或 Canal 做订阅,下游解析压力上升
- 某些 ORM(如 MyBatis-Plus)自动生成的
updateById默认不带 version 条件,容易漏写,上线即出 bug
真正落地时,version 不是加个字段就能跑,得配合 DAO 层强约束、SQL 审计规则、以及监控 UPDATE 返回影响行数是否为 0 的告警——否则问题暴露出来,已经是在凌晨三点的订单超卖现场了。











