锁定引用的表postgres的行

时间:2017-02-08 12:00:45

标签: sql ruby-on-rails database postgresql locking

我有一个表A,它由表B引用,也就是说A的架构如下所示:

Table A
(
  id int,
  name varchar,
)

虽然表B的架构是:

Table B
(
  id int,
  a_id int,
  val int
)

我有一段代码在表B中创建记录。但是,在竞争条件的情况下,如果有两个并行事务,我在该块中有一个条件失败,因此在表B而不是一个中创建了两个记录。 事务块看起来非常类似于(在Rails中):

ActiveRecord::Base.transaction do

  # a here is an ActiveRecord Object of Model A
  b = B.new(a_id: a.id, val: value)    # value is -ve

  raise ActiveRecord::Rollback unless b.save

  # this method calculates the sum of val's of all associated records b of a.
  # i.e. find all records from B where b.a_id = a.id and find the sum of val
  # column
  sum = calculateSum(a)

  # below condition fails in race conditions
  raise ActiveRecord::Rollback if sum <= 0
end

对此的一个解决方案是保持锁的集中散列,其密钥为A的{​​{1}},并且在进入块之前(在我的应用程序中)我一直在等待此锁定被发布。这个解决方案肯定会有效,但我在想Postgres是否已经提供了更好的解决方案。

编辑:没有这样的限制,id应该只有一个A的记录。 A可以有很多B个。只是在我提到的代码块中有一个检查在两个并行事务的情况下失败。

3 个答案:

答案 0 :(得分:1)

这种并发问题的最常见解决方案是将整个块放在SERIALIZABLE事务中。简而言之,这可以保证您的交易行为就像他们拥有对数据库的独占访问权一样。主要的缺点是,您可能会在任何时候触发序列化失败,从简单的SELECT开始,如果发生这种情况,您应该准备重试事务。 an example on the wiki似乎与您的案例非常相似,这可以让您更好地了解这些交易在实践中的表现。

除此之外,我认为你需要明确锁定一些东西。一种可能性是通过A语句将整个记录锁定在SELECT FOR UPDATE,这将阻止应用程序中的竞争进程,以及其他任何试图在B中插入引用行的内容。这里的缺点是您可能阻止(或被阻止)某些不相关的操作,例如在不同的引用表中插入,或者更新A本身。

更好的方法可能是在A.id上取出advisory lock。这基本上等同于您的集中式哈希,但这些锁具有Postgres管理的优势,并且在提交/回滚时自动释放。需要注意的是,因为你要取出任意整数上的锁,你要确保不要碰到其他一些因某些无关原因而锁定同一整数的进程。

您可以使用pg_advisory_xact_lock()的双参数版本处理此问题,并使用其中一个输入来识别锁定类型。我没有在客户端的某个地方维护一堆锁类型常量,而是发现一个有用的策略是将每个锁类型的调用包装在它自己的函数中,并使用该函数的oid作为类型标识符,例如:

CREATE FUNCTION lock_A_for_insert_into_B(a_id int) RETURNS VOID LANGUAGE sql AS $$
  SELECT pg_advisory_xact_lock('lock_A_for_insert_into_B(int)'::regprocedure::int, a_id)
$$

答案 1 :(得分:0)

如果了解您的困境,请尝试在BEGIN ... COMMIT块内执行。对于大多数操作,这取代了锁定。如果指令失败,则db保持不变。它对于多个表同时发生很大变化的操作特别有用。

答案 2 :(得分:-1)

有条件会阻止吗?这不是数据库的工作方式。你什么都不做。他们这样做。为什么你的应用有条件地做任何事情?数据库确保完整性,它会很好。 锁的集中哈希?我不确定你在做什么......但是你在错误的兔子洞里走得太远了,它的枪械是a lot cleverness to get you out

你必须回溯。快。

>

结果,

CREATE TEMP TABLE a ( id_a int PRIMARY KEY, name text );
CREATE TEMP TABLE b ( id_b int PRIMARY KEY, id_a int REFERENCES a, val int );

WITH ti AS (
  INSERT INTO a (id_a, name) VALUES (2,'foo')
  RETURNING id_a
)
INSERT INTO b (id_b,id_a,val)
SELECT 1,ti.id_a,42
FROM ti;