加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

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

发布时间:2020-12-13 16:01:40 所属栏目:百科 来源:网络整理
导读:在为PostgreSQL编写一些SQL查询时,我发现了一些不寻常的行为,我觉得有点令人不安. 假设我们有下表“test”: +----+-------+---------------------+| id | value | created_at |+----+-------+---------------------+| 1 | A | 2014-01-01 00:00:00 || 2 | A
在为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.

解决方法

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

This seems like a violation of transaction isolation.

取决于您正在运行的隔离级别. PostgreSQL’s default transaction isolation level是READ COMMITTED.

这是PostgreSQL中的documented behavior.

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. This is because ORDER BY is applied first.
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).

一种解决方法(也记录在案,相同链接)是将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,'2014-01-02 00:00:00'),(3,'B','2014-01-03 00:00:00'),(4,'2014-01-04 00:00:00'),(5,'2014-01-05 00:00:00'),(6,'2014-01-06 00:00:00'),(7,'2014-01-07 00:00:00'),(8,'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行以进行更新.

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读