人们正在使用我们的Rails web应用程序进行预订。
当预订可用时,我们会获得很高的流量,有时当只剩下一个游客时,两个游客会得到一个有效的预订。
我的验证有点复杂,但这里有一个简化的版本,它给出了正在检查的部分的概念:
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
如何确保两个同时发出的请求不会同时保存,而其中一个保存会使另一个请求无效?
发布于 2018-06-02 07:31:42
我以一种非常有趣的方式解决了这个问题,它不需要任何锁定、回滚或额外的事务包装,而是在数据库级别使用唯一索引。
我向Reservation
模型添加了一个名为seat_number
的列,并在date
、arrival_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
中解救出来,然后使用新的座位号重试保存
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个优雅地返回具有正确错误的保留。
发布于 2018-05-31 12:47:21
由于潜在的竞争条件,我不确定模型验证在这里是否有效-相反,您需要将其包装在事务中并向后执行:
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
这会创建一个悲观的保存,并且只有在“验证”成功时才会提交写入。这消除了正在运行的验证和正在执行的实际插入之间的潜在竞争条件。
发布于 2018-05-31 05:56:14
如果您需要执行更多操作,则需要将所有内容封装在一个事务中:
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
如果你有一个索引,你的代码应该引发一个StatementInvalid异常。
https://stackoverflow.com/questions/50613652
复制相似问题