【问题标题】:Rails / ActiveRecord: avoid two threads updating model at the same time with locksRails / ActiveRecord:避免两个线程同时使用锁更新模型
【发布时间】:2018-02-19 14:55:31
【问题描述】:

假设我需要确保ModelName 不能被两个不同的 Rails 线程同时更新;例如,当一个 webhook 发布到应用程序尝试修改它,同时一些其他代码正在运行时,就会发生这种情况。

根据Rails documentation,我认为解决方案是使用model_name_instance.with_lock,这也会开始一个新事务。

这可以正常工作并防止同时更新模型,但不会阻止其他线程在 with_lock 块运行时读取该表行。

我可以证明 with_lock 不会通过这样做来阻止其他 READS:

  • 打开 2 个导轨控制台;

  • 在控制台 1 上,输入类似ModelName.last.with_lock { sleep 30 }

  • 在控制台 2 上,输入 ModelName.last。您将能够毫无问题地读取模型。

  • 在控制台 2 上,输入 ModelName.update_columns(updated_at: Time.now)。您会看到它会等待 30 秒的锁定到期,然后再完成。

这证明锁不会阻止读取,据我所知,没有办法锁定数据库行不被读取。

这是有问题的,因为如果 2 个线程在完全相同的时间运行相同的方法,并且我必须决定运行 with_lock 块以了解先前对模型数据的一些检查,那么线程 2 可能正在读取陈旧的数据,这将是线程 1 在完成已经运行的 with_lock 块后很快就会更新它,因为线程 2 可以读取模型,而 with_lock 块在线程 1 中正在进行,它只能因为锁而无法更新它。

编辑:我找到了这个问题的答案,所以你可以停止阅读这里并直接进入下面:)

我的一个想法是开始 with_lock 块发布对模型的无害更新(例如 model_instance_name.update_columns(updated_at: Time.now)),然后使用 model_name_instance.reload 跟随它以确保它得到最新的更新数据。因此,如果两个线程同时运行相同的代码,那么只有一个线程能够发出第一个更新,而另一个线程需要等待锁被释放。一旦它被发布,它会跟着model_instance_name.reload 确保得到其他线程执行的任何更新。

问题是这个解决方案对我来说似乎太老套了,我不确定我是否应该在这里重新发明轮子(我不知道我是否遗漏了任何边缘情况)。如何确保当两个线程同时运行完全相同的方法时,一个线程等待另一个线程完成甚至读取模型?

【问题讨论】:

  • 能否展示部分代码,尤其是对数据库的操作?

标签: ruby-on-rails activerecord


【解决方案1】:

感谢 Robert 提供 Optimistic Locking 信息,我绝对可以看到我走那条路,但是 Optimistic Locking 的工作原理是在写入数据库时​​引发异常(SQL UPDATE),而且我有很多复杂的业务逻辑我什至不想一开始就使用陈旧的数据。

我就是这样解决的,比我想象的要简单。

首先,我了解到悲观锁定不会阻止任何其他线程读取该数据库行。

但我也了解到with_lock 也会立即启动锁定,无论您是否尝试写入。

所以如果你启动 2 个 rails 控制台(模拟两个不同的线程),你可以测试一下:

  • 如果您在控制台 1 上键入 ModelName.last.with_lock { sleep 30 },在控制台 2 上键入 ModelName.last,控制台 2 可以立即读取该记录。

  • 但是,如果您在控制台 1 上键入 ModelName.last.with_lock { sleep 30 } 并在控制台 2 上键入 ModelName.last.with_lock { p 'I'm waiting' },则控制台 2 将等待控制台 1 的锁定保持,即使它没有发出任何写操作。

所以这是“锁定读取”的一种方式:如果您有一段代码要确保它不会同时运行(即使是读取也不行!),开始该方法打开 @987654327 @block 并发出你的模型在其中读取,他们将等待任何其他锁首先被释放。如果你在它之外发出你的读取,你的读取将被执行,即使另一个线程中的一些其他代码已经锁定了该表行。

我学到的其他一些好东西:

  • 根据rails documentationwith_lock 不仅会启动一个带锁的事务,而且还会为您重新加载您的模型,因此您可以确定在块内ModelName.last 处于其最高位置-迄今为止的状态,因为它在该实例上发出.reload

  • 这是一些专门设计用于阻止同一段代码在多个线程中同时运行的 gem(我相信大多数 Rails 应用程序都在生产环境中),而不管数据库锁定如何。看看 redis-mutex、redis-semaphore 和 redis-lock。

  • 网上有很多文章(我至少可以找到 3 篇)指出 Rails with_lock 会阻止对数据库行进行 READ,而通过上面的测试我们可以很容易地看到情况并非如此。小心并始终确认自己测试的信息!我试图评论他们对此的警告。

【讨论】:

  • 阻止读取确实不是最佳做法。我希望你知道你在做什么。
  • @RobertPankowecki 在我们的案例中,这是合理的,因为我们有支付网关可以立即(在线)返回授权费用,但它也可以稍后通过 webhook 通知我们。问题是这个“稍后”可能会在几毫秒之后(并且它在生产中),所以我们面临内联代码正在处理授权付款并且 webhook 也在执行此操作(同时)的竞争条件。我们通过不关心 webhook 是否被重复调用来使我们的代码具有幂等性,但我们需要确保锁定读取,以便我们知道只有一个线程在运行代码。
  • 对一个棘手问题的出色而有益的讨论,尤其是在另一个线程中的 with_lock 没有阻止更正重新阅读
【解决方案2】:

你很接近,你想要乐观锁定而不是悲观锁定:http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html

它不会阻止读取对象并提交表单。但它可以检测到当用户看到对象的陈旧版本时提交了表单。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-10-18
    • 2020-10-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多