首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >如何在Rails中处理复杂的自定义验证中的竞争条件?

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

Stack Overflow用户
提问于 2018-05-31 05:34:03
回答 3查看 334关注 0票数 2

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

当预订可用时,我们会获得很高的流量,有时当只剩下一个游客时,两个游客会得到一个有效的预订。

我的验证有点复杂,但这里有一个简化的版本,它给出了正在检查的部分的概念:

代码语言:javascript
复制
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

如何确保两个同时发出的请求不会同时保存,而其中一个保存会使另一个请求无效?

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2018-06-02 07:31:42

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

我向Reservation模型添加了一个名为seat_number的列,并在datearrival_timeseat_number上添加了一个唯一索引

代码语言:javascript
复制
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中解救出来,然后使用新的座位号重试保存

代码语言:javascript
复制
class Reservation < ApplicationRecord
  around_save :check_for_race_condition

  def check_for_race_condition
    seat_count = Reservation.where(date: date, arrival_time: arrival_time).count
    begin
      self.seat_number = seat_count + 1
      yield
    rescue ActiveRecord::RecordNotUnique
      if seat_count + 1 >= ReservationMax.for(date, arrival_time)
        errors.add(:arrival_time, "This time is not available")
      else
        seat_count += 1
        retry
      end
    end
  end
end

它工作得很漂亮。

在我的多线程测试中,我将db池设置为15,将预留最大值(针对特定日期和时间)设置为9,并同时运行14个线程(主线程除外),尝试保存该日期和时间的预留。结果是seat_numbers为1到9的9个保留,其余5个优雅地返回具有正确错误的保留。

票数 2
EN

Stack Overflow用户

发布于 2018-05-31 12:47:21

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

代码语言:javascript
复制
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

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

票数 2
EN

Stack Overflow用户

发布于 2018-05-31 05:56:14

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

代码语言:javascript
复制
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异常。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/50613652

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档