ActiveRecord::StaleObject 在新选项卡上打开每个结果时出错

ActiveRecord::StaleObject error on opening each result on a new tab

最近我们在 RoR 应用程序中添加了一项功能,允许用户打开特定记录,比方说在他们自己的个人选项卡中。这样做后,我们开始经常看到 ActiveRecord::StaleObject 错误。在调查这个问题时,我发现 rails 确实在尝试在选项卡中打开资源并引发异常时首先更新会话存储。

我们的活动记录会话存储中有 lock_version,因此 Rails 默认将其视为乐观锁定。有没有什么方法可以在不引入太多复杂性的情况下解决这个问题,因为应用程序已经在客户端的机器上运行,并且不会影响我们存储在会话存储数据库中的任何会话数据。

如有任何建议,我们将不胜感激。谢谢

听起来您正在对数据库会话记录使用乐观锁定并在处理对其他记录的更新时更新会话记录。不确定您需要在会话中更新什么,但如果您担心会话对象的更新可能存在冲突(并且需要锁定),那么可能需要这些错误。

如果不这样做 - 您可以在保存会话之前刷新会话对象(或禁用它的乐观锁定)以避免这些会话更新出现此错误。

您还可以查看正在更新的会话的内容以及它是否绝对必要。如果您正在更新类似“last_active_on”的内容,那么您最好发送一个后台作业来执行此 and/or 使用 update_column 方法绕过相当重量级的 activerecord 保存回调链.

---更新---

模式:在后台作业中添加副作用

有几种常见的 Rails 模式会随着应用使用量的增长而开始失效。我 运行 最常见的情况之一是当特定记录的控制器端点也更新 common/shared 记录时(例如,如果创建 'message' 也会更新 messages_count 用于使用 counter cache 的用户,或在会话中更新 last_active_at)。这些模式会在您的应用程序中造成瓶颈,因为您的应用程序中的多种不同类型的请求将不必要地竞争相同数据库行上的写锁。

随着时间的推移,这些问题往往会逐渐渗透到您的应用程序中,并且在以后变得难以重构。我建议 always 处理异步作业中请求的副作用(使用类似 Sidekiq 的方法)。类似于:

class Message < ActiveRecord::Base
  after_commit :enqueue_update_messages_count_job
  def enqueue_update_messages_count_job
    Jobs::UpdateUserMessageCountJob.enqueue(self.id)
  end
end

虽然乍一看这似乎有点矫枉过正,但它创建了一个可扩展性显着提高的架构。如果计算消息的速度变慢......那会使工作变慢但不会影响产品的可用性。此外,如果某些活动创建了许多具有相同副作用的对象(假设您有一个“注册”控制器,它为用户创建了一堆对象,这些对象都会触发 user.updated_at 的更新),这就变得容易了丢弃重复作业并防止更新同一字段 20 次。

模式:跳过 activerecord 回调链

在 ActiveRecord 对象上调用 save 运行 验证和所有 beforeafter 回调。这些可能很慢而且(有时)是不必要的。例如,更新 message_count 缓存值不一定关心用户的电子邮件地址是否有效(或任何其他验证),您可能不关心其他回调 运行ning。如果您只是更新用户的 updated_at 值以清除缓存,则类似。您可以通过调用 user.update_attribute(:message_count, ..) 将该字段直接写入数据库来绕过 activerecord 回调链。从理论上讲,这对于设计良好的应用程序来说不是必需的,但实际上一些 larger/legacy 代码库可能会大量使用 activerecord 回调链来处理您可能不想调用的业务逻辑。

--- 更新 #2 ---

关于死锁

避免更新(或通常锁定)来自并发请求的 common/shared 对象的一个​​原因是它会引入死锁错误。

一般来说,数据库中的“死锁”是指两个进程都需要对方拥有的锁。两个线程都无法继续,因此它必须出错。实际上,很难检测到这一点,因此某些数据库(如 postgres)只是在线程等待 exclusive/write 锁 x 时间后抛出“死锁”错误。虽然锁争用很常见(例如,两个更新都在更新 'session' 对象),但真正的死锁通常很少见(线程 A 在线程 B 需要的会话上有锁,但线程 B 有锁在线程 A 需要的不同对象上),因此您可以通过查看/延长死锁超时来部分解决问题。虽然这可能会减少错误,但它并不能解决线程可能正在等待直到死锁超时的问题。另一种方法是设置较短的死锁超时和 rescue/retry 几次。