选择更新不会阻止 Rails 3.2.11 与 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

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 秒才能完成,因为第一个调用需要 15 秒才能完成,因为我在 mytable.save! 之前在代码中引入了sleep 15命令,第二个调用应该在线路mytable = Mytables.find(:first, :conditions => ['"myID" = ?', myID], :lock => true )处被阻止大约 10 秒,然后它获得锁然后休眠 15 秒。但事实证明,第二次调用仅比第一次调用晚了大约 5 秒。

如果我反转请求顺序,即第一次调用是将属性 1 更新为 3304,而 5 秒延迟的第二次调用是将属性 1 更新为 3303,则最终值为 3303。查看 Heroku 上的登录,第二个调用没有等待时间获取锁,而理论上第一个调用处于睡眠状态,因此仍然持有锁。

设置 2:运行同一应用程序的两个 Thin rails 服务器,一个在端口 3000 上,一个在端口 3001 上。 我的理解是它们连接到同一个数据库,因此如果服务器的一个实例通过SELECT FOR UPDATE获取锁,另一个实例应该无法获取锁并将被阻止。 但是,锁定行为与 Heroku 上的行为相同(没有按我的预期工作)。 而且由于服务器在本地运行,我设法执行了额外的调整测试,以便在第一个调用休眠 15 秒时,我在启动第二个调用之前更改了代码,以便 5 秒后的第二个调用在获得锁后仅休眠 1 秒,并且第二个调用确实比第一个调用早得多完成......

我还尝试使用SELECT FOR UPDATE NOWAIT并在SELECT FOR UPDATE行之后立即引入额外的行mytable.lock!,但结果是相同的。

所以在我看来,虽然 SELECT FOR UPDATE 命令已成功发送到 PostgreSQL 表,但其他线程/进程仍然可以SELECT FOR UPDATE同一行,甚至UPDATE同一行而不会阻塞......

我完全感到困惑,欢迎任何建议。 谢谢!

P.S.1 我在行上使用锁的原因是,我的代码应该能够确保只有将行更新为更高 attribute1 值的调用才会成功。

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"

事实证明,因为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

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

生成的 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

相关内容

  • 没有找到相关文章

最新更新