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

san*_*e89 10 activerecord ruby-on-rails

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

Per Rails文档,我认为解决方案是使用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中的块正在进行时CAN读取模型,它只能因为锁定而无法更新它.

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

我的一个想法是开始with_lock阻止对模型发出无害更新(model_instance_name.update_columns(updated_at: Time.now)例如),然后用a跟随它以model_name_instance.reload确保它获得最新的数据.因此,如果两个线程同时运行相同的代码,则只有一个线程能够发出第一个更新,而另一个线程需要等待释放锁定.一旦它被释放,它将遵循以model_instance_name.reload确保获得由另一个线程执行的任何更新.

问题是这个解决方案对我来说似乎过于苛刻,而且我不确定我应该在这里重新发明轮子(我不知道我是否缺少任何边缘情况).如何确保当两个线程在同一时间运行完全相同的方法时,一个线程等待另一个线程完成甚至读取模型?

san*_*e89 10

感谢Robert提供的乐观锁定信息,我绝对可以看到我走这条路,但是乐观锁定通过在写入数据库的时刻引发异常(SQL UPDATE),并且我有很多复杂的业务逻辑我不会甚至想要首先使用陈旧数据运行.

这就是我解决它的方式,它比我想象的更简单.

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

但我也了解到with_lock,无论你是否尝试写作,我都会立即锁定锁定.

因此,如果您启动2个rails控制台(模拟两个不同的线程),您可以测试:

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

  • 如果您ModelName.last.with_lock { sleep 30 }在控制台1和ModelName.last.with_lock { p 'I'm waiting' }控制台2上键入,控制台2将等待控制台1的锁定保持,即使很难,也不会发出任何写入.

所以这是一种"锁定读取"的方式:如果你有一段代码要确保它不会同时运行(甚至不能读取!),那么开始该方法打开一个with_lock块并发出你的模型在里面读到他们会等待任何其他锁首先被释放.如果你在它之外发出你的读取,你的读取将被执行甚至很难在另一个线程中的一些其他代码片段锁定该表行.

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

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

  • 这是Web上的很多文章(我发现至少有3篇)说明Rails with_lock会阻止对数据库行的READ,而我们可以很容易地看到上面的测试并非如此.小心,并始终确认自己测试信息!我试图评论他们对此的警告.

  • @RobertPankowecki在我们的案例中,这是有道理的,因为我们拥有可以立即(按行)退还授权费用的支付网关,但它也可以稍后通过网络钩子通知我们结束。问题是这种“稍后”可能会在几毫秒后(并且正在生产中)出现,因此我们面临着竞争激烈的条件,即内联代码正在处理授权付款,而webhooks也正在这样做(同时)。我们通过不关心是否反复调用webhook来使代码具有幂等性,但是我们需要确保锁定读取,因此我们知道只有一个线程在运行代码。 (2认同)