我有一个非常简单的Rails应用程序,它允许用户注册一组类(class)的出勤率。 ActiveRecord模型如下:
class Course < ActiveRecord::Base
has_many :scheduled_runs
...
end
class ScheduledRun < ActiveRecord::Base
belongs_to :course
has_many :attendances
has_many :attendees, :through => :attendances
...
end
class Attendance < ActiveRecord::Base
belongs_to :user
belongs_to :scheduled_run, :counter_cache => true
...
end
class User < ActiveRecord::Base
has_many :attendances
has_many :registered_courses, :through => :attendances, :source => :scheduled_run
end
ScheduledRun实例具有有限的可用位置,并且一旦达到限制,就不能再接受任何出勤。
def full?
attendances_count == capacity
end
Attenances_count是一个计数器缓存列,其中包含为特定ScheduledRun记录创建的出勤关联数。
我的问题是,当一个或多个人尝试同时注册类(class)中的最后一个可用位置时,我不完全了解确保不出现比赛条件的正确方法。
我的出勤 Controller 如下所示:
class AttendancesController < ApplicationController
before_filter :load_scheduled_run
before_filter :load_user, :only => :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
保存“出勤”模型后,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)
如果在保存新的出勤模型之前对ScheduledRun模型进行了后续更新,则应触发StaleObjectError异常。此时,如果尚未达到容量上限,则会重新尝试整个操作。
更新#2
在@kenn的响应之后,这里是SheduledRun对象上更新的Atten方法:
# 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
。当出现竞争状况时,并发进程尝试运行此确切查询,但只有第一个成功,因为第一个原子将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
这并不是要严格避免比赛条件,但实际上它在大多数情况下都应该起作用。
关于ruby-on-rails - 如何避免在Rails应用中出现竞争状况?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/3037029/