结论:version字段+UPDATE...WHERE id=? AND version=?+检查影响行数是最可控易落地的防更新丢失方案;需显式校验version、正确拼写SQL、主动判断返回值、重试前重新查询最新数据并更新version,绕过ORM的更新将使乐观锁失效。

直接说结论:用 version 字段 + UPDATE ... WHERE id = ? AND version = ? + 检查影响行数,是当前最可控、最易落地的防更新丢失方案。它不阻塞读,不依赖数据库锁机制,但要求你每一步都亲手把校验逻辑写进 SQL 和业务判断里——漏掉任何一环,乐观锁就形同虚设。
UPDATE 语句里必须显式写 WHERE ... AND version = ?
乐观锁不是数据库自动开启的功能,它完全依赖你写的 SQL 是否包含版本校验条件。常见错误是只写了 WHERE id = ?,或者用 ORM 自动生成语句时没启用乐观锁策略。
- 正确写法:
UPDATE order SET status = 'paid', version = version + 1 WHERE id = 123 AND version = 5 - 错误写法:
UPDATE order SET status = 'paid', version = 6 WHERE id = 123(漏掉AND version = 5,等于没锁) - MyBatis-Plus 用户注意:
@Version注解只对updateById等内置方法生效;手写 XML 或自定义 SQL 时,必须自己拼AND version = #{version} - JPA 用户注意:
@Version字段类型必须是Integer或Long,用short在高并发下会溢出变负数,导致条件恒成立
执行后必须立刻检查影响行数是否为 0
MySQL、PostgreSQL 都不会因为 WHERE 不匹配而抛异常,它们安静返回 0 行影响。如果你不主动判断,程序就会误以为“更新成功”,后续逻辑照常执行,数据实际没改——这就是丢失更新的根源。
- Java JDBC 示例:
int rows = stmt.executeUpdate(); if (rows == 0) throw new OptimisticLockException(); - MyBatis Mapper 方法返回类型必须是
int,不能是void;Spring Boot 中建议统一封装 BaseMapper 的 update 方法,强制校验返回值 - 别在事务外做重试:两次 SELECT 之间可能被第三方修改,重试逻辑应包在同一个事务内,或至少使用
REPEATABLE READ隔离级别
重试不是循环执行旧逻辑,而是重新读取再构造
失败后不能拿着旧的 version 值反复尝试更新。必须先重新 SELECT 当前最新数据(含新 version),再合并业务变更,生成新的 UPDATE 请求。
- 典型流程:
SELECT * FROM stock WHERE id = 1001→ 得到stock=10, version=7→ 扣减 1 → 构造UPDATE ... SET stock = 9, version = 8 WHERE id = 1001 AND version = 7 - 建议限制最大重试次数(如 3 次),避免雪崩;可加简单退避(
Thread.sleep(10)),但别在事务中 sleep - 强一致性场景(如支付状态变更),失败应直接返回「操作已被其他用户提交」,而不是静默重试
绕过 ORM 的更新会彻底失效
@Version 注解和 MyBatis-Plus 的乐观锁机制,只对走框架 update 方法的请求起作用。一旦出现以下情况,版本校验完全不触发:
- 原生 SQL 更新:
UPDATE user SET name = 'A' WHERE id = 123(没带version条件) - 数据库客户端直连执行
- 定时任务、其他微服务、ETL 工具直接写表
这类“绕过”行为无法靠应用层乐观锁兜底。关键表建议加数据库触发器拦截非法更新,或所有写操作统一收口到仓储层/领域服务,避免裸 SQL 泛滥。











