简体   繁体   English

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

[英]Wrong PostgreSQL query results with explicit locks and concurrent transaction

While writing some SQL queries for PostgreSQL, I discovered some unusual behaviour, which I find slightly disturbing. 在为PostgreSQL编写一些SQL查询时,我发现了一些异常行为,这些行为我觉得有些不安。

Say we have the following table "test": 假设我们有下表“ 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 |
+----+-------+---------------------+

There are two transactions, A and B, running in parallel. 有两个事务并行运行。

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;

As soon as transaction A commits and releases the row with id=1, the blocked query of transaction B returns the following result: 事务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 |
+----+-------+---------------------+

These rows are most certainly not ordered by "created_at" and row with id=1 shouldn't even be among the returned rows. 这些行最肯定不是按“ created_at”排序的,id = 1的行甚至不应该位于返回的行中。 The fact that transactions A and B were running concurrently, has resulted in wrong results in transaction B, which would not have happened if the transactions had been executed one after the other. 事务A和B同时运行的事实导致了事务B的错误结果,如果一个接一个地执行事务,则不会发生。 This seems like a violation of transaction isolation. 这似乎违反了事务隔离。

Is this a bug? 这是错误吗?

If this is not a bug and these results are expected, what does this mean in terms of the reliability of results returned by the DB? 如果这不是错误,并且可以预期得到这些结果,那么就DB返回的结果的可靠性而言,这意味着什么? If I had a highly concurrent environment and the subsequent code relied on the rows actually being ordered by date, there would be errors. 如果我有一个高度并发的环境,并且随后的代码依赖于按日期实际排序的行,则将出现错误。

If, however, we run the same sequence of instructions as above, but replace the update statement with the following: 但是,如果我们执行与上述相同的指令序列,但将update语句替换为以下内容:

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

... then the blocked query returns the correct result: ...然后被阻止的查询返回正确的结果:

+----+-------+---------------------+
| 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 |
+----+-------+---------------------+

In this case, does the blocked query get executed twice since its initial result gets invalidated? 在这种情况下, 由于查询的初始结果无效,阻塞查询是否会执行两次

I'm most interested in PostgreSQL, but I would also like to know if this is the case with other RDBMS that support row-level locking, such as Oracle, SQL Server and MySQL. 我对PostgreSQL最感兴趣,但是我也想知道其他支持行级锁定的RDBMS(例如Oracle,SQL Server和MySQL)是否属于这种情况。

There are a couple of things going on here. 这里发生了几件事。 First, this is documented behavior. 首先,这是记录的行为。 Second, you don't see the whole story, because you didn't try to update anything in session "B". 其次,您看不到整个故事,因为您没有尝试在会话“ B”中更新任何内容。

This seems like a violation of transaction isolation. 这似乎违反了事务隔离。

Depends on what isolation level you're running at. 取决于您所运行的隔离级别。 PostgreSQL's default transaction isolation level is READ COMMITTED . PostgreSQL的默认事务隔离级别READ COMMITTED

This is documented behavior in PostgreSQL. 这是PostgreSQL中记录的行为

It is possible for a SELECT command running at the READ COMMITTED transaction isolation level and using ORDER BY and a locking clause to return rows out of order. SELECT命令可能以READ COMMITTED事务隔离级别运行,并且使用ORDER BY和锁定子句可以使行无序返回。 This is because ORDER BY is applied first. 这是因为ORDER BY首先被应用。 The command sorts the result, but might then block trying to obtain a lock on one or more of the rows. 该命令对结果进行排序,但随后可能会阻止尝试获得对一个或多个行的锁定。 Once the SELECT unblocks, some of the ordering column values might have been modified, leading to those rows appearing to be out of order (though they are in order in terms of the original column values). 一旦SELECT解除阻塞,某些排序列值可能已被修改,从而导致这些行看起来是乱序的(尽管就原始列值而言,它们是有序的)。

One workaround (also documented, same link) is to move the FOR UPDATE into a subquery, but this requires a table lock. 一种解决方法(也已记录,同一链接)是将FOR UPDATE移到子查询中,但这需要使用表锁。

To see what PostgreSQL really does in this situation, run an update in session "B". 要查看的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;

Now, look at the table. 现在,看看桌子。

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

Session "A" succeeded in updating the row having id 1 to '2014-01-09'. 会话“ A”成功将ID为1的行更新为“ 2014-01-09”。 Session "B" succeeded in updating the three remaining rows whose value was 'A'. 会话“ B”成功更新了剩余的三行,其值为“ A”。 The update statement obtained locks on id numbers 2, 5, and 7; 更新语句获取ID号2、5和7的锁; we know that because those were the rows actually updated. 我们知道,因为这些是实际更新的行。 The earlier select statement locked different rows--rows 1, 2, and 5. 较早的select语句锁定了不同的行-第1、2和5行。

You can block session B's update if you start a third terminal session, and lock row 7 for update. 如果启动第三个终端会话,则可以阻止会话B的更新,并锁定第7行以进行更新。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM