在Oracle和PostgreSQL中,写入偏差异常不会回滚事务。

11

我注意到在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模型是否只提供快照隔离保证而没有写偏斜异常检测?

更新

我甚至更改了Tx1,发出了一个UPDATE语句,更改所有属于同一个postpost_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上。


1
TX2提交,因为它是第一个更改数据并提交的。TX1不更改相同的数据,因此不需要抛出任何异常,因为可以构建包括TX1在内的序列化事务时间线。如果TX1更改相同的数据或依赖于您的数据的数据,则会引发错误。 - hruske
我将尝试使用读写事务。 - Vlad Mihalcea
我在TX1中添加了一个UPDATE语句,但是TX1仍然能够提交。 - Vlad Mihalcea
@Vlad:这种行为完全符合您在运行TX1然后运行TX2时所看到的情况。换句话说,事务已经成功序列化;没有序列化失败。Postgres wiki上有很多好的例子,可以让您了解何时何地以及为什么应该期望发生序列化错误。 - Nick Barnes
这是有道理的。如果 Tx1 和 Tx2 完全按顺序运行,我们将得到相同的结果。 - Vlad Mihalcea
我成功添加了一个用例,在PostgreSQL上Tx1失败,这证实了你之前的说法。有趣的是,在Oracle中它运行得非常好。 - Vlad Mihalcea
4个回答

8
在1995年的论文《对ANSI SQL隔离级别的批判》中,Jim Gray等人将幻读描述为:

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

需要注意的一点是,ANSI SQL P3仅禁止对谓词进行插入(根据某些解释,还包括更新),而上述P3的定义则禁止任何满足谓词的写操作,一旦读取了谓词,写操作可以是插入、更新或删除。

因此,幻读并不意味着您可以简单地返回当前运行事务开始时的快照,并假装为查询提供相同的结果即可保护您免受实际的幻读异常。
在最初的SQL Server 2PL(两阶段锁定)实现中,为查询返回相同的结果意味着谓词锁定。
MVCC(多版本并发控制)快照隔离(在Oracle中错误地命名为Serializable)实际上不能防止其他事务插入/删除与当前正在运行的事务中已执行并返回结果集的查询相匹配的行。因此,我们可以想象以下情况,其中我们想对所有员工进行加薪:
1. Tx1:SELECT SUM(salary) FROM employee where company_id = 1; 2. Tx2:INSERT INTO employee (id, name, company_id, salary) VALUES (100, 'John Doe', 1, 100000); 3. Tx1:UPDATE employee SET salary = salary * 1.1; 4. Tx2:COMMIT; 5. Tx1:COMMIT: 在这种情况下,首席执行官运行第一个事务(Tx1),因此:
  1. 她首先检查公司所有薪水的总和。
  2. 与此同时,人力资源部门运行第二个事务(Tx2),因为他们刚刚成功地雇用了John Doe并给了他10万美元的薪水。
  3. CEO决定提高10%的工资,考虑到薪水总额,但不知道薪水总额已经增加了100k。
  4. 与此同时,人力资源事务Tx2已提交。
  5. Tx1已提交。

砰! CEO在旧快照上做出了决策,给出了一份可能无法由当前更新的薪资预算维持的加薪。

您可以在以下文章中查看此用例的详细说明(包括大量图表)。

这是幻读还是写入偏斜

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

A5B 写偏斜 假设T1读取与C()一致的x和y,然后T2读取x和y,写入x并提交。然后T1写入y。如果x和y之间存在约束,则可能会被违反。就历史记录而言:

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

在Oracle中,事务管理器可能会或可能不会检测到上述异常,因为它不使用谓词锁或索引范围锁(next-key locks),像MySQL一样。
只有当Bob针对员工表发出读取请求时,PostgreSQL才能捕获此异常,否则无法防止该现象。

更新

起初,我认为可串行性(serializability)也意味着时间顺序(time ordering)。然而,正如Peter Bailis非常好地解释的那样,墙钟排序(wall-clock ordering)或线性可串行性(linearizability)仅适用于严格可串行性(strict serializability)。

因此,我的假设是基于严格可串行系统进行的。但这不是可串行化所应提供的。可串行隔离模型对时间没有任何保证,并且只要它们等效于某个串行执行,操作就可以被重新排序。

因此,根据可串行定义,如果第二个事务没有发出任何读取,则可以发生幻象读取(Phantom Read)。但在严格可串行模型中(即2PL提供的模型),即使第二个事务没有针对我们试图防止幻像读取的相同条目进行读取,幻象读取也会被防止。


嘿@Vlad,在你的书第97页的总结表中,你提到Postgres允许WRITE SKEW,但是当你在第91页解释WRITE SKEW时,两个事务都会对两个表进行读取,Postgres将能够捕获并禁止此操作。 - GionJh
1
实际上,存在两种写入偏差类型:G1:反依赖循环(在不相交的读取上的写入偏差)G2:反依赖循环(在谓词读取上的写入偏差)。我书中的是G1,这里是G2。PostgreSQL 在RR模式下都不会阻止它们的出现。 - Vlad Mihalcea
是的,确实明白了,刚刚审查了一下,还看了一些关于这个主题的博客文章 :)。 - GionJh
1
我很高兴能够帮助。 - Vlad Mihalcea

2
我想指出Vlad Mihalcea的答案是完全错误的。
这是一个Phantom Read还是Write Skew?
都不是 - 这里没有异常情况,事务是可以序列化为Tx1 -> Tx2的。
SQL标准规定: “序列化执行被定义为并发执行SQL事务的操作,其产生与那些同样的SQL事务的某些串行执行相同的效果。”
如果Bob对员工表发出读取,则PostgreSQL可以捕获此异常,否则不会防止该现象。
PostgreSQL在这里的行为是100%正确的,只是“翻转”了表面上的事务顺序。

你在这里指的确切结果是什么?例如,在MS SQL中,Tx2被阻塞,因此我无法先提交Tx2。 - Yaroslav Schekin
没错。所以,像任何并发控制机制一样,冲突要么被预防(2PL),要么被检测到(MVCC)。在这里,2PL可以预防它,但MVCC允许它发生。 - Vlad Mihalcea
更多详情请关注我在Voxxed Zurich的Transactions and Concurrency Control presentation - Vlad Mihalcea
2
不,SSI不允许这样做。再说一遍,你没有展示出任何异常情况。看,任何产生与tx1->tx2或tx2->tx1相同结果的执行(当然只考虑已提交的事务),根据定义都是可串行化的。这里的区别在于2PL阻止了其中一个可串行化的调度,而SSI(正确地)没有这样做。 - Yaroslav Schekin
1
然而,Serializable隔离级别不涉及任何墙钟排序语义。是的,没错。感谢您修正答案(顺便提一下,感谢您提供的链接)! - Yaroslav Schekin
显示剩余2条评论

2
您看到的不是幻读。如果在第二次查询时出现新行,那才是幻读(幻像出现得出乎意料)。
在Oracle和PostgreSQL中,使用SERIALIZABLE隔离级别可以保护您免受幻读的影响。
Oracle和PostgreSQL之间的区别在于,Oracle中的SERIALIZABLE隔离级别仅提供快照隔离(足以防止幻读出现),而在PostgreSQL中,它将保证真正的串行化(即,总是存在一个SQL语句序列化的结果相同)。如果您想在Oracle和PostgreSQL中获得相同的结果,请在PostgreSQL中使用REPEATABLE READ隔离级别。

不,count的结果没有被修改。即使TX2在此期间插入了一个新值,TX1两次发出查询时都返回3。现在无法在TX1中看到新行。 - Laurenz Albe
没错。但是幻读应该防止第二个事务修改我之前选择的视图。这在SQL Server(2PL)和MySQL(因为InnoDB为Serializable获取锁)中发生,但不适用于Oracle和PostgreSQL。正是为了防止这些异常情况而进行了交易交错的设计。 - Vlad Mihalcea
@Vlad: SERIALIZABLE 仅保证事物可以被序列化。如果您想要锁定事物,请使用锁定 - Nick Barnes
@Nick SERIALIZABLE 保证事务的执行结果与顺序执行的结果相同。请查看更新的部分,即在 Tx2 中发出 select 的部分。如果我的业务逻辑是说,如果 select count 为 3,则插入新记录,则 Tx2 在 MVCC 中运行良好。但是,如果我在 Tx1 后执行 Tx2,则不会发出插入操作,因为计数值为 0(版本已在 Tx1 中更改为 100)。这在 PostgreSQL 上可以工作,但在 Oracle 的 SERIALZABLE 事务中无法工作。 - Vlad Mihalcea
这种现象的一个很好的描述可以在Fekete、Liarokapis、O'Neil、O'Neil和Shasha的《Making Snapshot Isolation Serializable》一文中找到。ACM Trans. Database Syst. 30(2): 492-528 (2005)。另一篇不错的阅读材料是:Serializable Executions with Snapshot Isolation: Modifying Application Code or Mixing Isolation Levels? DASFAA 2008: 267-281。 - Glenn Paulley
显示剩余7条评论

0

Postgres文档定义幻读为:

事务重新执行查询,返回满足搜索条件的一组行,并发现由于另一个最近提交的事务而更改了满足条件的行集。

因为您的选择在其他事务提交之前和之后都返回相同的值,所以不符合幻读的标准。


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接