如何在Rails应用程序中避免竞争条件?

我有一个非常简单的Rails应用程序,允许用户在一组课程中注册他们的出勤。 ActiveRecord模型如下:

class Course < ActiveRecord::Base has_many :scheduled_runs ... end class ScheduledRun  :attendances ... end class Attendance  true ... end class User  :attendances, :source => :scheduled_run end 

ScheduledRun实例具有有限数量的可用位置,一旦达到限制,就不能再接受更多的考勤。

 def full? attendances_count == capacity end 

attendances_count是一个计数器缓存列,包含为特定ScheduledRun记录创建的出勤关联数。

我的问题是,当一个或多个人同时尝试在课程中注册最后一个可用位置时,我不完全知道确保不会发生竞争条件的正确方法。

我的考勤控制器如下所示:

 class AttendancesController  :create def new @user = User.new end def create unless @user.valid? render :action => 'new' end @attendance = @user.attendances.build(:scheduled_run_id => params[:scheduled_run_id]) if @attendance.save flash[:notice] = "Successfully created attendance." redirect_to root_url else render :action => 'new' end end protected def load_scheduled_run @run = ScheduledRun.find(params[:scheduled_run_id]) end def load_user @user = User.create_new_or_load_existing(params[:user]) end end 

如您所见,它没有考虑ScheduledRun实例已达到容量的位置。

任何有关这方面的帮助将不胜感激。

更新

在这种情况下,我不确定这是否是执行乐观锁定的正确方法,但这就是我所做的:

我在ScheduledRuns表中添加了两列 –

 t.integer :attendances_count, :default => 0 t.integer :lock_version, :default => 0 

我还为ScheduledRun模型添加了一个方法:

  def attend(user) attendance = self.attendances.build(:user_id => user.id) attendance.save rescue ActiveRecord::StaleObjectError self.reload! retry unless full? end 

保存Attendance模型后,ActiveRecord将继续并更新ScheduledRun模型上的计数器缓存列。 这是显示发生这种情况的日志输出 –

 ScheduledRun Load (0.2ms) SELECT * FROM `scheduled_runs` WHERE (`scheduled_runs`.`id` = 113338481) ORDER BY date DESC Attendance Create (0.2ms) INSERT INTO `attendances` (`created_at`, `scheduled_run_id`, `updated_at`, `user_id`) VALUES('2010-06-15 10:16:43', 113338481, '2010-06-15 10:16:43', 350162832) ScheduledRun Update (0.2ms) UPDATE `scheduled_runs` SET `lock_version` = COALESCE(`lock_version`, 0) + 1, `attendances_count` = COALESCE(`attendances_count`, 0) + 1 WHERE (`id` = 113338481) 

如果在保存新的Attendance模型之前对ScheduledRun模型进行后续更新,则应触发StaleObjectErrorexception。 此时,如果尚未达到容量,则再次重试整个过程。

更新#2

继@ kenn的回复后,这是SheduledRun对象上更新的参与方法:

 # creates a new attendee on a course def attend(user) ScheduledRun.transaction do begin attendance = self.attendances.build(:user_id => user.id) self.touch # force parent object to update its lock version attendance.save # as child object creation in hm association skips locking mechanism rescue ActiveRecord::StaleObjectError self.reload! retry unless full? end end end 

乐观锁定是可行的方法,但正如您可能已经注意到的那样,您的代码永远不会引发ActiveRecord :: StaleObjectError,因为has_many关联中的子对象创建会跳过锁定机制。 看看下面的SQL:

 UPDATE `scheduled_runs` SET `lock_version` = COALESCE(`lock_version`, 0) + 1, `attendances_count` = COALESCE(`attendances_count`, 0) + 1 WHERE (`id` = 113338481) 

更新对象中的属性时,通常会看到以下SQL:

 UPDATE `scheduled_runs` SET `updated_at` = '2010-07-23 10:44:19', `lock_version` = 2 WHERE id = 113338481 AND `lock_version` = 1 

上面的语句显示了如何实现乐观锁定:注意WHERE子句中的lock_version = 1 。 当竞争条件发生时,并发进程尝试运行这个确切的查询,但只有第一个成功,因为第一个进程primefaces地将lock_version更新为2,后续进程将无法找到记录并引发ActiveRecord :: StaleObjectError,因为同一记录不再有lock_version = 1

因此,在您的情况下,可能的解决方法是在创建/销毁子对象之前触摸父对象,如下所示:

 def attend(user) self.touch # Assuming you have updated_at column attendance = self.attendances.create(:user_id => user.id) rescue ActiveRecord::StaleObjectError #...do something... end 

这并不意味着要严格避免竞争条件,但实际上它应该适用于大多数情况。

难道你不必测试@run.full?

 def create unless @user.valid? || @run.full? render :action => 'new' end # ... end 

编辑

如果添加如下validation怎么办?

 class Attendance < ActiveRecord::Base validate :validates_scheduled_run def scheduled_run errors.add_to_base("Error message") if self.scheduled_run.full? end end 

如果关联的scheduled_run已满,则不会保存@attendance

我没有测试过这段代码...但我相信没关系。