防止Rails中父关系中的竞争条件
Posted
技术标签:
【中文标题】防止Rails中父关系中的竞争条件【英文标题】:Preventing race condition in parent relation in Rails 【发布时间】:2013-03-11 17:37:07 【问题描述】:我有以下型号:
class Lyric < ActiveRecord::Base
belongs_to :user
belongs_to :song
after_create :add_to_song
end
class Song < ActiveRecord::Base
belongs_to :user
has_many :lyrics
end
这个想法是用户可以为一首歌添加任意数量的歌词。如果为该用户尚不存在的新歌曲输入歌词,则会为该用户创建一首新歌曲。这是通过调用 after_create 方法“add_to_song”来实现的,该方法检查用户是否有该歌曲的歌词:
def add_to_song
sl = self.song_line
# Check for adjacent songs
prior_song = Song.where(:user_id => self.user.id,
:title=> sl.title,
:artist => sl.artist,
:last_line => sl.linenum-1).first
next_song = Song.where(:user_id => self.user.id,
:title=> sl.title,
:artist => sl.artist,
:frst_line => sl.linenum+1).first
# Case 1 - No existing song
if !prior_song && !next_song
song = Song.create!(:user_id => self.user.id,
:length => 1,
:title=> sl.title,
:artist => sl.artist,
:frst_line => sl.linenum,
:last_line => sl.linenum )
self.update_attribute( :song_id, song.id )
# Case 2 - Lyric is between two songs -> merge songs
elsif prior_song && next_song
prior_song.absorb( next_song, self )
# Case 3 - Lyric is new first lyric of existing song
elsif next_song
next_song.expand( self )
# Case 4 - Lyric is new last lyric of existing song
else
prior_song.expand( self )
end
end
如果链接歌词是由用户添加的,那么 add_to_song 方法还会将两首“歌曲”合二为一。换句话说,如果用户拥有一首歌曲的第 1 行和第 3 行,则在她添加同一首歌曲的第 2 行之前,它们将被视为两首不同的歌曲。
问题
当用户同时从同一首歌曲中添加多个歌词时(通过从搜索结果中选择其中的一些歌词),mysql 中偶尔会出现竞争条件,并且会为同一首歌曲实例化两个歌曲模型,即使歌词是相邻的彼此,应组合成一首“歌曲”。 (这样做的不幸结果是歌词以正确的顺序呈现。)
我已经阅读了无数关于乐观与悲观锁定等的帖子,并尝试了各种选择,但似乎无法摆脱这个问题。每次用户创建歌词时锁定整个 Song 表似乎有点过头了。
这是防止这种情况发生的唯一方法吗? (这似乎对性能造成了巨大影响)。 我的架构中是否存在根本性错误?我想这是许多项目中的常见问题,但据我所知,它似乎并没有出现太频繁。似乎任何时候在 after_create 方法中实例化父关联时,如果父模型(在本例中为 Song)的创建依赖于另一个子模型(在本例中, 歌词)。
【问题讨论】:
当用户从搜索中选择一大堆歌词并将它们添加为一首歌曲时 - 您的控制器不知道这是一首正在创建(或更新)的歌曲,其中包含一组歌词?也许在那儿创作歌曲比在 lyric-after-create 钩子中创作歌曲更好? 可以添加add_to_song方法吗?您在该方法中是否有条件来检查歌曲是否已存在? 我已经添加了 add_to_song 方法。 将 add_to_song 方法分离为 after_create 钩子似乎更简洁。 【参考方案1】:如果您不想锁定表,有一种丑陋的方法可以防止这种情况发生:互斥锁。
类似这样的:
File.open(MUTEX_FILE_PATH, "w") unless File.exists?(MUTEX_FILE_PATH)
mutex = File.new(MUTEX_FILE_PATH,"r+")
begin
mutex.flock(File::LOCK_EX)
...code...
ensure
mutex.flock(File::LOCK_UN)
end
可以在不阻塞整个表格的情况下工作。您可以更好地提高性能并使用用户 ID 创建互斥锁,这样该块将适用于每个用户,而不是任何人。
我并没有真正得到检查下一首歌曲和检查上一首歌曲的内容,但如果你通过歌曲执行用户 has_many 的歌词不会比你当前的用户通过歌词的 has_many 歌曲效果更好?
【讨论】:
以上是关于防止Rails中父关系中的竞争条件的主要内容,如果未能解决你的问题,请参考以下文章