我注意到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语句,该语句更改属于同一version
的所有post_comment
记录的post
列。
这样,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提交juts很好。
顺便说一句,你可以自己运行所有这些例子,因为它们在GitHub上。
你观察到的不是幻读。那就是如果第二次发出查询时会出现一个新行(幻像出现意外)。
使用SERIALIZABLE
隔离可以保护Oracle和PostgreSQL中的幻像读取。
Oracle和PostgreSQL之间的区别在于,Oracle中的SERIALIZABLE
隔离级别仅提供快照隔离(这足以防止幻像出现),而在PostgreSQL中它将保证真正的可序列化(即,始终存在SQL语句的序列化,导致相同的结果)。如果你想在Oracle和PostgreSQL中获得相同的东西,请在PostgreSQL中使用REPEATABLE READ
隔离。
我喜欢这个问题,因为它证明了SQL Standard中的Phantom Read定义只是描绘了效果而没有说明这个数据异常的根本原因:
P3(“Phantom”):SQL事务T1读取满足某些行的N行。然后,SQL事务T2执行SQL语句,这些语句生成满足SQL事务T1使用的一行或多行。如果SQL事务T1然后重复使用相同的初始读取,它将获得不同的行集合。
在1995年的论文中,A Critique of ANSI SQL Isolation Levels,Jim Gray和co将Phantom Read描述为:
P3:r1 [P] ... w2 [y in P] ...(c1或a1)(幻影)
一个重要的注意事项是ANSI SQL P3仅禁止对谓词进行插入(以及根据某些解释进行更新),而上面的P3的定义禁止在读取谓词后满足谓词的任何写入 - 写入可以是插入,更新,或删除。
因此,Phantom Read并不意味着您可以简单地返回当前正在运行的事务开始时的快照,并假装为查询提供相同的结果将保护您免受实际的Phantom Read异常。
在原始的SQL Server 2PL(两阶段锁定)实现中,为查询返回相同的结果意味着Predicate Locks。
MVCC(多版本并发控制)快照隔离(在Oracle中错误地命名为Serializable)实际上不会阻止其他事务插入/删除与相同过滤条件匹配的行与已执行并在当前运行中返回结果集的查询交易。
出于这个原因,我们可以想象以下场景,我们希望将加薪应用于所有员工:
SELECT SUM(salary) FROM employee where company_id = 1;
INSERT INTO employee (id, name, company_id, salary)
VALUES (100, 'John Doe', 1, 100000);
UPDATE employee SET salary = salary * 1.1;
COMMIT;
COMMIT:
在这种情况下,CEO运行第一个事务(Tx1),因此:
繁荣!首席执行官已就旧的快照作出决定,加薪可能不会由当前更新的薪资预算维持。
您可以在the following post中查看此用例的详细说明(包含大量图表)。
这是Phantom Read还是Write Skew?
根据Jim Gray and co的说法,这是一个Phantom Read,因为Write Skew被定义为:
A5B写入偏斜假设T1读取x和y,它们与C()一致,然后T2读取x和y,写入x和提交。然后T1写y。如果x和y之间存在约束,则可能违反。在历史方面:
A5B:r1 [x] ... r2 [y] ... w1 [y] ... w2 [x] ...(发生c1和c2)
在Oracle中,事务管理器可能会或可能不会检测到上面的异常,因为它不使用谓词锁或index range locks (next-key locks),如MySQL。
只有当Bob针对employee表发出读取时,PostgreSQL才会设法捕获此异常,否则,这种现象不会被阻止。
最初,我假设Serializability也意味着时间排序。但是,正如very well explained by Peter Bailis一样,挂钟排序或线性化仅用于严格可串行化。
因此,我的假设是针对严格的可序列化系统。但这不是Serializable应该提供的。 Serializable隔离模型不保证时间,只要它们等同于某些串行执行,就允许重新排序操作。
因此,根据Serializable定义,如果第二个事务不发出任何读取,则可能发生这样的幻像读取。但是,在2PL提供的严格可序列化模型中,即使第二个事务没有针对我们试图防止幻像读取的相同条目发出读取,也会阻止幻像读取。
Postgres文档defines a phantom read为:
事务重新执行查询,返回满足搜索条件的一组行,并发现满足条件的行集由于另一个最近提交的事务而发生了更改。
由于您的select在提交的其他事务之前和之后都返回相同的值,因此它不符合幻像读取的条件。
我只想指出Vlad Mihalcea的回答是完全错误的。
这是幻影读或写歪?
这些都没有 - 这里没有异常,交易可序列化为Tx1 - > Tx2。
SQL标准规定:“可序列化执行被定义为执行同时执行SQL事务的操作,这些操作产生与那些相同SQL事务的某些串行执行相同的效果。”
只有当Bob针对employee表发出读取时,PostgreSQL才设法捕获此异常,否则不会阻止此现象。
PostgreSQL在这里的行为是100%正确的,它只是“翻转”明显的交易顺序。