MySQL不提供乐观锁原语,需应用层通过UPDATE+WHERE version=?模拟;version必须为整型并建组合索引(id,version);UPDATE后须检查影响行数,为0则冲突,应重试或返回409;MyBatis-Plus@Version非银弹,需确保字段类型、默认值及插件覆盖范围正确。

MySQL 本身不提供“乐观锁”原语,它只是配合应用层用 UPDATE + WHERE version = ? 这种写法模拟乐观锁行为。真正决定是否“高性能”的,不是 SQL 写法本身,而是你怎么设计版本字段、怎么处理失败、以及有没有踩进常见陷阱。
为什么 version 字段必须是整型且带索引
版本号字段用 INT 或 BIGINT 是硬性要求:不能用 VARCHAR 存“v1”“v2”,也不能用 TIMESTAMP(精度和时钟漂移会导致误判)。整型比较快,且天然支持原子自增。
索引不是可选项——如果 WHERE id = ? AND version = ? 频繁执行,而 version 没有索引,MySQL 可能全表扫描。尤其在大表、高并发更新场景下,UPDATE 会变慢甚至阻塞其他查询。
建议组合索引:INDEX (id, version),比单列 INDEX(version) 更高效,因为业务几乎总是按 id 定位再校验版本。
UPDATE 影响行数为 0 就代表冲突,但别直接抛异常
UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = 123 AND version = 42 执行后,必须立刻查 ROW_COUNT()(MySQL)或等价 API(如 JDBC 的 getUpdateCount())。
返回 0 ≠ 系统错误,它只说明“数据已被别人改过”。这时候怎么做,决定了性能和用户体验:
- 重试 1–3 次(读最新
version后再试),适合秒杀类短操作 - 返回
409 Conflict给前端,由用户刷新后重提,适合后台管理类操作 - 跳过更新、记录日志并告警,适合异步补偿场景
千万别在没检查 ROW_COUNT() 的情况下就认为“扣减成功”——这是线上超卖最常见源头。
MyBatis-Plus 的 @Version 注解不是银弹
Spring Boot + MyBatis-Plus 项目里加了 @Version 注解和 OptimisticLockerInnerInterceptor 插件,不代表万事大吉。
几个关键点必须手动确认:
- 实体类中
version字段类型必须是Integer或Long,不能是String或包装为空 - 数据库字段默认值要设为
0(不是NULL),否则首次插入时插件可能不生效 - 插件只拦截
updateById和update(带 Wrapper),对execute原生 SQL 或批量updateBatchById无效 - 如果你用了逻辑删除(
@TableLogic),确保version字段不在逻辑删除条件里被忽略
并发压力大时,version 自增可能撞上主键冲突或间隙锁
这容易被忽略:当多个事务同时尝试更新同一行,虽然最终只有一个是 ROW_COUNT() = 1,但其他事务的 UPDATE 仍会走到 InnoDB 的行锁流程。
尤其在 READ-COMMITTED 隔离级别下,这些失败的 UPDATE 依然会申请并释放间隙锁(gap lock),在极端高并发下可能引发锁等待甚至死锁报警——不是乐观锁设计错了,而是你低估了失败请求的底层开销。
应对方式很实际:把重试逻辑移到应用层做指数退避(比如第一次等 10ms,第二次 30ms,第三次 100ms),而不是让数据库反复扛压。



















