競合状態を回避するために悲観的なロックを使用しようとしています。あるスレッドがを介して行を取得した後SELECT FOR UPDATE
、同じ行を探している別のスレッドがロックが解除されるまでブロックされることを期待していました。ただし、テストの結果、最初のスレッドがまだその行を保存(更新)していない場合でも、ロックは保持されず、2番目のスレッドは行を取得して更新できるようです。
関連するコードは次のとおりです。
データベーススキーマ
class CreateMytables < ActiveRecord::Migration
def change
create_table :mytables do |t|
t.integer :myID
t.integer :attribute1
t.timestamps
end
add_index :mytables, :myID, :unique => true
end
end
mytables_controller.rb
class MytablessController < ApplicationController
require 'timeout'
def create
myID = Integer(params[:myID])
begin
mytable = nil
Timeout.timeout(25) do
p "waiting for lock"
mytable = Mytables.find(:first, :conditions => ['"myID" = ?', myID], :lock => true ) #'FOR UPDATE NOWAIT') #true)
#mytable.lock!
p "acquired lock"
end
if mytable.nil?
mytable = Mytables.new
mytable.myID = myID
else
if mytable.attribute1 > Integer(params[:attribute1])
respond_to do |format|
format.json{
render :json => "{\"Error\": \"Update failed, a higher attribute1 value already exist!\",
\"Error Code\": \"C\"
}"
}
end
return
end
end
mytable.attribute1 = Integer(params[:attribute1])
sleep 15 #1
p "woke up from sleep"
mytable.save!
p "done saving"
respond_to do |format|
format.json{
render :json => "{\"Success\": \"Update successful!\",
\"Error Code\": \"A\"
}"
}
end
rescue ActiveRecord::RecordNotUnique #=> e
respond_to do |format|
format.json{
render :json => "{\"Error\": \"Update Contention, please retry in a moment!\",
\"Error Code\": \"B\"
}"
}
end
rescue Timeout::Error
p "Time out error!!!"
respond_to do |format|
format.json{
render :json => "{\"Error\": \"Update Contention, please retry in a moment!\",
\"Error Code\": \"B\"
}"
}
end
end
end
end
2つの設定でテストしました。1つはHerokuでユニコーンを使用してアプリを実行し、もう1つはPostgreSQL worker_processes 4
9.1を設定したマシンでローカルに実行し、アプリの2つのシングルスレッドインスタンスを実行しています。何らかの理由で、私が実行するか、単独で実行すると、着信呼び出しのみが順番に処理されます)。rails server -p 3001
thin start
rails server
thin start
設定1:対象のmyIDのデータベース内の元のattribute1値は3302です。Herokuアプリに対して1回の更新呼び出しを実行し(attribute1を値3303に更新するため)、約5秒間待ってから、別の1回をHerokuアプリに対して実行しました。 (attribute1を値3304に更新します)。sleep 15
前にコードで導入したコマンドのために最初の呼び出しが終了するのに15秒かかり、2番目の呼び出しがその前に約10秒間mytable.save!
回線でブロックされるため、2番目の呼び出しが終了するのに約25秒かかると予想していました。mytable = Mytables.find(:first, :conditions => ['"myID" = ?', myID], :lock => true )
ロックを取得してから15秒間スリープします。しかし、2番目の呼び出しは最初の呼び出しよりも約5秒遅れて終了したことがわかりました。
そして、リクエストの順序を逆にすると、つまり、最初の呼び出しがattribute1を3304に更新し、5秒遅れた2番目の呼び出しがattribute1を3303に更新する場合、最終的な値は3303になります。Herokuのログを見ると、2番目の呼び出しは待機していません。理論的には最初の呼び出しがスリープしているため、ロックを保持している間にロックを取得する時間。
設定2:同じアプリの2つのThin Railsサーバーを実行します。1つはポート3000で、もう1つはポート3001で実行します。私の理解では、これらは同じデータベースに接続しているため、サーバーの一方のインスタンスがを介してロックを取得したSELECT FOR UPDATE
場合、もう一方のインスタンスはロックを取得できず、ブロックされます。ただし、ロックの動作はHerokuの場合と同じです(意図したとおりに機能しません)。また、サーバーがローカルで実行されているため、最初の呼び出しが15秒間スリープしている間に、2番目の呼び出しを開始する前にコードを変更して、5秒後の2番目の呼び出しが1秒間だけスリープするように、追加の微調整テストを実行できました。ロックを取得してから2番目で、2番目の呼び出しは最初の呼び出しよりもはるかに早く終了しました...
また、行の直後に追加の行を使用SELECT FOR UPDATE NOWAIT
して導入しようとしましたが、結果は同じです。mytable.lock!
SELECT FOR UPDATE
したがって、SELECT FOR UPDATE
コマンドがPostgreSQLテーブルに正常に発行されている間、他のスレッド/プロセスSELECT FOR UPDATE
は同じ行、さらにUPDATE
はまったくブロックせずに同じ行を実行できるように思われます...
私は完全に困惑しており、どんな提案も歓迎します。ありがとう!
PS1行にロックを使用する理由は、行をより高いattribute1値に更新するための呼び出しのみが成功することをコードが保証できるようにするためです。
PS2ローカルログからのサンプルSQL出力
"waiting for lock"
Mytables Load (4.6ms) SELECT "mytables".* FROM "mytables" WHERE ("myID" = 1935701094) LIMIT 1 FOR UPDATE
"acquired lock"
"woke up from sleep"
(0.3ms) BEGIN
(1.5ms) UPDATE "mytables" SET "attribute1" = 3304, "updated_at" = '2013-02-02 13:37:04.425577' WHERE "mytables"."id" = 40
(0.4ms) COMMIT
"done saving"