我有一个接收汽车预订请求的应用程序,并且cars
表中的汽车预订状态应设置为in_use
。
通常会执行以下操作:
def reserve_car(user_id)
car = Car.find_by(status: 'available')
car.update_columns(user_id: user_id, status: 'in_use')
car
end
但是此解决方案在选择可用汽车和更新其状态之间造成了差距,然后当应用程序必须处理相对大量的请求时出现了难题,因此在同一时间内很少有请求尝试保留同一辆汽车。
为减少这种风险,我在同一SQL查询中找到并更新了可用的汽车。我也将可用的汽车列表随机化,以进一步减少它。为了使结果随机化,我不使用ORDER BY RAND() LIMIT 1
,因为AFAIK会为每条记录生成随机id,仅在将结果限制为指定数字-1时才对其进行排序。将来的记录数量(超过100k)。
所以我想出了这个解决方案:
def reserve_car(user_id)
sql = <<-SQL
UPDATE
cars AS r0,
(
SELECT
r1.id
FROM
cars AS r1
JOIN (
SELECT
(
RAND() * (
SELECT
MAX(id)
FROM
cars
)
) AS id
) AS r2
WHERE
r1.status = 'available'
AND r1.id >= r2.id
LIMIT
1
) AS r3
SET
r0.status = 'in_use',
r0.user_id = #{ActiveRecord::Base.connection.quote(user_id)}
WHERE
r0.id = r3.id
SQL
updates = ActiveRecord::Base.connection.exec_update(sql)
car = Car.find_by(user_id: user_id, status: 'in_use')
if car.present?
car
else
raise "Failed to reserve car. Updates: #{updates}"
end
end
但是我经常收到异常“无法预订汽车。更新0”,尽管事实上我知道有很多可用的汽车。
可能是什么问题?也许有人可以提出更好的解决方案?
谢谢
答案 0 :(得分:1)
您不会说正在使用什么数据库,但是如今,大多数主要的DB都可以执行更新并从已更新的行中返回数据
例如,在oracle中:
update car
set in_use = 1
where in_use = 0 and id = (select min(id) from car where in_use = 0)
returning id into car_id_that_was_set_in_use
参数car_id_that_was_set_in_use将包含已预订汽车的ID
作为一个建立锁并且长时间不使事务保持打开状态的单个操作,它不会引起任何争用
MySQL似乎是一个明显的例外-我没有发现任何迹象表明MySQL支持UPDATE..RETURNING
之类的东西,但是还有其他解决方法,例如innodb支持SELECT..FOR UPDATE
以允许您锁定您想要更新的记录,以及涉及可能类似于以下内容的变量的黑客攻击:
UPDATE car SET
in_use = 1, id = @affectedid := id
WHERE in_use = 0 AND id=(SELECT MIN(id) FROM car WHERE in_use = 0);
SELECT @affectedid;
尽管进行测试;我从来没有使用过它,而是从SO答案中改编了它
尽管效率较低,但您也可以对前端应用程序进行编码以使其轮循。这是伪代码,因为我不做红宝石:
int rowsupdated = 0
int potentialId = -1
while(rowsupdated = 0 and potentialId is not null) {
potentialId = sql_scalar("SELECT MIN(id) FROM car WHERE in_use = 0")
rowsupdated = sql_nonquery("UPDATE car SET in_use = 1 WHERE in_use = 0 and id = " + potentialId)
}
if(potentialId is null)
//there was no car to book, we tried them all - potentialId would only be null if there were no more cars
else
//potentialId now contains the id of the car we booked
while循环将继续进行,直到预订了汽车。它既幼稚又效率低下,但提出了一个重要的观点,也适用于先前的查询
更新查询必须引用我们仍期望的in_use值
您不能选择一个ID,只能继续设置in_use = 1,而不考虑在我们空闲时是否有人设置了in_use = 1。这称为开放式并发-您希望没有其他人更改要编辑的行上的数据,但是您包含了有关该行的所有数据,因此,如果其他人DID更改了该行,则更新将失败并返回0记录已更新。如果其他人在空闲时将in_use设置为1,则更新失败,并且我们将更新条件设为in_use仍为0,以使更新成功。 如果更新返回0,则可以假定其他人在此之前更改了行。然后,知道我们没有得到那一行,我们尝试另一行(或做出覆盖/合并/接受对方更改的决定)
答案 1 :(得分:1)
如果您正在使用带有活动记录的Rails,则一旦找到该行车记录,就应该能够将其锁定,以防止其他请求检索到该行车记录。 像这样:
def reserve_car(user_id)
car = Car.find_by(status: 'available')
car.with_lock do
car.update_columns(user_id: user_id, status: 'in_use')
end
car
end
您可能需要根据ActiveRecord locking docs
进行调整