我正在编写一个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
的锁时才获得版本。
我的问题:
答案 0 :(得分:1)
来自"13.2.2. Repeatable Read Isolation Level":
UPDATE
,DELETE
,SELECT FOR UPDATE
和SELECT 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 |