人们正在使用我们的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
如果其中一个请求保存时另外两个请求同时无效,那么您如何确保两个同时请求都不会保存?
答案 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
如果你有索引,你的代码应该引发一个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_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
效果很好。
在我的多线程测试中,我将数据库池设置为15,将预留最大值(对于特定日期和时间)设置为9,并运行14个同时线程(除主线程外)尝试保存预留那个日期和时间。结果是按顺序对seat_numbers 1到9进行了9次预订,剩下的5次正常返回预订时出现了正确的错误。