mysql本身不提供乐观锁功能,必须由应用层通过update ... where id = ? and version = ?并检查影响行数来模拟;漏掉任一环节(如未校验version、未检查row_count()、未重试)即失效。

直接结论:MySQL 本身不提供乐观锁功能,必须靠应用层用 UPDATE ... WHERE id = ? AND version = ? + 检查影响行数来模拟;漏掉任一环节,就等于没加锁。
UPDATE 语句里必须显式写 WHERE version = ?
乐观锁不是数据库自动行为,它完全依赖你写的 SQL 是否包含版本校验条件。常见错误是只在 SELECT 时读了 version,但 UPDATE 时只写 WHERE id = ?,结果后写入者直接覆盖前写入者。
- 正确写法:
UPDATE user SET name = ?, version = version + 1 WHERE id = 123 AND version = 5 - 错误写法:
UPDATE user SET name = ?, version = version + 1 WHERE id = 123(没校验版本,纯覆盖) - ORM 框架如 MyBatis-Plus 的
@Version注解,只在调用updateById()时生效;自己写@Update就得手动拼AND version = #{version}
必须检查 ROW_COUNT() 返回值是否为 0
MySQL 执行完 UPDATE 后,ROW_COUNT() 返回 0 表示“没找到匹配的行”,即 version 已被别人改过——这不是异常,而是预期中的并发冲突信号。
- 别只看 JDBC 的
executeUpdate()是否抛异常;它成功返回 0 是常态,不是错误 - 返回 0 时应立即处理:重试(需重新
SELECT最新version)、返回 HTTP 409、或走异步补偿 - MyBatis 中可用
<selectkey></selectkey>或useGeneratedKeys配合,但最稳的是在 DAO 层显式调用getUpdateCount()
version 字段类型和索引不能省
用错类型或缺索引,会导致校验失效或性能崩塌。比如用 TIMESTAMP 当版本号,时钟漂移会让版本倒挂;没索引则 WHERE version = ? 可能触发全表扫描。
- 类型必须是整型:
INT UNSIGNED(推荐)或BIGINT UNSIGNED;禁用VARCHAR、TIMESTAMP、DATETIME - 索引必须建:
INDEX (id, version)组合索引,比单列INDEX(version)更高效,因为业务几乎总是按id定位后再校验 - SELECT 时必须显式包含
version字段,不能依赖SELECT *——中间件或字段顺序变化可能让它被忽略
重试逻辑要防死循环和状态污染
乐观锁天然需要重试,但无脑 while 循环会把 CPU 打满,且若重试前已发通知、调下游,失败后这些副作用不会自动回滚。
- 重试前加随机延迟:
Thread.sleep(10 + new Random().nextInt(50)),避免所有线程在同一毫秒撞同一行 - 限制最大重试次数(通常 3–5 次),超限抛
OptimisticLockException,由上层决定降级策略 - 每次重试都必须重新
SELECT当前最新version和业务字段——复用旧version会导致第二次也失败 - 整个“读→算→更”必须包在同一个事务里;否则中间状态可能被其他事务改掉,version 检查就只是最后一道补救
真正难的不是写对那条 UPDATE,而是把 version 值从查询带到更新、把影响行数检查嵌进业务流、把重试控制在合理边界内——这些地方一松动,乐观锁就退化成裸更新。











