运行 UPDATE 时 PostgreSQL 出现死锁

问题描述 投票:0回答:3

阅读有关 PostgreSQL 死锁的内容时我有点困惑。

一个典型的死锁例子是:

-- Transaction 1
UPDATE customer SET ... WHERE id = 1
UPDATE customer SET ... WHERE id = 2

-- Transaction 2
UPDATE customer SET ... WHERE id = 2
UPDATE customer SET ... WHERE id = 1

但是如果我将代码更改如下:

-- Transaction 1
UPDATE customer SET ... WHERE id IN (1, 2)

-- Transaction 2
UPDATE customer SET ... WHERE id IN (1, 2)

这里会有死锁的可能性吗?

我的问题本质上是:在第二种情况下,PostgreSQL 是逐一锁定行,还是锁定

WHERE
条件覆盖的整个范围?

提前致谢!

postgresql transactions database-deadlocks
3个回答
49
投票

在 PostgreSQL 中,行在更新时将被锁定——事实上,其实际工作方式是每个元组(行的版本)都有一个名为

xmin
的系统字段,以指示哪个事务使该元组成为当前元组(通过插入或更新)和一个名为
xmax
的系统字段来指示哪个事务使该元组过期(通过更新或删除)。当您访问数据时,它会检查每个元组,通过对照这些值检查您的活动“快照”,以确定它是否对您的交易可见。

如果您正在执行更新,并且与您的搜索条件匹配的元组具有使其对快照可见的 xmin 和活动事务的 xmax,则它会阻塞,等待该事务完成。如果首先更新元组的事务回滚,则您的事务将唤醒并处理该行;如果第一个事务提交,您的事务将被唤醒并根据当前事务隔离级别采取操作。

显然,死锁是发生在不同顺序的行上的结果。 RAM 中没有行级锁,可以同时为所有行获取行级锁,但如果行以相同的顺序更新,则无法获得循环锁。不幸的是,建议的

IN(1, 2)
语法并不能保证这一点。不同的会话可能有不同的活动成本因素,后台“分析”任务可能会更改一个计划和另一个计划生成之间表的统计信息,或者它可能正在使用 seqscan 并受到 PostgreSQL 优化的影响,从而导致新的 seqscan加入已经在进行中的一项并“循环”以减少磁盘 I/O。

如果您在应用程序代码中或使用游标以相同的顺序一次进行一项更新,那么您只会遇到简单的阻塞,而不是死锁。但一般来说,关系数据库很容易出现序列化失败,最好通过一个框架来访问它们,该框架将根据 SQLSTATE 识别它们并从头开始自动重试整个事务。在 PostgreSQL 中,序列化失败的 SQLSTATE 始终为 40001 或 40P01。

http://www.postgresql.org/docs/current/interactive/mvcc-intro.html


5
投票

PostgreSQL 是逐一锁定行,还是锁定整个范围

PostgreSQL 逐行锁定行。

令人沮丧的是,没有办法像选择和插入那样对更新(或删除)进行排序。

解决方案是预先使用

SELECT FOR UPDATE
锁定记录并自连接。

UPDATE customer AS c SET ...
FROM (
  SELECT ctid
  FROM customer
  WHERE id IN (1, 2)
  ORDER BY id -- the optimal ordering varies, but it must be strict and consistent
  FOR UPDATE
) AS c2
WHERE c.ctid = c2.ctid

(这里我使用行的物理 ID

ctid
,这对于连接来说可以稍微快一些。)

PostgreSQL 会找到记录,按顺序锁定记录,然后更新记录。

您可以检查查询计划来说服自己这一点。

有一些开销,但很小,特别是考虑到

UPDATE
首先通常不是一个轻松的操作。


0
投票

基于Paul Draper的解决方案改进SQL语句:

UPDATE customer SET ...
WHERE id IN (
  SELECT id
  FROM customer
  WHERE id IN (1, 2)
  ORDER BY id
  FOR UPDATE
)
© www.soinside.com 2019 - 2024. All rights reserved.