4

競合状態を回避するために悲観的なロックを使用しようとしています。あるスレッドがを介して行を取得した後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 49.1を設定したマシンでローカルに実行し、アプリの2つのシングルスレッドインスタンスを実行しています。何らかの理由で、私が実行するか、単独で実行すると、着信呼び出しのみが順番に処理されます)。rails server -p 3001thin startrails serverthin 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"
4

1 に答える 1

5

PostGreSQLでは自動コミットがデフォルトでオンになっているため、この行は

Mytables Load (4.6ms)  SELECT "mytables".* FROM "mytables" WHERE ("myID" = 1935701094) LIMIT 1 FOR UPDATE

実際には、自動的にコミットが続くため、ロックが解除されます。

このページhttp://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.htmlから読んだときに、私は間違っ ていました。

.find(____, :lock => true)

メソッドは、トランザクションを自動的に開きます。

.with_lock(lock = true) 

同じページの終わりでカバーされています...

したがって、Railsコードを修正するには、次のように追加して、トランザクション内でコードをラップする必要があります。

Mytables.transaction do 

begin

「rescue」行の直前に「end」を追加します。

結果のSQL出力は次のようになります。

(0.3ms)  BEGIN
Mytables Load (4.6ms)  SELECT "mytables".* FROM "mytables" WHERE ("myID" = 1935701094) LIMIT 1 FOR UPDATE
(1.5ms)  UPDATE "mytables" SET "attribute1" = 3304, "updated_at" = '2013-02-02 13:37:04.425577' WHERE "mytables"."id" = 40
(0.4ms)  COMMIT
于 2013-02-04T13:55:31.263 に答える