错误的PostgreSQL查询结果具有显式锁和并发事务

时间:2014-10-06 16:31:06

标签: sql postgresql transactions locking

在为PostgreSQL编写一些SQL查询时,我发现了一些不寻常的行为,我觉得这有点令人不安。

假设我们有下表“test”:

+----+-------+---------------------+
| id | value |     created_at      |
+----+-------+---------------------+
|  1 | A     | 2014-01-01 00:00:00 |
|  2 | A     | 2014-01-02 00:00:00 |
|  3 | B     | 2014-01-03 00:00:00 |
|  4 | B     | 2014-01-04 00:00:00 |
|  5 | A     | 2014-01-05 00:00:00 |
|  6 | B     | 2014-01-06 00:00:00 |
|  7 | A     | 2014-01-07 00:00:00 |
|  8 | B     | 2014-01-08 00:00:00 |
+----+-------+---------------------+

有两个交易,A和B,并行运行。

A: begin;           /* Begin transaction A */
B: begin;           /* Begin transaction B */
A: select * from test where id = 1 for update; /* Lock one row */
B: select * from test where value = 'B' order by created_at limit 3 for update; /* This query returns immediately since it does not need to return row with id=1 */
B: select * from test where value = 'A' order by created_at limit 3 for update; /* This query blocks because row id=1 is locked by transaction A */
A: update test set created_at = '2014-01-09 00:00:00' where id = 1; /* Modify the locked row */
A: commit;

只要事务A提交并释放id = 1的行,事务B的被阻止查询就会返回以下结果:

+----+-------+---------------------+
| id | value |     created_at      |
+----+-------+---------------------+
|  1 | A     | 2014-01-09 00:00:00 |
|  2 | A     | 2014-01-02 00:00:00 |
|  5 | A     | 2014-01-05 00:00:00 |
+----+-------+---------------------+

这些行肯定不是由“created_at”排序的,而id = 1的行甚至不应该在返回的行中。事务A和B同时运行的事实导致事务B中的错误结果,如果事务一个接一个地执行,则不会发生这种情况。这似乎违反了事务隔离。

这是一个错误吗?

如果这不是错误并且预期会产生这些结果,那么就DB返回的结果的可靠性而言,这意味着什么?如果我有一个高度并发的环境,并且后续代码依赖于按日期排序的行,则会出现错误。

但是,如果我们运行与上面相同的指令序列,但用以下内容替换update语句:

update test set value = 'B', created_at = '2014-01-09 00:00:00' where id = 1;

...然后被阻止的查询返回正确的结果:

+----+-------+---------------------+
| id | value |     created_at      |
+----+-------+---------------------+
|  2 | A     | 2014-01-02 00:00:00 |
|  5 | A     | 2014-01-05 00:00:00 |
|  7 | A     | 2014-01-07 00:00:00 |
+----+-------+---------------------+

在这种情况下,阻止查询是否会执行两次,因为其初始结果无效?

我对PostgreSQL最感兴趣,但我也想知道其他支持行级锁定的RDBMS是否就是这种情况,例如Oracle,SQL Server和MySQL。

1 个答案:

答案 0 :(得分:5)

这里有几件事情要发生。首先,这是记录在案的行为。其次,你没有看到整个故事,因为你没有尝试更新会话“B”中的任何内容。

  

这似乎违反了事务隔离。

取决于您正在运行的隔离级别。 PostgreSQL's default transaction isolation levelREAD COMMITTED

这是PostgreSQL中的documented behavior

  

可以在READ COMMITTED上运行SELECT命令   事务隔离级别并使用ORDER BY和一个锁定子句来实现   无序返回行。这是因为首先应用ORDER BY。   该命令对结果进行排序,但可能会阻止尝试获取结果   锁定一行或多行。一旦SELECT解除阻塞,一些   排序列值可能已被修改,导致这些行   似乎是出了问题(尽管它们按顺序排列   原始列值)。

一种解决方法(也记录在案,相同的链接)是将FOR UPDATE移动到子查询中,但这需要一个表锁。

要了解PostgreSQL 真正在这种情况下做了什么,请在会话“B”中运行更新。

create table test (
  id integer primary key,
  value char(1) not null,
  created_at timestamp not null
);
insert into test values
(1, 'A', '2014-01-01 00:00:00'),
(2, 'A', '2014-01-02 00:00:00'),
(3, 'B', '2014-01-03 00:00:00'),
(4, 'B', '2014-01-04 00:00:00'),
(5, 'A', '2014-01-05 00:00:00'),
(6, 'B', '2014-01-06 00:00:00'),
(7, 'A', '2014-01-07 00:00:00'),
(8, 'B', '2014-01-08 00:00:00');
A: begin;           /* Begin transaction A */
B: begin;           /* Begin transaction B */
A: select * from test where id = 1 for update; /* Lock one row */
B: select * from test where value = 'B' order by created_at limit 3 for update; /* This query returns immediately since it does not need to return row with id=1 */
B: select * from test where value = 'A' order by created_at limit 3 for update; /* This query blocks because row id=1 is locked by transaction A */
A: update test set created_at = '2014-01-09 00:00:00' where id = 1; /* Modify the locked row */
A: commit;
B: update test set value = 'C' where id in (select id from test where value = 'A' order by created_at limit 3); /* Updates 3 rows */
B: commit;

现在,看看桌子。

scratch=# select * from test order by id;
 id | value |     created_at      
----+-------+---------------------
  1 | A     | 2014-01-09 00:00:00
  2 | C     | 2014-01-02 00:00:00
  3 | B     | 2014-01-03 00:00:00
  4 | B     | 2014-01-04 00:00:00
  5 | C     | 2014-01-05 00:00:00
  6 | B     | 2014-01-06 00:00:00
  7 | C     | 2014-01-07 00:00:00
  8 | B     | 2014-01-08 00:00:00

会话“A”成功将id为1的行更新为“2014-01-09”。会话“B”成功更新了值为“A”的其余三个行。获取的更新语句锁定了id号2,5和7;我们知道因为那些是实际更新的行。较早的select语句锁定了不同的行 - 行1,2和5。

如果您启动第三个​​终端会话,则可以阻止会话B的更新,并锁定第7行以进行更新。