ActiveRecord触摸导致死锁

我的应用程序广泛使用touch ,以利用Rails的模板缓存系统。 当批量中的许多不同对象之间创建了许多关系时,我的应用程序会执行某种类型的工作。 有时,某些工作导致产生的级联touch导致死锁。

对于我经常看到它的一种情况,我可以围绕这个进行编码,但是看到它已经揭示了更大的问题,这可能发生在其他情况下,尽管它不太可能发生。

要理解这一点,请考虑两个人在Twitter上完全相同的时刻。 它们都单击“跟随”,导致在它们之间创建关系对象,然后touch它们的每个记录。 如果这些接触成为交织:

  1. 进程1触及用户A.
  2. 进程2触及用户B.
  3. 进程1触及用户B.
  4. 进程2触及用户A.

每个进程都使用数据库事务,因此这将导致死锁。

我错了,这可能发生在我奇怪的批处理作业场景之外的正常应用程序操作中吗? 如果我没错,有什么办法吗? 我可以以某种方式将touch移动到交易之外吗? (最后写的胜利无论如何都可以更新updated_at …)

更新 – 更多数据模型的解释

 class Follow belongs_to :follower, touch: true belongs_to :followee, touch: true end @u1 = User.find(1) @u2 = User.find(2) # Background Job 1 Follow.create!(follower: @u1, followee: @u2) # Background Job 2 Follow.create!(follower: @u2, followee: @u1) 

不确定是什么造成了死锁,但你可以在处理它们时在两个记录上添加悲观锁定,这将阻止另一个请求处理它们直到锁定被释放, ActiveRecord将在继续之前等待锁定释放。

 User.transaction do @u1, @u2 = User.lock.where(id: [1,2]) # Those two records are now locked, other transaction instances # can't proceed till this transaction block is exited Follow.create!(follower: @u1, followee: @u2) end # lock is released here 

注意:传递id: [2,1]不会按顺序返回它们,因此您需要处理该条件。

注意2:过多的锁定可能会影响您的整体应用程序性能,因为用户模型可能是一个使用频繁的模型,但我想这一切都取决于这些后续发生的频率。


更新:这是第二种也可能有效的方法,首先是跟随模型,没有触摸,而是after_create

 class Follow belongs_to :follower belongs_to :followee after_create :touch_users def touch_users # no locking and direct database update User.where(id: [follower.id, followee.id]).update_all(updated_at: :Time.now) end end 

然后控制器将执行正常的事务,或者根本不执行,因为您不需要它

 Follow.create!(follower: @u1, followee: @u2) 

注意: #update_all不会触发activerecord #update_all ,并且查询是直接在数据库上完成的,如果您有任何after_update方法,那么您可能希望避免使用此方法。