乐观锁到底是怎么发现冲突的?冲突之后该怎么办?为什么加了 @Version 还是丢更新?批量 update 会不会绕过它?这篇在 Spring Boot 4.0.8、Hibernate 7、MySQL 上用 8 个线程撞同一行实测。
初始 version = 0 SQL:update accv set balance=?,version=? where id=? and version=? SQL:update accv set balance=?,version=? where id=? and version=? SQL:update accv set balance=?,version=? where id=? and version=? 3 次修改之后 version = 3,balance = 130 -- 两个事务同时改同一行(A 先读,B 完整地改完提交,A 再改并提交) A 读到的 version = 3,B 提交后 A 准备提交 A 提交失败:ObjectOptimisticLockingFailureException:Unexpected row count (expected row count 1 but was 0) [update accv set balance=?,version=? where id=? and version=?] 最终 balance = 1130(B 的 +1000 保住了,A 的 +1 没有写进去),version = 4
update … set version=新 … where version=旧;命中 0 行就抛 ObjectOptimisticLockingFailureException。
没有 @Version:最终 balance = 131(应为 300),比应得的少 169;乐观锁冲突 0 次;放弃 0 次;总尝试 200 次;用时 73 ms @Version,失败就放弃:最终 balance = 131(应为 300),比应得的少 169;乐观锁冲突 169 次;放弃 169 次;总尝试 200 次;用时 53 ms @Version,失败重试(最多 20 次,随机退避):最终 balance = 300(应为 300),比应得的少 0;乐观锁冲突 72 次;放弃 0 次;总尝试 272 次;用时 174 ms
乐观锁只发现冲突,要成功得靠重试;不重试就是显式失败而不是丢更新。
B 用 update(不带 versioned) 加 50,A(已读到旧值 100)加 10 并提交:成功;最终 balance = 110(两笔都应该保住,应为 160),version = 1 B 用 update versioned 加 50,A 提交:失败 ObjectOptimisticLockingFailureException:Unexpected row count (expected row count 1 but was 0) [update accv set balance=?,version=? where id=? and version=?];最终 balance = 150,version = 1
批量 JPQL 不改 version,要写 update versioned。
save(带旧 version=0 的游离对象,而库里已经是 version=1):失败 ObjectOptimisticLockingFailureException:Row was already updated or deleted by another transaction for entity [demo.AccV with id '1'] save(没带 version 的游离对象):失败 DataIntegrityViolationException:Duplicate entry '1' for key 'accv.PRIMARY' 重新 find 一个最新的实体、再把表单里的字段拷进去(丢掉了页面上带回来的旧 version):提交成功,最终 balance = 200,version = 2——别人改的 500 被静默覆盖了
旧 version 要带回到更新语句;丢了它就会静默覆盖。