如何处理Rails中复杂的自定义验证的竞争条件?

时间:2018-05-30 21:34:03

标签: ruby-on-rails

人们正在使用我们的Rails网络应用程序进行预订。

我们在提供预订时获得高流量,有时两位访客在只有一位预订时获得有效预订。

我的验证有点复杂,但这里有一个简化的版本,可以让我们了解正在检查的部分内容:

validate :time_availability

def time_availability
  if Reservation.where(date: date, arrival_time: arrival_time).count >= ReservationMax.for(date, arrival_time)
    errors.add(:arrival_time, "This time is not available")
  end
end

如果其中一个请求保存时另外两个请求同时无效,那么您如何确保两个同时请求都不会保存?

4 个答案:

答案 0 :(得分:2)

由于潜在的竞争条件,我不确定模型验证是否会起作用 - 相反,您需要将其包装在事务中并向后执行:

date, arrival_time = @reservation.date, @reservation.arrival_time
Reservation.transaction do
  @reservation.save!
  unless Reservation.where(date: date, arrival_time:arrival_time).count >= ReservationMax.for(date, arrival_time)
    raise ActiveRecord::Rollback, "This time is not available"
  end
end

if @reservation.persisted?
  redirect_to @reservation
else
  redirect_to :somewhere_else
end

这会创建一个悲观的保存,并且只有在“验证”成功时才提交写入。这消除了正在运行的验证和正在执行的实际插入之间的潜在竞争条件。

答案 1 :(得分:0)

如果您需要执行更多操作,则需要将所有内容封装在事务中:

def create
  Reservation.transaction do
    reservation = Reservation.new(parsed_params)
    if reservation.save
      #do other things within the transaction
    else
      #...
    end
  end
end

请另请阅读:http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html

并且乐观锁定: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of-label-Concurrency+and+integrity

如果你有索引,你的代码应该引发一个StatementInvalid异常。

答案 2 :(得分:0)

尚未测试,但我有个主意。在与coorasse就数据库索引的答案进行评论之后,我找到了它。

如果在日期和时间已满时有另一个录制模型,那么before_save Reservation我可以创建该记录,数据库将处理唯一性。

新模型和表格的迁移:

class CreateFullReservationTimes < ActiveRecord::Migration[5.0]                                                                   
  def change
    create_table :full_reservation_times do |t|
      t.date :date
      t.datetime :arrival_time

      t.index [:date, :arrival_time], unique: true

      t.timestamps
    end
  end
end

before_save添加到Reservation模型:

class Reservation < ApplicationRecord
  before_save :add_full_reservation_time, if: :arrival_time_will_be_full?

  def add_full_reservation_time
    begin
      FullReservationTime.create!(date: date, arrival_time: arrival_time)                                                         
    rescue ActiveRecord::RecordNotUnique
      errors.add(:arrival_time, "This time is not available")
      throw :abort
    end
  end

  def arrival_time_will_be_full?
    Reservation.one_or_none_left_at?(date, arrival_time)                                                                                     
  end
end

这样,如果有一个(或没有)保留before save,它会尝试create一个FullReservationTime(以及是否存在竞争条件)同时进行两次&#34;最后一次预订&#34;由于ActiveRecord::RecordNotUniqe

,只有一次会成功

更新

我正在使用多个线程对此进行测试,以尝试同时保存多个记录。

如果我从一个预订开始,测试通过,只保存一个预订!

如果我从剩下的两个预订开始,并且同时运行3个线程,它会失败,因为它们都会在有时间之前保存,以检查是否有一个或者没有。

我从max的答案中尝试了这个方法(在事务中包装,在保存后执行验证,如果验证失败则引发ActiveRecord::Rollback,并且测试没有通过。所有线程都成功保存记录......

答案 3 :(得分:0)

我以一种非常有趣的方式解决了它,它不需要任何锁定,回滚或额外的事务包装,但在数据库级别使用唯一索引。

我在名为Reservation的{​​{1}}模型中添加了一列,并在seat_numberdatearrival_time上添加了唯一索引:

seat_number

然后,我使用class AddSeatNumberToReservations < ActiveRecord::Migration[5.0] def change add_column :reservations, :seat_number, :integer Reservation.update_all("seat_number=id") // so that existing reservations have a unique seat_number add_index :reservations, [:date, :arrival_time, :seat_number], unique: true end end 根据该日期和时间已存在多少预订来设置around_save,并从seat_number进行救援并使用新的座位号重试保存

ActiveRecord::RecordNotUnique

效果很好。

在我的多线程测试中,我将数据库池设置为15,将预留最大值(对于特定日期和时间)设置为9,并运行14个同时线程(除主线程外)尝试保存预留那个日期和时间。结果是按顺序对seat_numbers 1到9进行了9次预订,剩下的5次正常返回预订时出现了正确的错误。