You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:25:58