2013-02-02 86 views
3

我試圖使用悲觀鎖來避免競爭條件。我期待後面的一個線程通過SELECT FOR UPDATE獲得了一行,尋找同一行的另一個線程將被鎖定,直到鎖被釋放。但是,經過測試,看起來鎖並不成立,第二個線程可以獲取並更新該行,即使第一個線程還沒有保存(更新)該行。SELECT FOR UPDATE不會在使用PostgreSQL 9.1的Rails 3.2.11上阻塞

下面是相關代碼:

數據庫架構

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 

我在兩個設置進行了測試,一個是運行應用程序與麒麟與worker_processes 4在Heroku上,另一個在本地運行PostgreSQL 9.1的機器上運行,運行兩個單線程實例的應用程序,一個是rails server -p 3001,另一個是thin start(由於某種原因,如果我只是運行rails serverthin start,它們只會按順序處理來電)。

設置1: 感興趣的myID的數據庫中的原始屬性值爲3302.我向Heroku應用程序發送了一個更新調用(將屬性1更新爲值3303),然後等待約5秒鐘並再次發射一個用於Heroku應用程序(將屬性1更新爲值3304)。我預計第二次通話需要大約25秒鐘完成,因爲第一次通話時間爲mytable.save!之前的代碼中引入的sleep 15命令需要15秒才能完成,第二次通話應該在線路mytable = Mytables.find(:first, :conditions => ['"myID" = ?', myID], :lock => true)處被阻止約10秒,然後獲得鎖定,然後睡眠15秒。 但事實證明,第二次通話只比第一次通話晚約5秒鐘完成。

如果我反轉請求順序,即第一次調用是將屬性1更新爲3304,並且5秒延遲第二次調用是將屬性1更新爲3303,則最終值將是3303.查看Heroku上的日誌,第二次呼叫等待沒有時間獲得鎖,而理論上第一次呼叫正在睡覺,因此仍然保持鎖定。

設置2: 運行同一應用程序的兩個精簡導軌服務器,一個在端口3000上,另一個在端口3001.我的理解是它們連接到同一個數據庫,因此如果服務器的一個實例通過通過SELECT FOR UPDATE,另一個實例不應該能夠獲取鎖並將被阻止。但是,鎖的行爲與Heroku上的行爲相同(不按我的意圖工作)。由於服務器在本地運行,因此我設法執行了額外的調整測試,以便在第一次呼叫睡眠15秒時,我在啓動第二次呼叫之前更改了代碼,以便5秒鐘後的第二次呼叫僅休眠1第二獲取鎖,第二個電話後,去把前面要比第一個電話......

我還試圖用SELECT FOR UPDATE NOWAITSELECT FOR UPDATE行後引入一個額外的行mytable.lock!立即,但結果是一樣的。

所以,在我看來,雖然SELECT FOR UPDATE命令已經發出到PostgreSQL表成功後,其他線程/進程仍然可以SELECT FOR UPDATE在同一行,甚至UPDATE同一行,而完全不攔截...

我完全感到困惑,任何建議都會受到歡迎。謝謝!

P.S.1我在行上使用鎖的原因是,我的代碼應該能夠確保只有調用將行更新爲更高的attribute1值才能成功。從本地日誌

SQL輸出PS2樣品

"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" 
+0

的問題是不是哪裏事務邊界是運行'SELECT FOR UPDATE'交易很清楚,也不是從問題明確他們正在訪問同一行。如果你能澄清這些問題,這將有助於他人理解你的問題。 – kgrittn

+0

「我期待後一個線程通過SELECT FOR UPDATE獲得一行,另一個線程尋找相同的行將被阻止,直到鎖被釋放。」所以是的,他們正在訪問同一行。 然而,這確實是交易界限造成問題的原因,請參閱我的自我回復帖子 –

回答

4

事實證明,由於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 

,只是在 「救市」 行前添加一個額外的 「結束」。

產生的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