我在 ActiveRecord 和 PostgreSQL 中遇到竞争条件,我正在读取一个值,然后递增它并插入一条新记录:
num = Foo.where(bar_id: 42).maximum(:number)
Foo.create!({
bar_id: 42,
number: num + 1
})
在规模上,多个线程将同时读取然后写入相同的值number
。将其包装在事务中并不能解决竞争条件,因为 SELECT 不会锁定表。我不能使用自动增量,因为number
不是唯一的,只是在特定条件下才是唯一的bar_id
。我看到 3 个可能的修复方法:
所有这些解决方案似乎我都会重新实现大部分ActiveRecord::Base#save!
有更容易的方法吗?
更新:
我以为我找到了答案Foo.lock(true).where(bar_id: 42).maximum(:number)
但这使用SELECT FOR UDPATE
聚合查询不允许这样做
更新2:
我们的 DBA 刚刚告诉我,即使我们可以这样做INSERT INTO foo (bar_id, number) VALUES (42, (SELECT MAX(number) + 1 FROM foo WHERE bar_id = 42));
这并不能解决任何问题,因为 SELECT 与 INSERT 运行在不同的锁中
您的选择是:
Run in SERIALIZABLE
隔离。相互依赖的事务将在提交时因序列化失败而中止。您将收到大量错误日志垃圾邮件,并且您将进行大量重试,但它会可靠地工作。
定义一个UNIQUE
正如您所指出的,约束并在失败时重试。与上面相同的问题。
-
如果有父对象,则可以SELECT ... FOR UPDATE
在执行之前的父对象max
询问。在这种情况下你会SELECT 1 FROM bar WHERE bar_id = $1 FOR UPDATE
。您正在使用bar
作为所有人的锁foo
与此相关bar_id
。然后您就可以知道继续操作是安全的,只要执行计数器增量的每个查询都能可靠地执行此操作。这可以很好地发挥作用。
这仍然对每个调用进行聚合查询,这(每个下一个选项)是不必要的,但至少它不会像上面的选项那样向错误日志发送垃圾邮件。
-
使用柜台。这就是我要做的。要么在bar
,或者像这样的边桌bar_foo_counter
,使用获取行 ID
UPDATE bar_foo_counter SET counter = counter + 1
WHERE bar_id = $1 RETURNING counter
或者如果您的框架无法处理,则使用效率较低的选项RETURNING
:
SELECT counter FROM bar_foo_counter
WHERE bar_id = $1 FOR UPDATE;
UPDATE bar_foo_counter SET counter = $1;
Then, 在同一笔交易中,使用生成的计数器行number
。当您提交时,该计数器表行bar_id
被解锁以供下一个查询使用。如果回滚,更改将被丢弃。
我推荐计数器方法,使用专用的边桌作为计数器,而不是添加一列bar
。这使模型更清晰,并且意味着您可以减少更新膨胀bar
,这会减慢查询速度bar
.
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)