我正在尝试使用悲观锁来避免竞争条件。我期待在一个线程通过 获取一行之后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 的原始属性 1 值为 3302。我向 Heroku 应用程序发起了一次更新调用(将属性 1 更新为值 3303),然后等待大约 5 秒,然后向 Heroku 应用程序发起另一个更新调用(将属性 1 更新为值 3304)。我预计第二个调用大约需要 25 秒才能完成,因为由于sleep 15
我之前在代码中引入的命令,第一个调用需要 15 秒才能完成mytable.save!
,并且第二个调用应该在该行阻塞mytable = Mytables.find(:first, :conditions => ['"myID" = ?', myID], :lock => true )
大约 10 秒,然后获得锁,然后休眠 15 秒。但事实证明,第二次通话只比第一次通话晚了大约 5 秒。
如果我颠倒请求顺序,即第一次调用是将attribute1更新为3304,延迟5秒第二次调用是将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
同一行没有阻塞...
我完全困惑,任何建议都会受到欢迎。谢谢!
PS1 我在行上使用锁的原因是我的代码应该能够确保只有将行更新为更高的属性值的调用才会成功。
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"