我試圖使用悲觀鎖來避免競爭條件。我期待後面的一個線程通過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 server
或thin 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 NOWAIT
和SELECT 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"
的問題是不是哪裏事務邊界是運行'SELECT FOR UPDATE'交易很清楚,也不是從問題明確他們正在訪問同一行。如果你能澄清這些問題,這將有助於他人理解你的問題。 – kgrittn
「我期待後一個線程通過SELECT FOR UPDATE獲得一行,另一個線程尋找相同的行將被阻止,直到鎖被釋放。」所以是的,他們正在訪問同一行。 然而,這確實是交易界限造成問題的原因,請參閱我的自我回復帖子 –