2015-04-07 29 views

回答

2

這個問題一直沒有答案。我認爲這是一個非常好的問題,因爲像這樣的線程安全問題可能會損害應用程序的完整性,並且由於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會[R留在1500。 當您明確嘗試使用3500時,這會導致您擁有total1500

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,直到它結束。