隔离级别和显式锁定:意外的序列化错误

时间:2019-01-13 21:30:28

标签: postgresql

我正在编写一个Web应用程序,并且我一直在尝试用ISOLATION LEVEL REPEATABLE READ的事务包装来自每个Web请求的SQL语句,以查找我的Web应用程序可能在哪里进行不可重复的读取。我的计划是在无法重复读取的情况下不重试,而只是向用户报告服务器端错误(500)并记录信息(因为我希望这种情况很少见)。

同时,我的代码中有些地方使用显式锁定(SELECT ... FOR UPDATE)以确保我正确地序列化访问,并且不会引起不可重复的读取。

但是,将这两个想法结合在一起给我带来了意想不到的结果。

下面是一个最小示例


+--------------------------------------------------+--------------------------------------------------+
| Session 1                                        | Session 2                                        |
+--------------------------------------------------+--------------------------------------------------+
| BEGIN;                                           |                                                  |
| SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; |                                                  |
| SELECT * FROM users WHERE id = 1 FOR UPDATE;     |                                                  |
| (returns as expected)                            |                                                  |
+--------------------------------------------------+--------------------------------------------------+
|                                                  | BEGIN;                                           |
|                                                  | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; |
|                                                  | SELECT * FROM users WHERE id = 1 FOR UPDATE;     |
|                                                  | (blocks as expected)                             |
+--------------------------------------------------+--------------------------------------------------+
| UPDATE users SET name = 'foobar' WHERE id = 1;   |                                                  |
| COMMIT;                                          |                                                  |
| (works as expected)                              |                                                  |
+--------------------------------------------------+--------------------------------------------------+
|                                                  | ERROR:  could not serialize access due           |
|                                                  | to concurrent update                             |
+--------------------------------------------------+--------------------------------------------------+

我的期望是,由于会话2在该SELECT语句之前未进行任何读取,并且由于该语句仅在会话1完成更新后才返回,因此会话2应该查看该表的更新版本,这将使其可重复读取。

我认为,很可能Postgres在运行BEGIN时获得了版本,而不是在它获得第一个SELECT的锁时才获得版本。

我的问题

  • 我的理解正确吗?
  • 有没有办法使Postgres表现出我所期望的方式?
  • 这会被视为错误,还是按预期工作

1 个答案:

答案 0 :(得分:1)

来自"13.2.2. Repeatable Read Isolation Level"

  

UPDATEDELETESELECT FOR UPDATESELECT FOR SHARE命令在搜索目标行方面与SELECT相同:它们只会找到目标事务开始时已提交的行。但是,这样的目标行可能在被发现时已被另一个并发事务更新(或删除或锁定)。在这种情况下,可重复读事务将等待第一个更新事务提交或回滚(如果仍在进行中)。如果第一个更新程序回滚,则其效果将被否定,并且可重复读取事务可以继续更新最初找到的行。但是,如果第一个更新程序提交(并实际上更新或删除了该行,而不仅仅是锁定了该行),则可重复的读取事务将与消息一起回滚

    ERROR:  could not serialize access due to concurrent update
     

因为可重复读取事务开始后,可重复读取事务无法修改或锁定其他事务更改的行。

是的,如果您通过BEGIN表示交易开始,那么您的理解似乎是正确的。而且,这不是一个错误,而是按预期工作并记录在案。

据我所知,READ COMMITTED(默认值)应该执行您想要的操作。请注意,在客户端1中提交了第一笔事务后,SELECT FOR UPDATE被阻止,直到客户端2提交或回滚,因为SELECT FOR UPDATE现在成功了。因此,客户端2中的第一个事务将读取相同的值(除非自身对其进行更改),直到事务结束为止。

Client 1                                        | Client 2
------------------------------------------------+------------------------------------------------
BEGIN TRANSACTION;                              |
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
SELECT * FROM users WHERE id = 1 FOR UPDATE;    |
                                                | BEGIN TRANSACTION;
                                                | SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
                                                | SELECT * FROM users WHERE id = 1 FOR UPDATE;
                                                | -- blocks
UPDATE users SET name = 'foobar' WHERE id = 1;  |
COMMIT;                                         |
                                                | -- name = 'foobar' is read
BEGIN TRANSACTION;                              |
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
SELECT * FROM users WHERE id = 1 FOR UPDATE;    |
-- blocks                                       |
                                                | SELECT * FROM users WHERE id = 1 FOR UPDATE;
                                                | -- name = 'foobar' is read
                                                | COMMIT;
UPDATE users SET name = 'foobaz' WHERE id = 1;  |
-- name = 'foobaz' is written                   |