ActiveRecord 和 Postgres 行锁定
ActiveRecord and Postgres row locking
API 繁忙应用程序中的客户端正在争夺现有资源。他们一次请求 1 或 2 个,然后尝试对这些记录执行操作。我正在尝试使用事务来保护状态,但无法清楚地了解行锁,尤其是嵌套事务(我猜是保存点,因为 PG 不会 真正地 在事务中执行事务?)很关心。
流程应该是这样的:
- 请求N个资源
- 从资源池中删除这些资源以防止其他用户试图声明它们
- 使用这些资源执行操作
- 如果发生错误,回滚整个事务和 return 资源到池中
(假设所有示例的路径都正确。请求总是导致产品 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" 方法更加分散。
API 繁忙应用程序中的客户端正在争夺现有资源。他们一次请求 1 或 2 个,然后尝试对这些记录执行操作。我正在尝试使用事务来保护状态,但无法清楚地了解行锁,尤其是嵌套事务(我猜是保存点,因为 PG 不会 真正地 在事务中执行事务?)很关心。
流程应该是这样的:
- 请求N个资源
- 从资源池中删除这些资源以防止其他用户试图声明它们
- 使用这些资源执行操作
- 如果发生错误,回滚整个事务和 return 资源到池中
(假设所有示例的路径都正确。请求总是导致产品 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" 方法更加分散。