跳至内容 跳至搜索

悲观锁

Locking::Pessimistic 提供对行级锁的支持,使用 SELECT … FOR UPDATE 和其他锁类型。

链式调用 ActiveRecord::Base#findActiveRecord::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 self.class.current_preventing_writes
          raise ActiveRecord::ReadOnlyError, "Lock query attempted while in readonly mode"
        end

        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 97
def with_lock(*args)
  transaction_opts = args.extract_options!
  lock = args.present? ? args.first : true
  transaction(**transaction_opts) do
    lock!(lock)
    yield
  end
end