乐观锁必须显式在UPDATE语句中添加WHERE version = ?校验,否则无效;需确保SELECT查出version、SET中version = version + 1原子更新、且严格检查executeUpdate()返回值是否为0,三者缺一不可。

UPDATE语句必须带WHERE version = ?
乐观锁不是数据库自动启用的功能,它完全依赖你写的SQL是否包含版本校验。漏掉WHERE id = ? AND version = ?,就等于没锁。
常见错误:只写WHERE id = ?,或MyBatis手写XML时忘了拼AND version = #{version};JPA加了@Version但字段被@Transient忽略,导致查询不返回version值,后续WHERE version = null永远不匹配。
- MyBatis-Plus的
@Version只对updateById()等内置方法生效,自定义SQL必须手动加AND version = #{version} - JPA实体中
@Version字段类型必须是Integer或Long,禁用short——高并发下溢出变负数,WHERE version = -1可能意外命中旧记录 - SELECT时必须显式查出version字段,
SELECT *在某些ORM配置下会跳过映射,导致Java对象里version为0或null
SET子句要写version = version + 1
只校验version不更新它,下次WHERE条件就失效了。比如把version = #{version} + 1写成version = #{newVersion},相当于固定赋值,后续所有更新都会因version不递增而失败。
正确写法是让数据库原子执行version = version + 1,避免应用层计算后传入——防止两个并发请求读到相同version、算出相同新值,导致第二次更新成功覆盖第一次。
- MySQL和PostgreSQL都支持
SET version = version + 1这种表达式,无需应用层读-改-写 - 不要用
TIMESTAMP代替version字段:系统时钟可能回拨,毫秒级精度在高并发下也容易撞车 - 已有smallint字段需先
ALTER TABLE ... MODIFY COLUMN version BIGINT,再同步改Java实体类型,否则溢出后校验逻辑崩坏
必须检查executeUpdate()返回值是否为0
MySQL、PostgreSQL执行UPDATE时,WHERE不匹配不会报错,只会安静返回0行影响。如果你不主动判断,业务逻辑就会误走“更新成功”分支——库存没扣、订单状态没变,却给前端返回成功。
很多框架(包括MyBatis默认)把int rowsAffected当普通返回值,不强制校验。这是踩坑第二高发点。
- JDBC示例:
if (stmt.executeUpdate() == 0) throw new OptimisticLockException(); - MyBatis Mapper方法返回类型必须是
int,不能是void;建议在BaseMapper里统一封装update方法,强制抛异常 - Spring Retry不能直接套在service方法上——重试前必须重新SELECT最新数据并更新version,拿旧version循环重试毫无意义
批量更新别硬套乐观锁
UPDATE ... WHERE id IN (?, ?, ?)没法直接加乐观锁,因为你无法保证这批ID对应的version全一致。强行拼AND version IN (1,1,1)既无意义又破坏索引。
真要批量处理,优先选逐条带version更新(配合batch commit减少网络开销),而不是一条SQL扫多行。
- 逐条执行时,每条都走完整流程:SELECT → 计算变更 → UPDATE with version check → 判断rowsAffected
- 若必须单SQL批量更新,可换思路:用业务状态做条件,例如
WHERE status = 'pending' AND updated_at = ?,但前提是该状态在更新前未被其他线程修改过 - 更稳妥的方案是前置加分布式锁(如Redis
SETNX),控制整批操作的排他性,代价是引入额外组件和延迟

















