Ruby on rails 如何避免Rails中出现简单的争用情况?

Ruby on rails 如何避免Rails中出现简单的争用情况?,ruby-on-rails,ruby-on-rails-3,postgresql,Ruby On Rails,Ruby On Rails 3,Postgresql,我有一个简单的比赛条件。我有一个网站,人们可以对照片投票,但最多允许10票 当用户提交投票时,我会更新照片表中该特定照片的num_votes列。我这样做是为了方便查找投票数 如何确保vote.save和num_votes更新发生在同一事务中 谢谢 嗯,Rails/Postgres支持事务。您只需在任何ActiveRecord模型上声明一个: Photo.transaction do Vote.create(:whatever) Photo.votes = thing Photo.save!

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

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

如何确保vote.save和num_votes更新发生在同一事务中


谢谢

嗯,Rails/Postgres支持事务。您只需在任何ActiveRecord模型上声明一个:

Photo.transaction do
 Vote.create(:whatever)
 Photo.votes = thing
 Photo.save!
end
如果在事务块期间引发异常(例如,通过调用无效模型上的
.save!
),事务将回滚,并且不会提交在其中发生的任何数据库更改(在这种情况下,不会插入投票记录)。当然,您仍然需要拯救和处理异常


顺便说一句,在记录中存储关联对象的数量以便于查找是一种非常常见的模式,称为计数器缓存,Rails也支持这种模式-您可能希望正式地将
num\u vots
设置为计数器缓存(默认名称为
photos.vots\u count
,但这不是必需的)。不过,您可能仍然希望事务检查它是否超出限制。

为了实现这一点,您必须使用某种类型的锁定。基本上有3个选项:乐观/悲观rails锁定和一些外部锁定后端(如Redis::Lock)

如果这里没有高性能,我个人会选择悲观锁定

photo = Photo.find(photo_id)
photo.with_lock do
  photo.num_votes += 1
  photo.save!
end
我还应该指出,坚持只包装递增的num_投票并保存到一个事务中并不能解决竞争条件。大多数RDBMS默认在读提交模式下工作。这并不能阻止这样的比赛状态


仅供参考,请参阅和锁定参考

此操作不需要显式锁定

Photo.where(:id => photo_id).where('num_votes < 10').update_all('num_votes = num_votes+ 1')
Photo.where(:id=>Photo\u-id)。where('num\u-voces<10')。更新所有('num\u-voces=num\u-voces+1'))
将更新该照片的投票数,但前提是票数少于10票。您可以检查
update\u all
的返回值以查看是否有任何内容实际更新:返回值是更新的行数。如果更新失败,则不创建投票(或者如果已经创建了投票,则回滚事务)


乐观锁定使用类似的技术来检测并发更新的尝试:它在更新上设置一个条件,确保如果有人在您之前潜入其中,则不会发生任何事情,然后检查更新的行数。

如果这是一个简单的争用条件,则应将其作为争用条件解决。 尝试使用一些锁定机制。Redis很好用:

RedisLocker.new('vote#{@photo.id}')。快跑!{@photo.vote}
# ... 照片模型
def投票

如果num_投票,我相信如果两个用户同时投票(在并行事务中),这不会解决竞争条件问题。photo.num_投票将包含无效值(将错误地增加1,而不是2)。将postgresql事务隔离模式设置为“serializable”可以解决问题,但这是一个巨大的性能杀手这是一个公平的观点。我回答的是被问到的问题(“我如何在交易中实现这些事情”),而不是更一般的种族条件问题。
RedisLocker.new('vote_#{@photo.id}').run! { @photo.vote }

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