MySQL乐观锁通过UPDATE语句的WHERE version = ?实现,先校验再更新,需检查影响行数是否为1,version字段须为INT UNSIGNED/BIGINT且非NULL,应用层必须捕获0行返回并重试或抛异常。

UPDATE 语句里怎么用 version 字段做乐观锁?
直接在 WHERE 条件里带上版本号,更新成功才说明没被并发修改过。关键不是“加锁”,而是“校验+原子更新”。
常见错误是只查不校验:SELECT version FROM orders WHERE id = 123 后再拼 UPDATE ... SET version = version + 1 WHERE id = 123 AND version = ? —— 这中间如果别人先更新了,你的 UPDATE 就会匹配 0 行,必须靠返回影响行数判断是否失败。
- 字段类型建议用
INT UNSIGNED或BIGINT,避免溢出 - 初始值必须为
0或1,不能为NULL,否则WHERE version = NULL永远不成立 - 应用层必须检查
affected_rows == 1,否则抛异常或重试 - 不要在 UPDATE 里写
SET version = version + 1, updated_at = NOW()然后还依赖updated_at做条件——这会让乐观锁失效
时间戳字段(updated_at)能当乐观锁用吗?
可以,但风险高。本质是把 WHERE updated_at = ? 当作版本断言,但数据库时钟精度、批量更新、手动改时间都可能导致误判。
比如 MySQL 的 DATETIME 默认精度是秒,两笔毫秒级更新可能落在同一秒,第二次 UPDATE 就会覆盖第一次——这不是你想要的“拒绝覆盖”,而是“静默覆盖”。
- 如果非要用时间戳,至少用
TIMESTAMP(6)(微秒级),并确保所有写入都走NOW(6) - 禁止手动
SET updated_at = '2024-01-01',否则乐观锁形同虚设 - 应用层传入的时间戳必须来自上一次
SELECT,不能本地生成——时钟不同步会导致永远更新失败 - 比起
version,时间戳更难调试:你没法一眼看出“谁在什么时候覆盖了谁”
MyBatis / JPA 里怎么自动注入乐观锁逻辑?
框架能帮你省掉手拼 SQL,但底层仍是靠 WHERE 条件 + 影响行数判断,别以为加个注解就万事大吉。
MyBatis 示例:@Update("UPDATE product SET stock = stock - #{num}, version = version + 1 WHERE id = #{id} AND version = #{version}"),调用后必须检查 updateResult == 1,否则 throw OptimisticLockException。
JPA 的 @Version 注解更省事,但要注意:
-
@Version字段只能是数字类型(int/long),不能是Date或Timestamp(JPA 规范不保证时间戳语义) - 如果实体从缓存加载(比如二级缓存),且缓存没同步
version,会导致 UPDATE 带错旧版本号 - 批量更新(
@Modifying+@Query)不会触发@Version机制,得自己手写 WHERE 条件
为什么 UPDATE 返回 0 行却不报错?这是 bug 吗?
不是 bug,是设计使然。SQL 标准里 UPDATE 本就不因“没匹配到行”而报错,它只管执行逻辑。乐观锁依赖的就是这个行为:靠影响行数为 0 来识别冲突。
容易踩的坑是忽略返回值,或者用 ORM 的 save() 方法却没配 @Version,结果并发写入无声覆盖。
- 原生 JDBC:用
PreparedStatement.executeUpdate()拿返回值 - Spring JDBC:用
JdbcTemplate.update(...),返回值就是影响行数 - 千万别用
queryForObject类方法去执行 UPDATE - 日志里建议打印“expected 1 row, actual ${rows}”,方便排查并发冲突频次
实际最难的不是写对 SQL,是让整个业务流程接受“更新失败是常态”——比如下单减库存失败,得回滚本地事务、通知前端重试,而不是吞掉异常继续往下跑。

















