问题
My app uses touch extensively in order to take advantage of Rails' template caching system. There's a certain type of work my app does when many relationships are created between many different objects in a batch. Sometimes, some of this work results in the resulting cascading touches causing deadlock.
I can code around this for the one scenario where I am seeing it happen often, but seeing it has brought to light the larger problem, which could happen in other scenarios, albeit it's very unlikely.
To understand this, think about two people following one another on Twitter at exactly the same moment. They both click "Follow", resulting in the relationship objects being created between them and then each of their records being touched. If these touches become interweaved:
- process 1 touches user A
- process 2 touches user B
- process 1 touches user B
- process 2 touches user A
Each process is using a database transaction, so this will result in deadlock.
Am I wrong that this could happen in normal app operation outside of my weird batch job scenario? If I'm not wrong, is there any solution? Can I somehow move the touches to be outside of the transactions? (Last Write Wins is fine for updating updated_at anyway...)
update - more explanation of data models
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)
回答1:
Not sure what makes that deadlock but you could add a pessimistic lock on both records while you're handling them, this will prevent another request from handling them until the lock is released, ActiveRecord will wait for the lock release before proceeding.
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
Note: passing id: [2,1] won't return them in that order, so you'll need to handle that condition.
Note 2: Too much locking might affect your overall app performance, since the user model is probably a heavily used model, but I guess it all depends on how often these follows happen.
Update: Here's a second way that also might work, first the follow model, no touches, but instead an
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
Then the controller would do a normal transaction, or not at all, cause you don't need it
Follow.create!(follower: @u1, followee: @u2)
NOTE: #update_all doesn't fire the activerecord call backs and the queries are done directly on the database, if you have any after_update methods then you might want to avoid this method.
来源:https://stackoverflow.com/questions/29092573/activerecord-touch-causing-deadlocks