乐观锁版本号更新必须原子执行,即通过带条件的update一步完成检查与更新,若拆分为select+update两步则并发失效;悲观锁在读多写少场景下因行锁阻塞导致性能严重下降;乐观锁失败重试成本远低于悲观锁等待开销。

乐观锁版本号更新必须是原子的
乐观锁不是靠数据库锁住行,而是靠 UPDATE 语句里带条件的“检查+更新”一步完成。如果拆成 SELECT 读 version + UPDATE 写 version 两步,中间就可能被其他事务插队修改,彻底失效。
-
UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 2是安全的; - 先
SELECT version FROM products WHERE id = 1,再拼 SQL 更新,是危险的; - 哪怕加了事务,只要没加锁(比如没用
FOR UPDATE),就挡不住并发写;
悲观锁在读多写少时会严重拖慢查询性能
一旦用了 SELECT ... FOR UPDATE,哪怕只是查库存、查积分这种只读动作,也会对行加排他锁,后续所有对该行的读(包括普通 SELECT)都会被阻塞,直到事务提交或回滚。
- 电商详情页加载时顺手查个
stock,如果加了FOR UPDATE,100 个用户同时刷页面,可能有 99 个在排队等锁; - 锁持有时间越长(比如事务里混了 HTTP 调用、日志写入),阻塞越明显;
- InnoDB 行锁实际生效依赖索引,全表扫描会升级为表锁,更不可控;
乐观锁失败时重试成本比悲观锁等待低
读多写少场景下,UPDATE 影响行为 0 的概率极低,重试几乎不发生;即使发生,也只是重新走一遍逻辑,不涉及锁等待、上下文切换、连接池耗尽等系统级开销。
- 乐观锁失败返回
affected rows = 0,业务层可立即重试或降级提示; - 悲观锁等待超时(默认
innodb_lock_wait_timeout=50秒)会导致请求卡死、线程堆积; - 高并发下悲观锁容易触发死锁检测,InnoDB 虽能自动回滚,但代价是事务中断+日志记录+监控告警;
版本号字段本身带来轻微写放大,但远小于锁开销
每次更新都要写 version 字段,确实多一次磁盘 I/O 和 redo log 记录,但这属于可控的、线性的写成本;而悲观锁带来的锁管理、等待队列、死锁检测、事务状态维护,是指数级复杂度增长。
- 加
version字段只需ALTER TABLE ... ADD COLUMN version INT DEFAULT 0,无索引要求; - 不需要额外事务控制逻辑,应用层代码更轻量;
- 注意:
version类型别用TINYINT,避免溢出;建议用BIGINT或INT UNSIGNED;
版本号机制真正的坑不在实现,而在“以为加了 version 就万事大吉”——比如忘了在 WHERE 条件里校验它,或者把乐观逻辑写在事务外,这些细节一漏,锁就形同虚设。











