在Rails 3.2.11与PostgreSQL 9.1中,SELECT FOR UPDATE不会阻塞。

4
我将尝试使用悲观锁来避免竞争条件。我期望当一个线程通过 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 serverthin 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
我原本期望的是,当一个线程通过SELECT FOR UPDATE获取了一行数据后,另一个寻找相同行的线程将被阻塞,直到锁被释放。所以,他们正在访问同一行数据。 然而,问题的根源确实是事务边界,详见我的自问自答帖子。 - S.Y.Chan
1个回答

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

在"rescue"行之前加上额外的一个"end"。

输出的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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接