Oracle 和 PostgreSQL 中的 Write Skew 异常不回滚事务

Posted

技术标签:

【中文标题】Oracle 和 PostgreSQL 中的 Write Skew 异常不回滚事务【英文标题】:Write Skew anomaly in Oracle and PostgreSQL does not rollback transaction 【发布时间】:2017-01-26 19:04:24 【问题描述】:

我注意到 Oracle 和 PostgreSQL 中都出现了以下情况。

考虑到我们有以下数据库架构:

create table post (
    id int8 not null, 
    title varchar(255), 
    version int4 not null, 
    primary key (id));    

create table post_comment (
    id int8 not null, 
    review varchar(255), 
    version int4 not null, 
    post_id int8, 
    primary key (id));

alter table post_comment 
    add constraint FKna4y825fdc5hw8aow65ijexm0 
    foreign key (post_id) references post;  

有以下数据:

insert into post (title, version, id) values ('Transactions', 0, 1);
insert into post_comment (post_id, review, version, id) 
    values (1, 'Post comment 1', 459, 0);
insert into post_comment (post_id, review, version, id) 
    values (1, 'Post comment 2', 537, 1);
insert into post_comment (post_id, review, version, id) 
    values (1, 'Post comment 3', 689, 2); 

如果我打开两个单独的 SQL 控制台并执行以下语句:

TX1: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

TX2: BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

TX1: SELECT COUNT(*) FROM post_comment where post_id = 1;

TX1: > 3

TX1: UPDATE post_comment SET version = 100 WHERE post_id = 1;

TX2: INSERT INTO post_comment (post_id, review, version, id) VALUES (1, 'Phantom', 0, 1000);

TX2: COMMIT;

TX1: SELECT COUNT(*) FROM post_comment where post_id = 1;

TX1: > 3

TX1: COMMIT;

TX3: SELECT * from post_comment;

     > 0;"Post comment 0";100;1
       1;"Post comment 1";100;1
       2;"Post comment 2";100;1
       1000;"Phantom";0;1

正如预期的那样,SERIALIZABLE 隔离级别保留了 TX1 事务开始时的快照数据,而 TX1 仅看到 3 条 post_comment 记录。

由于Oracle和PostgreSQL中的MVCC模型,允许TX2插入新记录并提交。

为什么允许 TX1 提交?因为这是一个写入偏斜异常,所以我希望看到 TX1 会因“序列化失败异常”或类似情况而回滚。

PostgreSQL 和 Oracle 中的 MVCC Serializable 模型是否只提供快照隔离保证而没有 Write Skew 异常检测?

更新

我什至更改了 Tx1 以发出一条 UPDATE 语句,该语句更改了属于同一 post 的所有 post_comment 记录的 version 列。

这样,Tx2 会创建一条新记录,而 Tx1 将在不知道已添加满足 UPDATE 过滤条件的新记录的情况下提交。

实际上,在 PostgreSQL 上使其失败的唯一方法是,在插入幻像记录之前,我们在 Tx2 中执行以下 COUNT 查询:

Tx2: SELECT COUNT(*) FROM post_comment where post_id = 1 and version = 0

TX2: INSERT INTO post_comment (post_id, review, version, id) VALUES (1, 'Phantom', 0, 1000);

TX2: COMMIT;

然后 Tx1 将被回滚:

org.postgresql.util.PSQLException: ERROR: could not serialize access due to read/write dependencies among transactions
  Detail: Reason code: Canceled on identification as a pivot, during conflict out checking.
  Hint: The transaction might succeed if retried.

写偏斜异常预防机制很可能检测到此更改并回滚事务。

有趣的是,Oracle 似乎并没有受到这种异常的困扰,因此 Tx1 只是成功提交。由于 Oracle 不会阻止写入倾斜的发生,因此 Tx1 提交很好。

顺便说一句,您可以自己运行所有这些示例,因为它们位于 GitHub。

【问题讨论】:

TX2 提交,因为它是第一个更改数据并提交的。 TX1 不会更改相同的数据,因此不需要抛出任何异常,因为可以构建包括 TX1 在内的交易的序列化时间线。如果 TX1 会更改相同的数据或依赖于您的数据的数据,则会引发错误。 那我试试读写事务。 我在 TX1 中添加了一条 UPDATE 语句,但 Tx1 仍然能够提交。 @Vlad:这种行为与您运行 TX1 然后运行 ​​TX2 所看到的完全一致。换句话说,交易已经成功序列化;没有序列化失败。 the Postgres wiki 上有很多很好的示例,它们可能会让您了解应该在何处/何时/为什么会发生序列化错误。 这是有道理的。如果 Tx1 和 Tx2 完全一个接一个地运行,我们将得到相同的结果。 【参考方案1】:

在 1995 年的论文中,A Critique of ANSI SQL Isolation Levels,Jim Gray 及其同事将 Phantom Read 描述为:

P3: r1[P]...w2[y in P]...(c1 or a1) (幻影)

一个重要的注意事项是 ANSI SQL P3 只禁止插入(和 更新,根据一些解释)到谓词,而 上面 P3 的定义禁止任何满足谓词的写入 一旦谓词被读取 - 写入可能是插入, 更新或删除。

因此,幻读并不意味着您可以简单地返回当前运行事务开始时的快照,并假装为查询提供相同的结果将保护您免受实际幻读异常的影响。

在最初的 SQL Server 2PL(两阶段锁定)实现中,为查询返回相同的结果意味着谓词锁定。

MVCC(多版本并发控制)快照隔离(在 Oracle 中错误地命名为 Serializable)实际上不会阻止其他事务插入/删除与已执行并返回结果集的查询匹配相同过滤条件的行我们当前正在运行的事务。

因此,我们可以想象以下场景,我们希望对所有员工进行加薪:

    Tx1:SELECT SUM(salary) FROM employee where company_id = 1; Tx2:INSERT INTO employee (id, name, company_id, salary) VALUES (100, 'John Doe', 1, 100000); Tx1:UPDATE employee SET salary = salary * 1.1; Tx2:COMMIT; Tx1:COMMIT:

在这种情况下,CEO 运行第一笔交易(Tx1),因此:

    她首先检查了她公司所有工资的总和。 与此同时,人力资源部门运行第二笔交易 (Tx2),因​​为他们刚刚设法雇用 John Doe 并给了他 10 万美元的薪水。 CEO 考虑到工资总额,认为加薪 10% 是可行的,但他不知道工资总额已经提高了 10 万。 同时,HR 事务 Tx2 已提交。 Tx1 已提交。

轰隆隆! CEO 已对旧快照做出决定,加薪可能无法由当前更新的工资预算维持。

您可以在the following post查看此用例的详细说明(带有大量图表)。

这是幻读还是Write Skew?

根据Jim Gray and co,这是一个幻读,因为写入偏斜定义为:

A5B Write Skew 假设 T1 读取 x 和 y,与 C(),然后 T2 读取 x 和 y,写入 x,然后提交。然后T1 写 y。如果 x 和 y 之间存在约束,则可能是 违反。在历史方面:

A5B: r1[x]...r2[y]...w1[y]...w2[x]...(c1 和 c2 出现)

在 Oracle 中,事务管理器可能会也可能不会检测到上述异常,因为它不像 mysql 那样使用谓词锁或index range locks (next-key locks)。

只有当 Bob 发出对雇员表的读取时,PostgreSQL 才设法捕捉到这种异常,否则,这种现象不会被阻止。

更新

最初,我假设 Serializability 也意味着时间排序。然而,正如very well explained by Peter Bailis,挂钟排序或线性化仅被假定为严格的可串行化。

因此,我的假设是针对 Strict Serializable 系统。但这不是 Serializable 应该提供的。 Serializable 隔离模型不保证时间,并且操作可以重新排序,只要它们等效于 some 串行执行。

因此,根据 Serializable 定义,如果第二个事务没有发出任何读取,则可能发生这种幻读。但是,在 2PL 提供的 Strict Serializable 模型中,即使第二个事务没有针对我们试图防止幻读的相同条目发出读取,幻读也会被阻止。

【讨论】:

嘿@Vlad,在您的书第 97 页中,您在 REPEATABLE_READS 的摘要表中提到 Postgres 允许 WRITE SKEW,但是当您在第 91 页解释 WRITE SKEW 时,两个 txs 都向两者发出读取表,Postgres 将能够捕获并禁止它。 实际上有两种写入倾斜:G1: Anti-dependency Cycles (write skew on disjoint read)G2: Anti-Dependency Cycles (write skew on predicate read)。我书里的那个是G1,这里这个是G2。 PostgreSQL 不会在 RR 中阻止它们中的任何一个。 是的,确实知道了,刚刚复习了一遍,还看了一些关于该主题的博客文章:)。 很高兴能帮上忙?【参考方案2】:

你观察到的不是幻读。那就是当第二次发出查询时会出现一个新行(幻象意外出现)。

SERIALIZABLE 隔离可以保护您免受 Oracle 和 PostgreSQL 中的幻读。

Oracle 和 PostgreSQL 的区别在于 Oracle 中的SERIALIZABLE 隔离级别仅提供快照隔离(这足以防止幻象出现),而在 PostgreSQL 中它将保证真正的可序列化(即始终存在序列化导致相同结果的 SQL 语句)。如果您想在 Oracle 和 PostgreSQL 中获得相同的效果,请在 PostgreSQL 中使用 REPEATABLE READ 隔离。

【讨论】:

不,count 的结果没有修改。两次 TX1 发出查询,它都返回 3,即使 TX2 在此期间插入了一个新值。现在可以查看 TX1 中的新行。 我不认为它这么简单,或者像 SQL 标准试图描绘幻像行应该如何发生那样简单。这仅取决于交易执行者是否认为异常。查看更新的部分。如果我在 Tx2 中添加选择 COUNT,PostgreSQL 将检测到 Tx1 不再成立,因为 Tx2 已经根据 Tx1 即将更改的快照值做出决定。 Oracle 没有检测到任何异常,所以它只是成功地提交了两个事务。 我看不出你的问题出在哪里。数据库为每个事务提供一致的世界观,在这种情况下,TX1 甚至在 TX2 开始之前就完成了。没有人看到不一致。为什么要抛出错误? 假设您选择了公司中的所有员工并希望给他们加薪。运行查询后,人力资源部门决定添加新员工。您的线程被恢复,您更新了您之前选择的员工的薪水。在这种情况下,人力资源部门添加的条目不会得到任何加薪。有两种方法可以解决这个问题:要么阻止 Tx2 直到 Tx1 运行,要么回滚 Tx1,因为自 Tx1 启动以来世界观发生了变化。 不,因为新员工是在(逻辑上)所有员工都得到加薪之后添加的。没有人向您保证一定的交易顺序。【参考方案3】:

我只是想指出 Vlad Mihalcea 的回答是完全错误的。

这是幻读还是写偏斜?

两者都不是——这里没有异常,交易可以序列化为 Tx1 -> Tx2。

SQL 标准规定: “可序列化的执行被定义为同时执行 SQL 事务的操作的执行,它产生与那些相同 SQL 事务的一些串行执行相同的效果。”

只有当 Bob 发出对雇员表的读取时,PostgreSQL 才设法捕获此异常,否则不会阻止该现象。

这里 PostgreSQL 的行为是 100% 正确的,它只是“翻转”了明显的事务顺序。

【讨论】:

那么,您如何解释如果您在基于 MVCC 的数据库或基于 2PL 的数据库中运行相同的示例会得到不同的结果?尝试在 MySQL 或 SQL Server 上以 Serializable 隔离级别运行,您将看到不同的结果。因此,这意味着并发控制的风格允许不同的排序和不同的结果,同时仍然考虑到两种结果都是正确的,对吗? 你在这里的确切结果是什么意思?例如。在 MS SQL 中,Tx2 刚刚被阻止,所以我不能先提交 Tx2。 没错。因此,与任何并发控制机制一样,冲突要么被阻止(2PL),要么被检测到(MVCC)。在这里,2PL 阻止它,但 MVCC 允许它。 不,SSI 不允许。同样,您没有表现出任何异常。看,根据定义,任何产生与 tx1->tx2 或 tx2->tx1 相同结果的执行(当然只考虑已提交的事务)都是可序列化的。这里的区别在于 2PL 阻止了可序列化的调度之一,而 SSI(正确地)没有。 “但是,Serializable 隔离级别不需要任何挂钟排序语义。”对,就是这样。感谢您修复答案(以及链接,顺便说一句)!【参考方案4】:

Postgres 文档defines a phantom read 为:

事务重新执行查询,返回一组满足 一个搜索条件,并发现满足 由于最近提交的另一笔交易,条件发生了变化。

因为您的选择在另一个事务提交之前和之后返回相同的值,所以它不符合幻读的条件。

【讨论】:

它返回相同的值,因为 SERIALIZABLE 指示查询执行器采用 TX1 事务开始时的行版本。但是有一个异常,因为实际上,在 Tx1 事务结束时,我们有 4 条记录(Tx2 已经提交)而不是 3 条。因此,假设在 Tx1 结束时不成立,这就是 Tx1 应该回滚的原因。 如果您和 SQL 标准对应该回滚的内容存在分歧,我认为 Postgres 遵循 SQL 标准是明智的;) 我更新了问题,现在 Tx1 发出 UPDATE 语句,并且 Tx1 仍然能够提交。在 MySQL 和 SQL Server 上,Tx2 被阻止执行。在 PostgreSQL 和 Oracle 上,Tx2 运行良好,Tx1 提交没有任何失败。你不认为 Tx1 应该回滚吗?查看 Tx3 上的结果。 在 UPDATE 之前放置幻像插入,您应该会遇到序列化失败。您将执行时间与提交时间混淆了;序列化检查发生在语句执行时,考虑到其他未提交事务中发生的情况,但只查看其他已执行的未提交语句

以上是关于Oracle 和 PostgreSQL 中的 Write Skew 异常不回滚事务的主要内容,如果未能解决你的问题,请参考以下文章

有没办法在postgreSQL中查询oracle上的数据

PostgreSQL 中的 ORACLE wm_concat 等效项

使用 JDBC 时,Oracle 的 REF CURSOR 在 Postgresql 中的等价物是啥?

PostgreSQL Oracle 兼容性之存储过程

PostgreSQL兼容性之oracle的rowid(APP)

postgresql数据类型test相当于oracle中的啥数据类型