SELECT FOR UPDATE在PostgreSQL 9.1的Rails 3.2.11上不会阻塞

S.Y*_*han 4 activerecord locking ruby-on-rails-3 postgresql-9.1

我正在尝试使用悲观锁来避免出现竞争状况。我期望一个线程通过获取另一行后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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

我已经在两种设置中对其进行了测试,一种是worker_processes 4在Heroku上运行带有unicorn的应用程序,另一种是在我的计算机上以PostgreSQL 9.1设置在本地运行,运行该应用程序的两个单线程实例,一个是rails server -p 3001,另一个是thin start(对于由于某种原因,如果我只是跑步rails serverthin 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,而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我对行使用锁的原因是,我的代码应能够确保只有将行更新为更高的attribute1值的调用才会成功。

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"
Run Code Online (Sandbox Code Playgroud)

S.Y*_*han 5

事实证明,由于PostGreSQL已将自动提交默认设置为启用,因此该行

Mytables Load (4.6ms)  SELECT "mytables".* FROM "mytables" WHERE ("myID" = 1935701094) LIMIT 1 FOR UPDATE
Run Code Online (Sandbox Code Playgroud)

实际上是紧随其后的是自动提交,因此释放了锁定。

从这个页面读取时我错了 http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html

.find(____, :lock => true)
Run Code Online (Sandbox Code Playgroud)

方法自动打开交易,类似于

.with_lock(lock = true) 
Run Code Online (Sandbox Code Playgroud)

覆盖在同一页的末尾...

所以要修复我的Rails代码,我只需要通过添加以下代码将其包装在一个事务中

Mytables.transaction do 
Run Code Online (Sandbox Code Playgroud)

begin
Run Code Online (Sandbox Code Playgroud)

并在“救援”行之前添加一个额外的“结束”。

产生的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
Run Code Online (Sandbox Code Playgroud)