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

时间:2015-04-07 21:31:22

标签: ruby-on-rails multithreading activerecord

参考:http://api.rubyonrails.org/classes/ActiveRecord/Batches.html

find_each的实现是否是线程安全的?换句话说,我可以做一些像

这样的事情
count = 0
MyModel.find_each do |model|
    count += 1 if model.foo?
end

并期望它是线程安全的吗?

1 个答案:

答案 0 :(得分:3)

这个问题暂时没有答案。我认为这是一个非常好的问题,因为像这样的线程安全问题可能会对应用程序的完整性造成损害,而且由于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。 当您明确尝试在total处获得1500时,这会导致3500 each

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 不是完全线程安全的,但它保证了这种情况)

如果您需要确保线程安全,一种简单的方法是锁定表(例如,在postgres中)。请记住,锁定整个桌面会对您的表现造成很大影响。

MyModel.lock.find_each

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

id通过主索引(通常为1000)对所有内容进行排序,并使用批量大小限制结果(默认为SELECT "models".* FROM "models" WHERE "models"."id" > 1000) ORDER BY "models"."id" ASC LIMIT $1 )。

models.id > last_id

它存储批处理中的最后一个id,然后为每一行调用块。一旦为每一行执行了块,它就会使用import bs4 as bs import urllib.request source = urllib.request.urlopen('https://pythonprogramming.net/parsememcparseface/').read() soup = bs.BeautifulSoup(source,'lxml') print(soup.get_text()) 运行另一个查询,直到它到达结尾。