ActiveRecord::Batches::find_each 线程安全吗?

7
1个回答

11

这个问题已经有一段时间没有回答了。我认为这是一个非常好的问题,因为像这样的线程安全问题可能会对应用程序的完整性造成损害,而且由于Rails感觉如此神奇,所以最好总是翻开引擎盖,了解发生了什么。

在指定情况下,如果数据的状态可以在代码执行期间更改并影响结果,则此方法(find_each)将不是线程安全的。(例如,使用已删除的数据调用块,两次使用相同的数据调用块,以及块跳过部分数据)。

总之,find_each 不是线程安全的。它不进行任何锁定,因此它不能确保数据已被删除、更新、插入或移动,当它调用块时。它唯一保证的是不会两次调用同一个主索引的块。

以下是一个示例,说明它可能产生奇怪的结果(尽管是愚蠢的案例)。假设有以下账户(Account)表:

|id|balance|
| 1|   1000|
| 2|    500|
| 3|   2000|

以下是代码(由于表格很小,让我们使用batch_size: 1):

total = 0
Account.find_in_batches(batch_size: 1) |acc|
   total += acc.balance
end

第一次迭代时,它将运行带有Account(id: 1, balance: 1000)的块,因此total将等于1000。 现在,在第二次迭代正在运行时,另一个线程运行以下代码:

Account.transaction do
    acc1 = Account.find(1).lock!
    acc3 = Account.find(3).lock!
    acc1.update(balance: acc1.balance + acc3.balance)
    acc3.update(balance: 0)
end

它基本上是将账户1的所有内容转移到账户3。现在表格看起来像是这样:

|id|balance|
| 1|   3000|
| 2|    500|
| 3|      0|
但要记住,我们已经处理了第一个帐户,它将继续运行第二个帐户的块,所以total会等于1500,最后运行第三个帐户的块,因为余额现在为0total将保持在1500
这将导致您的total1500,而您明显想将其设置为3500
each不是完全线程安全的,但它保证这种情况)
如果需要确保线程安全,一种简单的方法是在表上获取锁定(例如,在postgres中)。请记住,锁定整个表可能会严重影响性能。
count = 0
MyModel.transaction do
   ActiveRecord::Base.connection.execute("LOCK TABLE mymodels SHARE")
   MyModel.find_each do |model|
       count += 1 if model.foo?
   end
end

请注意,MyModel.lock.find_each 也不是线程安全的。


find_each 的工作方式是通过按照主索引(通常是id)对所有内容进行排序,并使用批处理大小(默认为1000)限制结果。

SELECT  "models".* FROM "models" WHERE "models"."id" > 1000) ORDER BY "models"."id" ASC LIMIT $1

它存储了批处理中的最后一个 ID,然后为每一行调用该块。一旦对每一行执行完该块,它会使用 models.id > last_id 运行另一个查询,直到达到结束。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接