引用:http://api.rubyonrails.org/classes/ActiveRecord/Batches.html .
是执行find_each
线程安全?换句话说,我可以做类似的事情吗
count = 0
MyModel.find_each do |model|
count += 1 if model.foo?
end
并期望它是线程安全的?
最佳答案
这个问题已经有一段时间没有答案了。我认为这是一个很好的问题,因为像这样的线程安全问题可能对应用程序的完整性有害,而且由于 Rails 感觉如此神奇,所以最好总是深入了解并了解发生了什么。
如果数据的状态可以在代码执行期间更改并影响结果,则此方法 ( find_each
) 在指定情况下将不是线程安全的。 (例如,使用删除的数据调用块,使用相同的数据调用两次块,以及跳过部分数据的块)。
总之,find_each
不是 线程安全。它不做任何锁定,所以它 不确保在调用块时数据已被删除、更新、插入或移动。它唯一确保的是不会为同一个主索引调用该块两次。
这是一个可能会产生奇怪结果的示例(尽管这是愚蠢的情况)。让我们假设以下 Account
table :
|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
,然后最后运行第三个帐户的块,因为余额现在是 0
, total
将保持在 1500
.这将导致您拥有
total
在 1500
当您显然想在 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
运行另一个查询。 , 直到结束。
关于ruby-on-rails - ActiveRecord::Batches::find_each 线程安全吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29501684/