我将尝试使用悲观锁来避免竞争条件。我期望当一个线程通过
以下是相关代码:
数据库架构
我已在两个环境中进行了测试,一个是在Heroku上使用unicorn和
第一组环境: 感兴趣的myID在数据库中的原始attribute1值为3302。我向Heroku应用程序发出了一个更新调用(将attribute1更新为值3303),然后等待约5秒钟,并向Heroku应用程序发出了另一个更新调用(将attribute1更新为值3304)。我预计第二个调用需要大约25秒才能完成,因为第一个调用花费了15秒才完成,这是由于我在
如果我颠倒请求顺序,即首先更新attribute1为3304,然后延迟5秒的第二个调用是更新attribute1为3303,则最终值将为3303。 查看Heroku上的日志,第二个调用在理论上第一个调用正在休眠并且仍然持有锁时没有等待时间即可获取锁。
第二组环境: 运行相同应用程序的两个Thin rails服务器,一个在端口3000上,另一个在端口3001上。我理解它们连接到同一个数据库,因此如果服务器的一个实例通过
我还尝试使用
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上使用unicorn和
worker_processes 4
运行应用程序,另一个是在我本地机器上设置了PostgreSQL 9.1,在其中运行了两个单线程实例的应用程序,一个是rails server -p 3001
,另一个是thin start
(由于某种原因,如果我只运行rails server
或thin start
,它们只会按顺序处理传入的调用)。第一组环境: 感兴趣的myID在数据库中的原始attribute1值为3302。我向Heroku应用程序发出了一个更新调用(将attribute1更新为值3303),然后等待约5秒钟,并向Heroku应用程序发出了另一个更新调用(将attribute1更新为值3304)。我预计第二个调用需要大约25秒才能完成,因为第一个调用花费了15秒才完成,这是由于我在
mytable.save!
之前引入的sleep 15
命令造成的,而第二个调用应该在mytable = Mytables.find(:first, :conditions => ['"myID" = ?', myID], :lock => true )
处阻塞约10秒钟,然后获取锁并休眠15秒钟。
但事实证明,第二个调用完成的时间只比第一个调用晚了约5秒钟。如果我颠倒请求顺序,即首先更新attribute1为3304,然后延迟5秒的第二个调用是更新attribute1为3303,则最终值将为3303。 查看Heroku上的日志,第二个调用在理论上第一个调用正在休眠并且仍然持有锁时没有等待时间即可获取锁。
第二组环境: 运行相同应用程序的两个Thin rails服务器,一个在端口3000上,另一个在端口3001上。我理解它们连接到同一个数据库,因此如果服务器的一个实例通过
SELECT FOR UPDATE
获得了锁,则另一个实例不应该能够获得锁并将被阻止。但是,锁的行为与在Heroku上的行为相同(未按照我的意图工作)。由于服务器正在本地运行,因此我设法进行额外的调整测试,以便在第一个调用睡眠15秒钟时,我更改了启动第二个调用之前的代码,以便5秒后的第二个调用在获取锁后只睡眠1秒钟,第二个调用确实比第一个调用提前完成......我还尝试使用
SELECT FOR UPDATE NOWAIT
并在SELECT FOR UPDATE
行后立即引入一行mytable.lock!
,但结果相同。
对我来说,似乎虽然已经成功向PostgreSQL表发出了SELECT FOR UPDATE
命令,但其他的线程/进程仍然可以在不阻塞的情况下选择相同的行,甚至更新相同的行...
我完全感到困惑,欢迎任何建议。谢谢!
P.S.1 我使用行锁的原因是为了确保我的代码能够确保只有调用将行更新为更高属性1值的请求才会成功。
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"
SELECT FOR UPDATE
事务边界在哪里,也没有明确表明他们正在访问同一行。如果您能澄清这些问题,将有助于其他人理解您的问题。 - kgrittn