避免Rails中父关系中的竞争条件

4

我有以下模型:

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方法还可以将两个“歌曲”合并成一个,如果用户添加了关联歌词。换句话说,如果用户拥有一首歌的第一行和第三行,直到她添加了同一首歌的第二行,它们被视为两首不同的歌曲。
问题在于,当用户同时添加同一首歌的多个歌词(通过从搜索结果中选择若干个歌词)时,在MySQL中会偶尔发生竞态条件,即尽管这些歌词相邻且应该组合成单个“歌曲”,但会实例化两个歌曲模型。(不幸的是,这样会导致歌词以正确的顺序呈现。)
我已经阅读了无数关于乐观锁定与悲观锁定等的帖子,并尝试了各种选项,但似乎无法摆脱这个问题。每次用户创建歌词时,锁定整个Song表似乎是唯一的预防措施(这似乎对性能产生了巨大影响)。
这是防止此类问题发生的唯一方法吗?我的架构基本上有什么问题吗?我想象这在许多项目中都是一个常见问题,但据我所知,它似乎并不经常出现。似乎每当在after_create方法中实例化父级关联时,如果父模型的创建(在这种情况下为Song)取决于另一个子模型(在这种情况下为Lyric)的存在,则有竞态条件的可能性。

当用户从搜索中选择一大堆歌词,并将它们作为一首单曲添加时,你的控制器难道不知道正在创建(或更新)一首带有一组歌词的单曲吗?也许在那里创建这首歌会更好,而不是在歌词创建后的钩子函数中创建。 - Taryn East
你能添加add_to_song方法吗?在该方法中是否有一个条件来检查歌曲是否已经存在? - John
我已经添加了add_to_song方法。 - Andy
将add_to_song方法作为after_create钩子分离出来似乎更加清晰。 - Andy
1个回答

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歌词通过歌曲,是否比当前用户has_many歌曲通过歌词更好?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接