乐观锁的CAS判断需在UPDATE语句中显式校验version字段,正确写法为:UPDATE table SET col=?, version=version+1 WHERE id=? AND version=?,并检查影响行数是否为0。

UPDATE 语句里怎么写 version 字段的 CAS 判断
乐观锁不是靠数据库自动加锁,而是靠你在 UPDATE 里显式校验 version 值是否没被改过。核心就是:只更新当前值等于预期旧值的那条记录,否则影响行为为 0 行。
常见错误是漏掉 WHERE 中的 version = ? 条件,或者把它写成 version = version + 1 这种无效表达式。
- 正确写法:
UPDATE user SET name = ?, version = version + 1 WHERE id = ? AND version = ? - 执行后必须检查返回的受影响行数 —— 是 0 就代表 CAS 失败,别人抢先改了
- 注意:MySQL 的
version字段建议用INT UNSIGNED,避免溢出;PostgreSQL 同理,但要注意序列默认不支持 unsigned - 别在同一个事务里多次读取再拼 SQL,容易因中间态导致逻辑错乱
Java 里怎么安全地重试 version 冲突
重试不是无脑循环,关键在于「重新加载最新数据 + 重新计算业务变更」,而不是拿旧值反复撞墙。
典型错误是把重试逻辑写在 DAO 层、或把业务逻辑和重试耦合在一起,导致状态不一致或无限重试。
- 重试前必须重新
SELECT当前行(含最新version),不能复用第一次查出来的对象 - 推荐用固定最大次数(比如 3 次),超限就抛
OptimisticLockException,由上层决定降级或提示用户 - Spring 的
@Retryable不适合这里 —— 它不帮你 reload 数据,纯重放原方法,大概率一直失败 - 如果业务允许,可把重试封装成一个函数式接口,入参是「基于最新快照的变更逻辑」,避免状态残留
timestamp 类型做乐观锁比 version 有什么坑
用 updated_at 时间戳代替 version 数字,看起来省事,实则更难控制精度和时序一致性。
最常踩的坑是:数据库时间精度不够(比如 MySQL 5.6 默认秒级),或应用服务器时钟不同步,导致两个合法更新被误判为冲突。
- MySQL 5.6.4+ 才支持
DATETIME(6)微秒级,老版本基本不可靠 - PostgreSQL 的
TIMESTAMP WITH TIME ZONE虽然准,但 JDBC 驱动默认可能截断精度,需显式设useLegacyDatetimeCode=false - 跨服务场景下,绝对时间永远不如自增
version可控 —— 你没法让所有机器时钟完全对齐 - 如果真要用 timestamp,建议只用于“最后修改时间”展示,乐观锁仍坚持用
version字段
MyBatis 和 JPA 对 version 字段的特殊处理
ORM 框架会帮你自动管理 version 字段,但前提是配置到位,否则照样失效。
很多人以为加了 @Version 就万事大吉,结果发现 UPDATE 语句里根本没带 version = ? 条件 —— 其实是实体没被识别为乐观锁实体。
- JPA:
@Version字段必须是非 null、非 transient、且类型是数字(int/long/Integer等),否则 Hibernate 直接忽略 - MyBatis-Plus:
@TableField(fill = FieldFill.UPDATE)不管用,得配MetaObjectHandler+ 在 XML 或注解 SQL 里手动写version = #{version} + 1 - MyBatis 原生:如果用
标签,必须显式写出version = version + 1和AND version = #{version},框架不会自动注入 - 注意 Hibernate 的
dynamic-update=true会影响 version 更新时机,慎开
version 字段的可靠性,不取决于你用了什么框架,而取决于每次 UPDATE 是否真实参与了条件判断和自增更新 —— 这一步漏了,后面所有重试都白搭。










