如何以原子方式替换表数据的子集

vek*_*tor 5 postgresql concurrency locking update postgresql-9.6

在 PostgreSQL 9.6 我有一个T这样的表

category | id | data
---------+----+------
A        | 1  | foo
A        | 2  | bar
A        | 3  | baz
B        | 4  | eh
B        | 5  | whatcomesafterfoobarbaz
Run Code Online (Sandbox Code Playgroud)

有一个视图V为我提供了数据T,所以它有列category, id, dataT本质上是 的物化视图V,除了我需要以比“刷新所有内容”更多的粒度来刷新它。

所以我会选择V例如

SELECT * FROM V WHERE category = 'A';
Run Code Online (Sandbox Code Playgroud)

或者

SELECT * FROM V WHERE category = 'A' AND id = 2;
Run Code Online (Sandbox Code Playgroud)

T用任何data V给我的东西替换相关的行。不幸的是,我不能做一个简单的UPDATE:问V例如。因为WHERE category = 'A'可能会给我一组与以前完全不同的行。因此我需要做这个序列:

DELETE FROM T WHERE <condition>;
INSERT INTO T (SELECT FROM V WHERE <condition>);
Run Code Online (Sandbox Code Playgroud)

<condition>WHERE category = ?WHERE category = ? AND id = ?

我该怎么做才能满足以下条件?

  • 从不满意的行读取<condition>应该不受影响。
  • 更改应该是原子的,这意味着从满足的行读取<condition>应该看到旧行集或新行集,而不是混合。

注意:与此问题不同,我不想一次替换整个表 - 只有受影响的行。

添加了详细信息

  • 读取次数比写入次数多,大约是写入次数的 10-100 倍。每次写入后都会读取相邻类别。该应用程序是在看一组categoriesidsdata和更新data的一个或多个categories在同一时间。紧接着它会重新获取那些categories并显示它们,它必须看到新鲜的data. 所有的ids 总是用 "their" 获取category

  • 每个category都会有 1-10 ids 之类的东西,会有数以万计的categories.

第一次回答后的更多细节

  • 事务可以并发运行。肯定会出现两个事务以DELETE FROM T WHERE category = 'A';.

  • 有一个表categories可以锁定行FOR UPDATE。还有一个id可以锁定 s的表FOR UPDATE

  • RETURNING在这里没有多大意义,因为我需要获取的不仅仅是更改的行。因此,使用单独的SELECT.

Erw*_*ter 6

并发读取不是问题。在默认READ COMMITTED隔离级别中,写入器不会阻止读取器,反之亦然。将DELETE并包含INSERT在单个事务中以使操作具有原子性(全部应用或不应用)。

如果可以有多个事务尝试同时写入,那就是游戏规则的改变者。单个事务可以保护您免受不一致的更新,但它不能保护您免受并发事务之间的竞争条件:死锁。

假设我们有两个事务T1T2,类别“A”有 10 个 ID:

T1: DELETE FROM T WHERE category = 'A';
-- starts taking row locks in arbitrary order: id 1,2,3,4,5,6,7 ...
                    T2: DELETE FROM T WHERE category = 'A';
                    -- starts taking row locks in arbitrary order: id 10, 9, 8, ...
T1: wait for T2 to release lock on id 8
                    T2: wait for T1 to release lock on id 7

DEADLOCK.
Run Code Online (Sandbox Code Playgroud)

Postgres 会在一段时间后检测到死锁并终止两个事务之一。(报告死锁错误。)

可以切换到SERIALIZABLE事务隔离。但这要昂贵得多,您需要为序列化失败做好准备并在这种情况下重试。

或者您可以通过始终以相同的确定性顺序删除行来避免该问题。喜欢:

WITH del AS (
   SELECT category, id
   FROM   T
   WHERE  category = 'A'
   ORDER  BY category, id  -- enforce this order in *all* writing queries
   FOR    UPDATE
   )
DELETE FROM T 
USING  del
WHERE  T.category = del.category
AND    T.id = del.id;
Run Code Online (Sandbox Code Playgroud)

但通常情况下,有一个更方便的选择。如果您有一个单独的表,其中包含名为的唯一类别,例如cat,您可以使用以下命令锁定单个父行cat

SELECT * FROM cat WHERE category = 'A' FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)

然后(在同一事务中)随意写入类别“A”行T(仍封装在单个事务中以避免中间的、不一致的状态可见)。当然,所有的写查询都必须遵循相同的协议。然后,并发事务将cat在写入之前等待锁定T,一切都很好......

在 Postgres 9.4或更高版本中,请考虑FOR NO KEY UPDATE


关于:

每次写入后都会读取相邻类别。

你知道这个RETURNING条款,对吧?如果您只是插入给定类别的所有行,则无需单独读取。例子: