Ruby on rails Rails/ActiveRecord:避免两个线程同时使用锁更新模型

Ruby on rails Rails/ActiveRecord:避免两个线程同时使用锁更新模型,ruby-on-rails,activerecord,Ruby On Rails,Activerecord,假设我需要确保两个不同的Rails线程不能同时更新ModelName;例如,当webhooks发布到应用程序时,在运行其他代码的同时尝试修改它,就会发生这种情况 根据,我认为解决方案是使用model\u name\u instance.with\u lock,这也会开始一个新的事务 这可以很好地工作,并防止同时更新模型,但它不会阻止其他线程在运行带锁定的块时读取该表行 我可以通过这样做证明带锁的不会阻止其他读取: 打开2个导轨控制台 在控制台1上,键入类似于ModelName.last.wi

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

根据,我认为解决方案是使用
model\u name\u instance.with\u lock
,这也会开始一个新的事务

这可以很好地工作,并防止同时更新模型,但它不会阻止其他线程在运行带锁定的
块时读取该表行

我可以通过这样做证明带锁的
不会阻止其他读取:

  • 打开2个导轨控制台

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

  • 在控制台2上,键入
    ModelName.last
    。你可以很容易地读懂模型

  • 在控制台2上,键入
    ModelName.update\u列(更新时间:Time.now)
    。您将看到它将等待30秒的锁过期,然后才完成

这证明了锁不会阻止读取,而且据我所知,无法锁定数据库行以防止读取

这是有问题的,因为如果两个线程在完全相同的时间运行相同的方法,并且我必须决定运行
with_lock
块来检查模型数据,线程2可能正在读取过时的数据,这些数据将在线程1完成已运行的
带锁
块后很快由线程1更新,因为线程2可以读取模型,而
带锁
块在线程1中进行,它只能因为锁而无法更新

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

我的一个想法是在
开始时使用_lock
块对模型进行无害的更新(比如
model\u instance\u name.update\u columns(更新时间:Time.now)
),然后使用
model\u name\u instance.reload
来确保它获得最新的数据。因此,如果两个线程同时运行相同的代码,则只有一个线程能够发出第一次更新,而另一个线程则需要等待释放锁。一旦发布,它后面会跟着
model\u instance\u name.reload
,以确保获得其他线程执行的任何更新


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

如果您很接近,您希望使用乐观锁定而不是悲观锁定:


它不会阻止读取对象和提交表单。但是它可以检测到表单是在用户看到对象的过时版本时提交的。

感谢Robert提供的乐观锁定信息,我肯定可以看到我走了这条路,但是乐观锁定通过在写入数据库时引发异常(SQL更新)来工作,我有很多复杂的业务逻辑,我甚至不想在一开始就用陈旧的数据运行这些逻辑

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

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

但我还了解到,无论您是否尝试写入,带锁的
也会立即启动锁

因此,如果启动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的锁保持,即使它没有发出任何写入操作

因此,这是一种“锁定读取”的方法:如果您有一段代码,您希望确保它不会同时运行(甚至不用于读取!),则从该方法开始,使用_lock
块打开
,并在其中发出模型读取,它们将等待任何其他锁定首先被释放。如果您在它之外发出读取,那么即使另一个线程中的其他代码段在该表行上有锁,也会执行读取

我还学到了其他一些好东西:

  • 根据,
    with_lock
    不仅会使用锁启动事务,还会为您重新加载模型,因此您可以确保块
    ModelName.last
    处于最新状态,因为它会在该实例上发出
    .reload

  • 这是一些专门为阻止同一段代码同时在多个线程中运行而设计的gem(我相信大多数Rails应用程序在生产环境中运行时都是这样),而不考虑数据库锁。看看redis互斥、redis信号量和redis锁

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


您能否显示部分代码,尤其是DB的操作?阻止读取实际上并不重要