ActiveRecord 和 Postgres 行锁定

ActiveRecord and Postgres row locking

API 繁忙应用程序中的客户端正在争夺现有资源。他们一次请求 1 或 2 个,然后尝试对这些记录执行操作。我正在尝试使用事务来保护状态,但无法清楚地了解行锁,尤其是嵌套事务(我猜是保存点,因为 PG 不会 真正地 在事务中执行事务?)很关心。

流程应该是这样的:

(假设所有示例的路径都正确。请求总是导致产品 returned。)

一个版本可能是这样的:

def self.do_it(request_count)
  Product.transaction do
    locked_products = Product.where(state: 'available').lock('FOR UPDATE').limit(request_count).to_a
    Product.where(id: locked_products.map(&:id)).update_all(state: 'locked')
    do_something(locked_products)
  end
end

在我看来,如果两个用户请求 2 个并且只有 3 个可用,我们可能会在第一行出现死锁。所以,为了解决这个问题,我想做...

def self.do_it(request_count)
  Product.transaction do
    locked_products = []
    request_count.times do
      Product.transaction(requires_new: true) do
        locked_product = Product.where(state: 'available').lock('FOR UPDATE').limit(1).first
        locked_product.update!(state: 'locked')
        locked_products << locked_product
      end
    end
    do_something(locked_products)
  end
end

但是根据我在网上找到的内容,内部事务的 end 不会释放行锁——它们只会在最外层事务结束时释放。

最后,我考虑了这个:

def self.do_it(request_count)
  locked_products = []
  request_count.times do
    Product.transaction do
      locked_product = Product.where(state: 'available').lock('FOR UPDATE').limit(1).first
      locked_product.update!(state: 'locked')
      locked_products << locked_product
    end
  end
  Product.transaction { do_something(locked_products) }
ensure
  evaluate_and_cleanup(locked_products)
end

这为我提供了两个完全独立的事务,然后是执行操作的第三个事务,但如果 do_something 失败,我不得不进行手动检查(或者我可以挽救),这使事情变得更加混乱。如果有人从事务中调用 do_it,这也可能导致死锁,这是很有可能的。

所以我的大问题:

我的小问题:

这里是否有一些既定的或完全明显的模式跳出来让某人更理智地处理这个问题?

事实证明,通过深入 PostgreSQL 控制台并尝试处理事务,可以很容易地回答这些问题。

回答大问题:

是的,我对行锁的理解是正确的。在保存点内获取的独占锁不会在保存点释放时释放,它们会在整个事务提交时释放。

No,没有更改锁类型的命令。那会是怎样的魔法呢?一旦您拥有独占锁,所有将触及该行的查询都必须等待您释放锁才能继续。

除了提交事务外,回滚保存点或事务也会释放排他锁。

就我的应用程序而言,我通过使用多个事务并在应用程序内非常仔细地跟踪状态来解决我的问题。这为重构提供了一个很好的机会,代码的最终版本更简单、更清晰、更易于维护,尽管它的代价是比 "throw-it-all-in-a-PG-transaction" 方法更加分散。