私は次のようなRails ActiveRecordコードを持っています:
new_account_number = Model.maximum(:account_number)
# Some processing that usually involves incrementing
# the new account number by one.
Model.create(foo: 12, bar: 34, account_number: new_account_number)
このコードはそれ自体で正常に機能しますが、DelayedJobワーカーによって処理されるバックグラウンドジョブがいくつかあります。 2人のワーカーがいて、両方がこのコードを処理するジョブのバッチの処理を開始すると、最大値を見つけてから新しいものを作成するまでの遅延のため、同じaccount_numberを持つ新しいModel
レコードを作成することになります。さらに高いアカウント番号で記録します。
今のところ、データベースレベルで一意性制約をモデルテーブルに追加し、この制約が例外をトリガーする場合に備えて最大値を再選択して再試行することで、これを解決しました。
しかし、それはハックのように感じます。
account_number
列にデータベースレベルで自動インクリメントを追加することはオプションではありません。これは、account_numberの割り当てには単なるインクリメント以上のものが伴うためです。
理想的には、読み取りのために問題のテーブルをロックしたいので、完了するまで他の人はテーブルに対して最大選択クエリを実行できません。しかし、どうすればいいのかわかりません。私はPostgresqlを使用しています。
ActiveRecord :: Locking docs に基づくRailsはテーブルレベルのロック用の組み込みAPIを提供していません。
ただし、生のSQLを使用してこれを行うことはできます。 Postgresの場合、これは次のようになります
ActiveRecord::Base.transaction do
ActiveRecord::Base.connection.execute('LOCK table_name IN ACCESS EXCLUSIVE MODE')
...
end
ロックはトランザクション内で取得する必要があり、トランザクションが終了すると自動的に解放されます。
ここで使用するSQLは、データベースによって異なることに注意してください。
テーブル全体をロックする方法についてはすでに回答がありますが、それは避けてください。代わりに、アドバイザリーロックを確認する必要があると思います。他のビジネスのためにテーブルを開いたまま、同じコードブロックが2台のマシンで同時に実行されないようにします。
それはまだデータベースを使用しますが、テーブルをロックしません。
「with_advisory_lock」というgemは次のように使用できます。
Model.with_advisory_lock("ADVISORY_LOCK_NAME") do
# Your code
end
https://github.com/ClosureTree/with_advisory_lock
SQLiteでは動作しません。
一度に1つのプロセスのコードをロックするために、ホールテーブルをロックする必要はありません。テーブル全体をロックすると、パフォーマンスの問題が発生します。「with_lock」メソッドを使用すると、同じ行を常に1つロックできます。これにより、コードが完全に保護されます。余分な宝石は必要ありません。また、トランザクションを作成します。このような: m = Model.order(:id).first m.with_lock do #aquire lock #some code here for a single process at a time
end #release lock
ActiveRecord :: Locking :: Pessimistic からlock
メソッドを使用できます。