SQL 乐观锁(version/timestamp)的实现模板与 CAS 失败重试策略

2次阅读

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

SQL 乐观锁(version/timestamp)的实现模板与 CAS 失败重试策略

UPDATE 语句里怎么写 version 字段的 CAS 判断

乐观锁不是靠数据库自动加锁,而是靠你在 UPDATE 里显式校验 version 值是否没被改过。核心就是:只更新当前值等于预期旧值的那条记录,否则影响行为为 0 行。

常见错误是漏掉 WHERE 中的 version = ? 条件,或者把它写成 version = version + 1 这种无效表达式。

  • 正确写法:UPDATE user SET name = ?, version = version + 1 WHERE id = ? AND version = ?
  • 执行后必须检查返回的受影响行数 —— 是 0 就代表 CAS 失败,别人抢先改了
  • 注意:mysqlversion 字段建议用 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 + 1AND version = #{version},框架不会自动注入
  • 注意 Hibernate 的 dynamic-update=true 会影响 version 更新时机,慎开

version 字段的可靠性,不取决于你用了什么框架,而取决于每次 UPDATE 是否真实参与了条件判断和自增更新 —— 这一步漏了,后面所有重试都白搭。

text=ZqhQzanResources