乐观锁用一个 version 做比较并交换,不加锁就能防住丢失更新。它在什么情况下好用、什么情况下白做、能不能被 ABA 欺骗、冲突严重时怎么重试?这篇用真实的 MySQL,把这几个问题的数字逐一量出来。
最终余额 103,丢了 697 次更新(87%)
读出后在应用里算再写回,别人的更新会被覆盖,800 次只剩 103。
最终余额 800(应为 800,丢 0 次);一共 5619 次尝试成功 800 次,平均每次成功试 7.02 次,p99 106 次,最多 189 次;4819 次尝试因 version 变了而更新 0 行(白做) 用时 0.55 s(1444 成功/秒),延迟中位数 0.68 ms,p99 84.93 ms
UPDATE ... WHERE version = ?,0 行就是冲突,重读重试;结果正确,代价是白做的尝试。
并发线程数 | 乐观锁 成功/秒 | 乐观锁 平均每次成功试几次 | FOR UPDATE 操作/秒 | 原子 UPDATE 操作/秒 ------+----------+---------------+-----------------+--------------- 1 | 4573 | 1.00 | 5092 | 6122 2 | 4162 | 1.71 | 5363 | 8485 4 | 3119 | 3.57 | 6359 | 7061 8 | 1612 | 7.03 | 6294 | 8118 16 | 833 | 13.94 | 5866 | 7805 32 | 417 | 27.57 | 5041 | 7188 线程越多,同一行上乐观锁的白做越多(每次成功要试的次数涨),吞吐不再增长甚至下降;悲观加锁让冲突的请求排队而不是重做
同一行线程数翻倍,每次成功要试的次数大约翻倍;悲观加锁让请求排队,吞吐稳定。
写法 | 操作/秒 | 平均每次成功试几次 -----------+------+---------- 乐观锁 | 8958 | 1.00 FOR UPDATE | 9958 | 1.00 没有冲突时乐观锁一次就成功,不需要任何等待;它的价值就在“冲突少”的场景
没有冲突时乐观锁一次成功,和悲观锁同一量级。
写法 | 试验次数 | 发货时订单上的支付单已不是发货方看到的那张(原单已退款)的次数 ---------------------------------+------+-------------------------------- 按状态值 CAS(WHERE status = 'PAID') | 300 | 208 按 version CAS(WHERE version = ?) | 300 | 0 ABA:状态值回到了“PAID”,CAS 看不出中间发生过退款和重新支付,于是按旧的支付单发了货;version 每次改动都会增加,不会“变回去”
按状态值做 CAS 看不出 PAID → REFUNDED → PAID 中间发生过什么;version 每次改动都递增,不会变回去。
重试策略 | 成功/秒 | 平均每次成功试几次 | 延迟 p99 | 白做的尝试(每轮) ------------+------+-----------+-----------+---------- 无退避(失败立刻重试) | 409 | 27.30 | 1484.1 ms | 168318 随机指数退避 | 3332 | 2.49 | 169.7 ms | 9544
冲突严重时无退避的重试把数据库打满,随机指数退避让总吞吐高 8 倍。