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 上运行带有独角兽的应用程序,worker_processes 4另一种是在我的机器上本地运行,安装了 PostgreSQL 9.1,运行应用程序的两个单线程实例,一个是rails server -p 3001,另一个是thin start(对于出于某种原因,如果我只是跑步rails serverthin 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"
4

1 回答 1

5

事实证明,因为 PostGreSQL 有 autocommit 默认开启,所以该行

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
于 2013-02-04T13:55:31.263 回答