错误的PostgreSQL查询结果具有显式锁和并发事务
在为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”中的任何内容.
取决于您正在运行的隔离级别. PostgreSQL’s default transaction isolation level是READ COMMITTED. 这是PostgreSQL中的documented behavior.
一种解决方法(也记录在案,相同链接)是将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行以进行更新. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |