3

我正试图悲观地锁定表的子集(Postgres)以进行条件插入,并且花了很长时间找到有效的语法。这基本上是我想要做的:

ActiveRecord::Base.transaction do
  if consumer.purchases.lock.sum(&:amount) < some_threshold
    consumer.purchases.create!(amount: amount)
  end
end

不幸的是,上述方法不起作用。但感觉应该。我只需要锁定特定消费者的所有行而不锁定整个表。不幸的是,我处理的是真钱,它是一种分类账,所以它必须是防弹的。

consumer.purchases.lock.to_sql结果SELECT "purchases".* FROM "purchases" WHERE "purchases"."consumer_id" = ? FOR UPDATE如我所料,但由于某种原因,链接.create!导致查询生成器删除FOR UPDATE锁。

好的,所以我把它拆开并尝试了各种我认为应该有效但不要的东西:

# Process 1
ActiveRecord::Base.transaction do
  consumer.purchases.tap{ |p| p.lock! }.create!(amount: amount)
  sleep 20
end

# Process 2
ActiveRecord::Base.transaction do
  consumer.purchases.tap{ |p| p.lock! }.create!(amount: amount)
  # Should wait but doesn't
end
ActiveRecord::Base.transaction do
  purchases = Consumer.find(3).purchases
  purchases.lock
  purchase = purchases.new(amount: amount)
  purchase.save!
  sleep 20
end

... Other process doesn't wait...

我可以让它工作的唯一方法是迭代地锁定行;这确实有效:

# DOES WORK!
# Process 1
ActiveRecord::Base.transaction do
  purchases = Consumer.where(id: 3).first.purchases
  purchases.each(&:lock!)
  purchase = purchases.new(amount: amount)
  purchase.save!
  sleep 20
end

# Process 2
ActiveRecord::Base.transaction do
  purchases = Consumer.where(id: 3).first.purchases
  purchases.each(&:lock!)
  purchase = purchases.new(amount: amount)
  purchase.save!
  # waits as it should
end

但是我不能被要求迭代地锁定它们,这太疯狂了:) 所以我想也许这是一个奇怪的 Postgres 怪癖?(我对 MySQL 更熟悉),所以我在 Postgres 中手工完成,它没有问题:

BEGIN;
SELECT * FROM purchases WHERE consumer_id = 3 FOR UPDATE;
SELECT pg_sleep(30);
INSERT INTO purchases (name, amount) VALUES ('shouldBlock30Seconds', '1000');
END;

BEGIN;
SELECT * FROM purchases WHERE consumer_id = 3 FOR UPDATE;
INSERT INTO purchases (name, amount) VALUES ('shouldWait30Seconds', '1000');
END;

BEGIN;
SELECT * FROM purchases WHERE consumer_id = 24839992 FOR UPDATE;
INSERT INTO purchases (name, amount) VALUES ('shouldInsertImmediately', '1000');
END;

shouldInsertImmediately立即shouldBlock30Seconds插入,30 秒后插入,然后shouldWait30Seconds立即插入。

我正在拔头发:) 以前有没有人遇到过这个问题,或者我只是很累并且遗漏了一些明显的东西?

(Rails 5.1.7、Ruby 2.4.1、Postgres 11.6)

4

1 回答 1

2

purchases.lock不锁定任何东西,它只返回一个新关系,它将在获取时锁定记录。

尝试强制选择:purchases.lock.to_a

于 2020-02-12T17:10:26.323 回答