当我在不同的事务中并行执行查询时,我遇到了 Postgres 的奇怪行为。 Postgres 15.3,数据库上的事务隔离级别 -读提交。请求如下:
with inserting as (
insert into device(device_guid, platform, created_at)
values (
'5f0ca10f-a257-44ac-940e-a808c63a9914'::uuid,
'WEB',
NOW()
)
ON CONFLICT (device_guid) DO NOTHING
returning *
)
select id, device_guid, platform, created_at, 'INSERT' as db_operation from inserting
UNION ALL
select id, device_guid, platform, created_at, 'GET' as db_operation from device where device_guid = '5f0ca10f-a257-44ac-940e-a808c63a9914'::uuid
(device_guid 上有一个唯一索引)
预期会出现以下行为:
- 输入是device_guid和一些数据(在示例中为'5f0ca10f-a257-44ac-940e-a808c63a9914','WEB',NOW())
- 尝试在 CTE 中插入传递的 device_guid。如果这样的device_guid已经存在,那么我们只需退出CTE进入下一步(选择)。如果表中没有这样的 device_guid,那么我们将其插入表中,并将其插入到 CTE 中
- 接下来,我们从物理表设备和插入临时 CTE 表中进行选择。这里 db_operation 会告诉您该行是从哪里获取的,是从 CTE(INSERT)获取的,还是该行已经在设备表中(GET)。
- 如果执行查询两次,一切都会按预期工作:第一次,出现一行 db_operation = 'INSERT',第二次,出现一行 db_operation = 'GET'。
但是,当两个事务同时开始使用相同的 device_guid 执行此查询时,一切都会中断。我期望以下行为是正确的:
- 事务 A比事务 B稍早开始。
- 字符串 A尝试将一行插入到具有唯一索引的表中。这意味着事务 A将对该行采取排他锁,并对索引采取共享锁,直到事务提交为止。
- 线程 B看到线程 A已获取锁,则只需等待锁被释放。
- Thr-A插入成功并继续进行两次选择。但自从如果事务还未提交,则不会移除锁。
- Tr-ya A完成其选择和整个查询脚本并提交事务,释放锁。
- Tr-ya B发现锁被移除,尝试使用相同的 device_guid 向设备中插入一行,发生冲突,它悄悄地跳过冲突,并使用 select 转到块中
- 在这个阶段,字符串A应该已经被提交(或者更准确地说,已经在第5点),这意味着从设备表中选择应该已经看到字符串A插入的行。这保证了我们的读取已提交隔离级别。
- 这里带有 union all 的选择块应该返回一行,其中 db_operation = 'GET',即物理表设备中已有的行,由tr-ya A放置在那里
但事实上,第 8 点返回0 行..
该错误最初是在 spring data jdbc 和 jooq 中发现的,但后来我在标准 psql 中多次重现了该错误。这可以通过在begin 中包装 SQL 脚本来完成; ... 犯罪;并在两个独立的 psql 控制台中运行它。但只能在一次写入开始; ...(没有提交;),然后在第二个控制台中使用begin 执行整个脚本; ... 犯罪; 如果您查看锁,您会发现第二个事务将被第一个事务阻止,并且锁在 device_guid 字段的 unicode 索引上可见。但是一旦你输入提交;在第一个控制台中,第二个控制台将完成其交易并返回 0 行。
看上去出现这种情况的原因是,在准备事务A的提交时,首先会释放索引和行上的锁,然后才提交整个事务A。在释放锁和提交之间的这段时间内,事务 B设法执行其代码,而从未看到事务 A提交的行。第三天,我一直在试图了解我是否期待了错误的行为,或者 Postgres 是否表现异常。审查了有关阻塞、CTE 和隔离级别的文档。看来一切都正如我所料的那样。如有任何信息我将不胜感激!