PostgreSQL 教程: 避免死锁

三月 5, 2025

摘要:在本教程中,您将学习如何在 PostgreSQL 中避免发生死锁的问题。

目录

准备工作

在任何数据库管理系统中,由于并发的资源锁定,都可能会发生死锁。数据库引擎负责检测死锁,而应用程序负责防止死锁。PostgreSQL 数据库能够检测死锁;它还为开发人员提供了一些特性,以防止其应用程序代码中的死锁。

处理方案

让我们构造一些简单的死锁情况,我们将看到 PostgreSQL 为处理它们提供的所有选择:

有两个不同的数据库会话,分别执行 SQL 语句,如下所示:

会话 1 会话 2
BEGIN;

UPDATE test SET t=1 WHERE t=1;

UPDATE test SET t=2 WHERE t=2;
-- Waiting for the record 2
-- which is locked in session 2

ERROR: deadlock detected
DETAIL: Process 10417 waits for
ShareLock on transaction 452459;
blocked by process 8913.
Process 8913 waits for ShareLock on
transaction 452458; 
blocked by process 10417.

ROLLBACK;
BEGIN;

UPDATE test SET t=2 WHERE t=2;

UPDATE test SET t=1 WHERE t=1;
-- Waiting for the record 1
-- which is locked in session 1

END;

从前面的示例中,在会话 1 中,由于会话 1 和 2 之间的相互锁定,发生了死锁的错误。在前面的详细消息中,它清楚地表明进程 8913 等待由进程 10417 持有的事务,而进程 10417 正在等待由进程 8913 持有的事务。

从前面的示例中可以看出,死锁不会导致任何数据丢失,只会导致事务失败。死锁通常发生在多个事务相互等待对方释放锁的时候,形成一个循环依赖。所以,为了避免这些死锁情况,关键在于如何打破这种循环。

按固定顺序访问资源

首先,让我们来考虑下事务的设计。如果事务都以不同顺序访问表或行,可能会出现循环依赖。

如果,约定所有事务按照相同顺序操作数据,这样不同的会话就会以相同的顺序请求锁,从而避免循环依赖。数据库中通常会有多个表,如果每个事务都按同样的顺序更新这些表,如表A → 表B,就能减少死锁的可能。同样,在表中有很多行,如果每个事务都按主键以相同的顺序更新行,也能减少死锁的可能。

会话 1 会话 2
BEGIN;

UPDATE test SET t=1 WHERE t=1;

UPDATE test SET t=2 WHERE t=2;

END;
BEGIN;

UPDATE test SET t=1 WHERE t=1;
-- Waiting for the record 1
-- which is locked in session 1

UPDATE test SET t=2 WHERE t=2;

END;

不过,实际应用中,可能很难保证所有事务都遵循同一顺序,特别是复杂业务逻辑的时候。

使用 FOR UPDATE

这是一种尝试避免死锁问题的方法,方法是预先锁定将要在会话中更新的所有必需的记录。要预锁定所有必需的元组,我们必须在 SELECT 语句中使用 FOR UPDATE 子句。让我们来看看我们将如何使用这种方法解决上述问题:

会话 1 会话 2
BEGIN;

SELECT * FROM test WHERE t IN(1, 2)
 FOR UPDATE;

UPDATE test SET t=1 WHERE t=1;
UPDATE test SET t=2 WHERE t=2;
END;
BEGIN;

SELECT * FROM test WHERE t IN(1, 2)
 FOR UPDATE;
-- Waiting for the session to
-- release the lock on records 1, 2

UPDATE test SET t=2 WHERE t=2;
UPDATE test SET t=1 WHERE t=1;
END;

在前面的示例中,会话 2 的事务将处于等待状态,直到会话 1 的事务完成。在这里,我们只是让两个事务以可序列化的方式运行。这意味着事务不会相互冲突。此方法无法用于处理集合相关操作的 SQL 查询。也就是说,在执行 UNION/INTERSECT/EXCEPT 操作的 SQL 查询中,FOR UPDATE 的使用会受到限制。

咨询锁

PostgreSQL 提供了咨询锁,这是一种外部锁定机制,我们可以在应用程序中强制执行锁定,以实现并发数据访问。让我们来看看我们将如何使用咨询锁避免死锁:

会话 1 会话 2
BEGIN;
SELECT pg_advisory_lock(t) FROM test
 WHERE t IN (1,2);

UPDATE test SET t=1 WHERE t=1;
UPDATE test SET t=2 WHERE t=2;
SELECT pg_advisory_unlock(t) FROM test
 WHERE t IN (1,2);
END;
BEGIN;
SELECT pg_advisory_lock(t) FROM test
 WHERE t IN (1,2);
-- Waiting for the session1 to release lock

UPDATE test SET t=2 WHERE t=2;
UPDATE test SET t=1 WHERE t=1;
SELECT pg_advisory_unlock(t) FROM test
 WHERE t IN (1,2);
END;

在前面的示例中,我们使用咨询锁,让事务以可序列化的方式运行。使用咨询锁的唯一缺点是,我们需要一个应用程序,来强制执行锁定和解锁行为。此外,会话级咨询锁不会释放锁,即使事务失败或回滚也是如此。但是,当会话关闭时,该会话中所有关联的咨询锁会自动释放。

注意:有关咨询锁的详细信息,请参阅 PostgreSQL 文档:咨询锁

管理员最佳实践

在 PostgreSQL 中,很多管理操作也可能会造成死锁的发生,管理员可以通过以下策略来降低其发生概率:

  • ALTER TABLE:在业务低峰期执行ALTER TABLE, 该命令需要一个ACCESS EXCLUSIVE锁,这几乎会阻塞访问该表的所有其他进程。

  • 创建索引:尽量使用CREATE INDEX CONCURRENTLY

  • 手动清理VACUUM FULL需要表上的ACCESS EXCLUSIVE锁,因此应仅在极少数情况下使用。

  • 减少事务持有锁的时间:长事务会长时间占用锁,增加了冲突发生的概率。可以将事务拆分为更小的操作,尽快提交或回滚。避免在事务中执行耗时操作(如外部 API 调用、复杂计算)。

  • 设置锁超时:让事务在等待锁超时后自动回滚,避免无限等待。在应用程序中引入重试机制(应用层捕获异常并重试)。

    SET lock_timeout = '2s';
    
  • 日志与监控:启用死锁日志记录,配置和监控死锁的指标。

了解更多

PostgreSQL 教程

处理死锁问题