乐观锁通过WHERE version = ?实现并发控制,核心是先更新后校验,需检查影响行数、显式查询version字段、避免SQL陷阱,且仅保障单行更新原子性。

UPDATE时用WHERE version = ?判断是否被并发修改
乐观锁本质是“先更新,再校验”,不依赖数据库锁机制。核心逻辑是:每次更新前检查当前记录的version是否等于你读取时的值,相等才更新,同时把version加1;不等说明已被别人改过,操作失败。
常见错误现象:UPDATE user SET balance = 100, version = version + 1 WHERE id = 123 AND version = 5执行后affected rows = 0,但业务没检查这个返回值,导致“以为改成功了”,实际数据没变。
- 必须在应用层检查
executeUpdate()或execute()的返回影响行数,为0就抛异常或重试 -
version字段建议用INT UNSIGNED或BIGINT,避免溢出;不要用TIMESTAMP或DATETIME模拟版本号——精度和时钟漂移会导致误判 - 如果SQL里写
version = version + 1,MySQL会自动读取当前值再+1,不需要先SELECT再拼SQL
SELECT时必须带上version字段,且不能用SELECT *隐式遗漏
很多同学写SELECT id, name, balance FROM user WHERE id = 123,结果后续更新时拿不到version值,只能硬编码或默认填0,一更新就永远失败。
使用场景很明确:只要这个表启用了基于version的乐观锁,所有读取该记录的查询(包括列表页详情、编辑页加载)都必须显式包含version字段。
- ORM框架如MyBatis要确保
resultMap里映射了version字段,JPA实体类里对应属性不能加@Transient - 如果用
SELECT *,要确认表结构中version字段确实存在且未被忽略(比如某些中间件会过滤掉以_开头字段) - 注意MySQL 8.0+对
SELECT *的列顺序保证更严格,但依然不建议依赖——显式写字段最稳
事务中混合读写时,version检查点位置决定是否真正防住并发
乐观锁不是“加个version就能万事大吉”。如果在事务里先做其他DB操作,最后才UPDATE带version,那前面的操作可能已基于过期数据做了计算,version检查只是最后一道补救。
性能影响很小,但逻辑漏洞很大:比如转账场景,A查到余额100、version=3,B同时查到同一条记录也是100、version=3;A扣10元并UPDATE成功(version→4),B仍按100算,再扣10元,UPDATE时WHERE version = 3失败——但B可能已经发了通知、记了日志、调了下游接口。
- 关键原则:读取
version和最终UPDATE之间,尽量不穿插业务逻辑或外部调用 - 如果必须分步处理,考虑把校验提前到事务开始后立即做一次“轻量SELECT FOR UPDATE”(仅锁定,不更新),或者用
SELECT ... LOCK IN SHARE MODE配合version比对 - 高并发下重试策略要谨慎:简单while循环重试可能压垮DB;建议加随机退避、限制最大重试次数
MyBatis/MyBatis-Plus里version字段的自动填充陷阱
MyBatis-Plus的@Version注解看似省事,但容易踩两个坑:一是字段名不匹配导致自动填充失效,二是多表JOIN更新时MP不识别version字段。
错误现象:userMapper.update(user, wrapper)执行后version没自增,或者UPDATE语句里根本没出现version = version + 1和WHERE version = ?。
- 确认实体类中
version字段有@Version且类型是Integer/Long,字段名必须和数据库列名一致(MP默认不支持下划线转驼峰用于version) - 如果用
QueryWrapper,别写wrapper.eq("version", xxx)——MP会把它当普通条件,覆盖掉内置的version校验逻辑 - 原生
UPDATESQL或@UpdateProvider写的动态SQL,MP完全不干预,@Version无效,必须手动写SET version = version + 1 WHERE version = #{version}


















