gpt4 book ai didi

ruby-on-rails - 如何避免 Rails 应用程序中的竞争条件?

转载 作者:太空狗 更新时间:2023-10-30 01:40:21 26 4
gpt4 key购买 nike

我有一个非常简单的 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

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

我的问题是,我不完全知道当 1 个或多个人同时尝试注册类(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 对象上的参与方法:

# 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)

当您更新parent 对象中的属性时,您通常会看到以下 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/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com