在Rails中防止父关系中的竞争条件

时间:2013-03-22 00:21:57

标签: mysql ruby-on-rails ruby locking race-condition

我有以下型号:

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'。 (不幸的结果是歌词以正确的顺序呈现。)

我已经阅读了关于乐观与悲观锁定等的无尽帖子,并尝试了各种选项,但似乎无法摆脱问题。每次用户创建一个Lyric时,锁定整个Song表似乎是一种矫枉过正。

这是防止这种情况发生的唯一方法吗? (这似乎是对表演的巨大打击)。 我的架构中是否存在根本错误的问题?我认为这是许多项目中的常见问题,但就我所知,它似乎并不太常见。看来,只要在after_create方法中实例化父关联,如果父模型(在本例中为Song)的创建依赖于另一个子的存在(在这种情况下),则存在竞争条件的可能性。 ,抒情)。

1 个答案:

答案 0 :(得分:0)

如果你不想锁定表格,有一种丑陋的方法可以防止这种情况发生:互斥体。

这样的事情:

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歌词并不比你当前的用户通过歌词更好地播放歌曲?