介绍
许多人可能看到过 PostgreSQL 发出这样的错误消息:"ERROR: deadlock detected"。但这到底意味着什么?我们如何防止死锁,如何重现问题?让我们来深入了解下 PostgreSQL 的锁,并了解死锁和 deadlock_timeout 的真正含义。
死锁是一个重要的问题,每个数据库都可能发生死锁。基本上,如果两个事务必须相互等待,就会发生死锁。死锁通常与阻塞的查询有关,但也略有不同,它会导致查询被取消,因为它给另一个查询造成了死锁。
示例
让我们来看一个简单的例子,首先开始设置一个小的测试用例:
CREATE TABLE test1 (id int, num int);
INSERT INTO test1 VALUES (1, 100), (2, 200);
重现一个死锁的最简单方法是,执行以下操作:
--- session 1
BEGIN;
UPDATE test1 SET num = num + 10 WHERE id = 1;
--- session 2
BEGIN;
UPDATE test1 SET num = num + 10 WHERE id = 2;
UPDATE test1 SET num = num + 10 WHERE id = 1; --- this will block waiting for session 1 to finish
--- session 1
UPDATE test1 SET num = num + 10 WHERE id = 2; --- this can never finish as it deadlocks against session 2
在经过deadlock_timeout设置的时间之后,你会在 PostgreSQL 日志中看到死锁的问题。在这种情况下,PostgreSQL 发现此操作永远不会完成,并向日志中发出以下内容:
ERROR: deadlock detected
DETAIL: Process 70725 waits for ShareLock on transaction 891717; blocked by process 70713.
Process 70713 waits for ShareLock on transaction 891718; blocked by process 70725.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,1) in relation "test1"
PostgreSQL 甚至还善意地告诉我们是哪一行导致了冲突。在此处的示例中,问题的根源是在一个元组 (0,1)。您在此处可以看到的是 ctid,它是表中行的唯一标识符。它告诉我们表内一个行的物理位置。在此示例中,它是第一个块 (0) 中的第一行。
你可能会认为,在生产环境中永远不会发生死锁,但不幸的事实是,在大量使用 ORM 框架时,可能会隐藏产生死锁的循环依赖情况,当你使用到复杂的事务时,这肯定是需要注意的。
查询 pg_stat_database 视图
您可以在视图pg_stat_database中看到数据库级别的统计信息,其中有一个 deadlocks 列。你应该多留意这个列,因为它会告诉你,你的数据库是否有很多死锁,这将不可避免地减慢操作速度。
当两个或多个事务都锁定了资源,并试图获取已被其他事务锁定的资源时,就会发生死锁。在正常情况下,事务 A 锁定资源 A,事务 B 锁定资源 B。在死锁情况下,事务 A 将尝试锁定资源 B。这反过来又使其进入等待模式,因为事务 B 已经锁定了资源 B。这样,两个事务都会锁定彼此的资源。当发生死锁时,PostgreSQL 会取消其中一个涉及的事务,并将其从等待队列中删除,以让其他事务继续其工作。
通常,死锁的出现,意外着您的应用程序存在设计错误,一般的建议是,修复导致死锁的应用程序逻辑。一个推荐的实践是在 postgresql.conf 中启用 log_lock_waits 选项,重新加载配置,并不时地检查日志。当发生死锁时,将会在日志中记录有关冲突查询的额外信息。
我们可以使用下面的查询,来检查死锁:
SELECT deadlocks FROM pg_stat_database
WHERE datname = current_database();
一旦你发现了很多死锁,你就可以找到那些导致死锁的查询,并优化它们。