ActiveRecord乐观锁多线程场景:2线程仅一次StaleObjectError异常原因咨询
为什么双线程仅触发一次乐观锁异常,三线程却多次触发?
这问题挺有意思的,咱们结合ActiveRecord乐观锁的核心逻辑,拆解不同线程数量下的行为差异:
首先先明确乐观锁的工作方式:
- ActiveRecord靠
lock_version字段标记记录版本(你这里定义成string不影响,框架会自动转成数值处理)。 - 执行
save时,框架会生成带版本校验的SQL:UPDATE users SET number = ?, lock_version = ? WHERE id = ? AND lock_version = ?。 - 如果这条SQL没命中任何记录(说明当前记录的版本和你读取时的不一样),就会抛出
StaleObjectError。
1. 单线程:完全无冲突(符合预期)
只有一个线程在操作,读取、修改、保存全是串行的,永远不会出现版本不匹配,自然不会抛异常。
2. 双线程:仅初始一次冲突
这是最关键的部分,咱们分阶段看:
- 第一次冲突(唯一的异常):两个线程几乎同时启动,都执行
User.find(1),拿到了初始的lock_version(比如0)。随机sleep后,假设线程A先完成save:把number改成自己的object_id,同时把lock_version升到1,这一步成功。 - 线程B重试后进入交替状态:线程B执行
save时,发现数据库里的lock_version已经是1,和自己手里的0对不上,抛出异常然后触发retry。这时候线程B重新查询,拿到最新的lock_version=1,把number改成自己的id,save成功,lock_version升到2。 - 后续循环无冲突:接下来的99次循环里,两个线程会进入一种稳定的交替更新模式:
- 线程A查询时,拿到的是线程B刚更新完的记录(版本N,number是B的id),改成自己的id后保存,版本升到N+1。
- 线程B查询时,拿到的是线程A刚更新完的记录(版本N+1,number是A的id),改成自己的id后保存,版本升到N+2。
- 哪怕有随机sleep,只要不是极端到两个线程同时读取同一个版本,每次查询到的都是最新版本,自然不会再触发异常。
3. 三线程:多次冲突(符合预期)
三个线程的场景就乱得多了:
- 很容易出现多个线程同时读取到同一个版本的情况,比如A、B、C都读到了
lock_version=2。 - 假设A先保存成功,版本升到3;B紧接着保存,版本不匹配抛异常;C这时候保存也会因为版本不对抛异常。
- 重试过程中还会继续出现冲突:比如B重试读到版本3,C也重试读到版本3,或者A又读到版本3,这时候又会有多个线程拿着同一个版本去保存,再次触发异常。
- 这种“抢版本”的情况会在100次循环里反复出现,所以你会看到数十次异常提示。
最后提个小细节:你把lock_version定义成string类型了,其实ActiveRecord乐观锁默认用integer类型,但这对当前问题的核心逻辑没影响,框架会自动处理版本递增。
内容的提问来源于stack exchange,提问作者Lasse Kliemann
相关产品推荐
相关产品推荐

