如何在Rails中避免简单的竞争条件?

时间:2012-12-11 18:42:18

标签: ruby-on-rails ruby-on-rails-3 postgresql

我有一个简单的竞争条件。我有一个网站,人们可以对照片进行投票,但最多允许10票。

当用户提交投票时,我会在照片表格中为该特定照片更新num_votes列。我这样做是为了便于查找投票数。

如何确保在同一交易中发生vote.save和num_votes更新?

谢谢!

4 个答案:

答案 0 :(得分:4)

为了达到这个目的,你必须使用某种锁定方式。基本上你有3个选择:乐观/悲观的rails锁定和一些外部锁定后端(如Redis :: Lock)。

如果高性能并非如此,我个人会选择悲观锁定

photo = Photo.find(photo_id)
photo.with_lock do
  photo.num_votes += 1
  photo.save!
end

我还应该指出,坚持仅包装递增num_votes并保存到一个事务中将无法解决竞争条件。默认情况下,大多数RDBMS都在读取提交模式下工作。这并不能阻止这种竞争条件。

仅供参考{I}见PessimisticOptimistic锁定参考

答案 1 :(得分:2)

如果这是一个简单的竞争条件,那么你应该将其解决为竞争条件。 尝试使用一些锁定机制。 Redis很不错: redis locking for ruby

RedisLocker.new('vote_#{@photo.id}').run! { @photo.vote }

# ... photo model
def vote
  if num_votes <= 10   
    self.num_votes += 1
    save
  end
end

答案 2 :(得分:0)

嗯,Rails / Postgres支持交易。您可以在任何ActiveRecord模型上简单地声明一个:

Photo.transaction do
 Vote.create(:whatever)
 Photo.votes = thing
 Photo.save!
end

如果在事务块期间引发异常(例如,通过在无效模型上调用.save!),则事务将回滚,并且在那里发生的任何数据库更改都未提交(在此例如,投票记录未插入)。当然,你仍然需要救援和处理例外。

顺便提一下,在记录中存储关联对象的数量以便于查找是一种非常常见的模式,称为计数器缓存,Rails也支持这些 - 您可能希望正式研究num_votes计数器缓存(默认名称为photos.votes_count,但不是必需的)。但是,您可能仍希望事务检查它是否超出限制。

答案 3 :(得分:0)

您不需要为此

显式锁定
Photo.where(:id => photo_id).where('num_votes < 10').update_all('num_votes = num_votes+ 1')

将更新该照片的投票数,但仅限于少于10票。您可以检查update_all的返回值以查看是否实际更新了任何内容:返回值是更新行的数量。如果更新失败,则不创建投票(或者如果您已经创建了投票,则回滚事务)。

乐观锁定使用类似的技术来检测并发更新的尝试:它为更新设置了一个条件,确保如果有人在你之前偷偷摸摸然后检查更新的行数,就不会发生任何事情。