跳至内容 跳至搜索

悲观锁

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)

将传递的块包装在事务中,在 yield 之前使用锁重新加载对象。您可以将 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