我试图使用悲观锁来避免竞争条件。我期待后一个线程通过SELECT FOR UPDATE
获取一行,另一个寻找同一行的线程将被阻塞,直到锁被释放。但是,在测试时,似乎锁定不成立,第二个线程只能获取行并更新它,即使第一个线程尚未保存(更新)该行。
以下是相关代码:
数据库架构
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
我已经在两个设置中测试了它,一个是在Heroku上运行带有worker_processes 4
的独角兽的应用程序,另一个是在设置了PostgreSQL 9.1的机器上本地运行,运行应用程序的两个单线程实例,一个是rails server -p 3001
,另一个是thin start
(出于某种原因,如果我只是单独运行rails server
或thin start
,它们将只按顺序处理来电。)
设置1:
数据库中感兴趣的myID的原始attribute1值是3302.我向Heroku应用程序发出了一个更新调用(将attribute1更新为值3303),然后等待大约5秒并向Heroku应用程序发出另一个更新(更新) attribute1到值3304)。我期待第二个呼叫大约需要25秒才能完成,因为第一个呼叫需要15秒才能完成,因为我在sleep 15
之前的代码中引入了mytable.save!
命令,第二个呼叫应该被阻止在mytable = Mytables.find(:first, :conditions => ['"myID" = ?', myID], :lock => true )
行约10秒钟,然后获得锁定然后再睡眠15秒。
但事实证明,第二次通话仅比第一次通话晚了约5秒。
如果我反转请求顺序,即第一次调用是将attribute1更新为3304,而第二次延迟第二次调用是将attribute1更新为3303,则最终值为3303。 看着Heroku上的日志,第二个电话等待没有时间获得锁定,理论上第一个电话正在休眠,因此仍然保持锁定。
设置2:
运行同一个应用程序的两个Thin rails服务器,一个在端口3000上,另一个在端口3001上。我的理解是它们连接到同一个数据库,因此如果服务器的一个实例通过SELECT FOR UPDATE
获得锁定,则另一个实例应该无法获取锁定并将被阻止。但是,锁的行为与Heroku相同(不按我的意图工作)。并且由于服务器在本地运行,我设法执行额外的调整测试,以便在第一个呼叫休眠15秒时,我在启动第二个呼叫之前更改了代码,以便5秒后的第二个呼叫仅睡眠1获得锁定后第二次,第二次通话比第一次通话更早完成......
我还尝试使用SELECT FOR UPDATE NOWAIT
并在mytable.lock!
行之后立即引入额外的行SELECT FOR UPDATE
,但结果是相同的。
所以在我看来,虽然SELECT FOR UPDATE
命令已成功发布到PostgreSQL表,但其他线程/进程仍可SELECT FOR UPDATE
同一行,甚至UPDATE
相同行没有阻塞......
我完全感到困惑,任何建议都会受到欢迎。谢谢!
P.S.1我在行上使用锁的原因是我的代码应该能够确保只有将行更新为更高的attribute1值的调用才能成功。
P.S.2本地日志的示例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"
答案 0 :(得分: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
并在“救援”线之前添加额外的“结束”。
生成的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