三月 5, 2025
摘要:在本教程中,您将学习如何在 PostgreSQL 中避免发生死锁的问题。
目录
准备工作
在任何数据库管理系统中,由于并发的资源锁定,都可能会发生死锁。数据库引擎负责检测死锁,而应用程序负责防止死锁。PostgreSQL 数据库能够检测死锁;它还为开发人员提供了一些特性,以防止其应用程序代码中的死锁。
处理方案
让我们构造一些简单的死锁情况,我们将看到 PostgreSQL 为处理它们提供的所有选择:
有两个不同的数据库会话,分别执行 SQL 语句,如下所示:
会话 1 | 会话 2 |
---|---|
|
|
从前面的示例中,在会话 1 中,由于会话 1 和 2 之间的相互锁定,发生了死锁的错误。在前面的详细消息中,它清楚地表明进程 8913 等待由进程 10417 持有的事务,而进程 10417 正在等待由进程 8913 持有的事务。
从前面的示例中可以看出,死锁不会导致任何数据丢失,只会导致事务失败。死锁通常发生在多个事务相互等待对方释放锁的时候,形成一个循环依赖。所以,为了避免这些死锁情况,关键在于如何打破这种循环。
按固定顺序访问资源
首先,让我们来考虑下事务的设计。如果事务都以不同顺序访问表或行,可能会出现循环依赖。
如果,约定所有事务按照相同顺序操作数据,这样不同的会话就会以相同的顺序请求锁,从而避免循环依赖。数据库中通常会有多个表,如果每个事务都按同样的顺序更新这些表,如表A → 表B
,就能减少死锁的可能。同样,在表中有很多行,如果每个事务都按主键以相同的顺序更新行,也能减少死锁的可能。
会话 1 | 会话 2 |
---|---|
|
|
不过,实际应用中,可能很难保证所有事务都遵循同一顺序,特别是复杂业务逻辑的时候。
使用 FOR UPDATE
这是一种尝试避免死锁问题的方法,方法是预先锁定将要在会话中更新的所有必需的记录。要预锁定所有必需的元组,我们必须在 SELECT 语句中使用 FOR UPDATE 子句。让我们来看看我们将如何使用这种方法解决上述问题:
会话 1 | 会话 2 |
---|---|
|
|
在前面的示例中,会话 2 的事务将处于等待状态,直到会话 1 的事务完成。在这里,我们只是让两个事务以可序列化的方式运行。这意味着事务不会相互冲突。此方法无法用于处理集合相关操作的 SQL 查询。也就是说,在执行 UNION/INTERSECT/EXCEPT 操作的 SQL 查询中,FOR UPDATE 的使用会受到限制。
咨询锁
PostgreSQL 提供了咨询锁,这是一种外部锁定机制,我们可以在应用程序中强制执行锁定,以实现并发数据访问。让我们来看看我们将如何使用咨询锁避免死锁:
会话 1 | 会话 2 |
---|---|
|
|
在前面的示例中,我们使用咨询锁,让事务以可序列化的方式运行。使用咨询锁的唯一缺点是,我们需要一个应用程序,来强制执行锁定和解锁行为。此外,会话级咨询锁不会释放锁,即使事务失败或回滚也是如此。但是,当会话关闭时,该会话中所有关联的咨询锁会自动释放。
注意:有关咨询锁的详细信息,请参阅 PostgreSQL 文档:咨询锁。
管理员最佳实践
在 PostgreSQL 中,很多管理操作也可能会造成死锁的发生,管理员可以通过以下策略来降低其发生概率:
-
ALTER TABLE:在业务低峰期执行
ALTER TABLE
, 该命令需要一个ACCESS EXCLUSIVE
锁,这几乎会阻塞访问该表的所有其他进程。 -
创建索引:尽量使用
CREATE INDEX CONCURRENTLY
。 -
手动清理:
VACUUM FULL
需要表上的ACCESS EXCLUSIVE
锁,因此应仅在极少数情况下使用。 -
减少事务持有锁的时间:长事务会长时间占用锁,增加了冲突发生的概率。可以将事务拆分为更小的操作,尽快提交或回滚。避免在事务中执行耗时操作(如外部 API 调用、复杂计算)。
-
设置锁超时:让事务在等待锁超时后自动回滚,避免无限等待。在应用程序中引入重试机制(应用层捕获异常并重试)。
SET lock_timeout = '2s';