乐观锁必须显式校验version字段:update语句需同时满足where id=? and version=?、set version=version+1、执行后检查影响行数为0则冲突,三者缺一不可;orm中@version仅对内置方法生效,手写sql须手动拼接校验条件。

必须显式校验 version 字段,否则乐观锁形同虚设。 你写的 UPDATE 语句里漏掉 WHERE version = ?、没在 SET 里写 version = version + 1、或者执行后不检查影响行数,三者任一缺失,就等于没加锁。
UPDATE 语句必须同时满足三个条件
乐观锁不是数据库自动启用的功能,而是靠你亲手构造 SQL 实现的原子条件更新:
-
WHERE子句中必须包含id = ? AND version = ?,只写id = ?就失去版本校验能力 -
SET中必须更新version = version + 1,否则下次WHERE version = ?永远匹配不上 - 执行后必须立刻检查返回值:MySQL 用
ROW_COUNT(),PostgreSQL 用GET DIAGNOSTICS ROW_COUNT,JDBC 用executeUpdate()返回值;等于 0 表示冲突,必须中断流程
MyBatis 或 JPA 里 version 容易失效的点
ORM 不会自动帮你补全 version 校验逻辑,很多“看似加了 @Version 却没效果”的问题都出在这里:
- MyBatis-Plus 的
@Version注解只对updateById()等内置方法生效;手写 XML 或自定义@Update时,必须手动拼AND version = #{version} - JPA 实体中
@Version字段类型不能是short——高并发下溢出变负数,WHERE version = -1可能意外命中旧记录 -
resultMap或实体映射里漏掉version字段,导致应用层拿不到原始值,WHERE version = ?实际填的是null或默认值 0
重试时必须重新 SELECT 最新数据
失败后不能拿着旧 version 值反复尝试更新。常见错误是 while 循环里重复执行同一 SQL,结果永远失败:
- 每次重试前,必须重新
SELECT ... FOR UPDATE(或普通SELECT加事务隔离)读取当前version和业务字段 - 重试次数建议设上限(如 3 次),第 3 次失败应抛
OptimisticLockException,而不是无限循环 - 整个“查→算→更”要包在同一个事务里,否则中间状态可能被其他事务改掉
version 字段类型和索引别踩坑
字段设计不当会导致校验失效或性能下降:
- 类型选
INT或BIGINT,禁用TIMESTAMP、short、UUID;时间字段精度不足且无法保证单调递增 - 必须为
(id, version)建联合索引(或至少version单独有索引),否则WHERE version = ?可能全表扫描,甚至误判 - 不要用
SELECT MAX(version) + 1赋值——竞态明显,且无法保证原子性
真正容易被忽略的是:version 字段是否参与业务逻辑校验。比如金融类系统要求每次资金变动必须带前序版本号,否则拒绝执行;而日志类表可能只用它做简单并发防护。没做幂等或版本比对的应用,加了 version 字段也等于没加。










