乐观锁生效的前提是update语句必须包含version条件并检查affectedrows:正确写法为update ... set ..., version = version + 1 where ... and version = ?,且应用层须判断影响行数是否为1,否则乐观锁失效。

UPDATE 语句必须带 version 条件,否则等于没锁
乐观锁不是数据库自动启用的机制,它完全依赖你写的 UPDATE 语句是否包含校验逻辑。如果只写 UPDATE t SET stock = stock - 1 WHERE id = 1,哪怕表里有 version 字段也毫无作用——根本没参与判断。
正确写法必须把读取时拿到的 version 值作为 WHERE 条件的一部分:
UPDATE t_goodsku SET count = count - 2, version = version + 1 WHERE id = 123 AND version = 5;
- 这条语句执行后,用
ROW_COUNT()或程序里的affectedRows判断影响行数:为1表示更新成功;为0表示版本不匹配,已被他人抢先更新 - 不能省略
version = ?条件,也不能把它放在SET子句里(比如写成SET version = 5 + 1) - 如果业务逻辑中存在“先查再算再更新”的多步操作,中间不能被其他事务打断——但这不是数据库保证的,而是靠应用层重试机制兜底
version 字段必须是整型且初始值为 0 或 1,别用 TIMESTAMP 模拟
虽然资料里常提“时间戳方式”,但实际生产中强烈建议只用整型 version 字段。原因很实在:
-
TIMESTAMP在高并发下可能重复(尤其 MySQL 5.6 及更早版本,精度只有秒级;即使 5.7+ 支持微秒,仍可能因系统时钟回拨或事务延迟导致冲突误判) -
version是纯递增整数,语义清晰、比较可靠、索引效率高 - 初始值设为
0或1都可以,但要统一。常见错误是建表时写DEFAULT NULL,结果第一次更新时WHERE version = NULL永远不成立 - 字段类型推荐
INT UNSIGNED,避免负数和溢出问题(42亿次更新才到上限,够用)
应用层必须处理 affectedRows == 0 的情况,不能静默忽略
MySQL 执行带 version 条件的 UPDATE 后,不会报错,只是返回影响行数为 0。很多开发者卡在这一步:以为“没报错就是成功了”,结果覆盖了别人刚写的值。
- Java 中
JDBC的executeUpdate()返回int,必须显式判断是否等于1 - Spring Data JPA 的
@Modifying方法默认不返回影响行数,需加int返回类型并检查 - 常见做法是做有限次数重试(如最多 3 次),每次重试前重新
SELECT当前version和业务字段 - 注意:重试逻辑不能放在数据库事务内盲目循环,否则可能造成死锁或长事务;建议在 service 层控制
复合条件更新时,version 校验仍要独立存在
有些业务场景需要同时校验多个字段(比如库存扣减还要检查 status = 'on_sale'),容易误以为 “加上其他条件就够了”,从而漏掉 version。
- 错误写法:
UPDATE t SET stock = stock - 1 WHERE id = 1 AND status = 'on_sale'—— 完全没用 version - 正确写法:
UPDATE t SET stock = stock - 1, version = version + 1 WHERE id = 1 AND status = 'on_sale' AND version = 7 - 所有前置业务约束(状态、时间范围、权限标记等)和乐观锁校验(
version)必须共存于同一个WHERE子句,缺一不可 - 如果业务规则复杂到难以单条 SQL 表达,说明这个场景可能不适合乐观锁,该考虑悲观锁或分布式锁
真正起作用的从来不是“有没有 version 字段”,而是每一次 UPDATE 是否老老实实带上它,并且应用代码是否真正在意那行 if (rows != 1)。漏掉任意一环,乐观锁就退化成普通更新。











