跳到內容 跳到搜尋

悲觀鎖定

Locking::Pessimistic 提供使用 SELECT … FOR UPDATE 和其他鎖定類型進行列級鎖定的支援。

ActiveRecord::Base#find 串聯到 ActiveRecord::QueryMethods#lock 來取得所選列的獨佔鎖定

# select * from accounts where id=1 for update
Account.lock.find(1)

呼叫 lock('some locking clause') 來使用你自己的資料庫特定鎖定子句,例如『LOCK IN SHARE MODE』或『FOR UPDATE NOWAIT』。範例

Account.transaction do
  # select * from accounts where name = 'shugo' limit 1 for update nowait
  shugo = Account.lock("FOR UPDATE NOWAIT").find_by(name: "shugo")
  yuko = Account.lock("FOR UPDATE NOWAIT").find_by(name: "yuko")
  shugo.balance -= 100
  shugo.save!
  yuko.balance += 100
  yuko.save!
end

你也可以使用 ActiveRecord::Base#lock! 方法透過 id 鎖定單一記錄。如果你不需要鎖定每一列,這可能會比較好。範例

Account.transaction do
  # select * from accounts where ...
  accounts = Account.where(...)
  account1 = accounts.detect { |account| ... }
  account2 = accounts.detect { |account| ... }
  # select * from accounts where id=? for update
  account1.lock!
  account2.lock!
  account1.balance -= 100
  account1.save!
  account2.balance += 100
  account2.save!
end

你可以透過呼叫含有區塊的 with_lock 來啟動交易並取得鎖定。區塊在交易內部被呼叫,物件已經被鎖定。範例

account = Account.first
account.with_lock do
  # This block is called within a transaction,
  # account is already locked.
  account.balance -= 100
  account.save!
end

關於列鎖定的資料庫特定資訊

MySQL

dev.mysql.com/doc/refman/en/innodb-locking-reads.html

PostgreSQL

www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE

方法
L
W

執行個體公開方法

lock!(lock = true)

取得此記錄的列鎖定。重新載入記錄來取得請求的鎖定。傳遞 SQL 鎖定子句來附加在 SELECT 語句的結尾,或傳遞 true 代表「FOR UPDATE」(預設值,獨佔列鎖定)。傳回已鎖定的記錄。

# File activerecord/lib/active_record/locking/pessimistic.rb, line 69
      def lock!(lock = true)
        if persisted?
          if has_changes_to_save?
            raise(<<-MSG.squish)
              Locking a record with unpersisted changes is not supported. Use
              `save` to persist the changes, or `reload` to discard them
              explicitly.
              Changed attributes: #{changed.map(&:inspect).join(', ')}.
            MSG
          end

          reload(lock: lock)
        end
        self
      end

with_lock(*args)

在交易中包覆已傳遞的區塊,在回傳之前重新載入含有鎖定的物件。你可以傳遞 SQL 鎖定子句作為可選參數(請參閱 lock!)。

你也可以傳遞像是 requires_new:isolation:joinable: 等選項到包覆的交易(請參閱 ActiveRecord::ConnectionAdapters::DatabaseStatements#transaction)。

# File activerecord/lib/active_record/locking/pessimistic.rb, line 92
def with_lock(*args)
  transaction_opts = args.extract_options!
  lock = args.present? ? args.first : true
  transaction(**transaction_opts) do
    lock!(lock)
    yield
  end
end