2015-09-03 41 views
2

我經歷的ActiveRecord的競爭條件與PostgreSQL的,我正在讀的值,然後遞增,並插入一個新的記錄:PostgreSQL和ActiveRecord的子查詢的競爭條件

num = Foo.where(bar_id: 42).maximum(:number) 
Foo.create!({ 
    bar_id: 42, 
    number: num + 1 
}) 

大規模,多線程會同時讀取然後寫入相同的值number。將它包裝在事務中並不能解決競爭條件,因爲SELECT不鎖定表。我不能使用自動增量,因爲number不是唯一的,只有在給定bar_id時纔是唯一的。我看到3級可能的解決方法:(?行級鎖)

  • 明確地使用一個Postgres鎖定
  • 採用獨特的約束和重試失敗
  • 覆蓋保存使用子查詢(呸!) ,IE

    INSERT INTO foo (bar_id, number) VALUES (42, (SELECT MAX(number) + 1 FROM foo WHERE bar_id = 42));

所有這些解決方案看起來像我會重新實現的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));不解決任何事情,因爲在選擇不同的鎖比INSERT運行

+0

如果它在飛行中計算,它會隨時間而改變,並且會受到相同的競爭條件 –

回答

2

的選項有:

  • 運行在SERIALIZABLE隔離。由於序列化失敗,相互依賴的事務將在提交時中止。你會得到很多錯誤日誌垃圾郵件,你會做很多重試,但它會可靠地工作。

  • 定義UNIQUE約束並在失敗時重試,如您所述。與上面相同的問題。

  • 如果存在父對象,則可以在執行max查詢之前使用SELECT ... FOR UPDATE父對象。在這種情況下,你會SELECT 1 FROM bar WHERE bar_id = $1 FOR UPDATE。您正在使用bar作爲所有foo的鎖與bar_id。然後您就可以知道繼續進行是安全的,只要每個正在執行計數器遞增的查詢都能可靠地執行此操作。這可以工作得很好。

    這仍然會爲每個調用執行一個聚合查詢,該查詢(每個下一個選項)是不必要的,但至少它不會像上述選項一樣垃圾郵件錯誤日誌。

  • 使用計數器表。這就是我要做的。無論是在bar,或像bar_foo_counter邊桌,獲得使用

    UPDATE bar_foo_counter SET counter = counter + 1 
    WHERE bar_id = $1 RETURNING counter 
    

    或低效率的選項行ID,如果你的框架不能處理RETURNING

    SELECT counter FROM bar_foo_counter 
    WHERE bar_id = $1 FOR UPDATE; 
    
    UPDATE bar_foo_counter SET counter = $1; 
    

    然後,在同一交易,使用生成的計數器行爲number。當您提交時,該bar_id的計數器錶行會被解鎖以供下一個查詢使用。如果您回滾,則更改將被丟棄。

我推薦櫃檯的方式,使用專用的側表計,而不是增加一列bar。這對於模型更清晰,並且意味着您在bar中創建的更新膨脹更少,這會減慢查詢的速度至bar