许多人可能看到PostgreSQL发出以下错误消息:"ERROR: deadlock detected"
。但这到底意味着什么呢?我们如何防止死锁,如何重现问题?让我们深入研究PostgreSQL的锁机制,了解死锁和死锁超时的真正含义。
死锁是如何产生的?
许多人都想了解什么是死锁以及死锁是如何发生的。他们还想了解如何避免死锁,以及软件开发人员可以做些什么。
如果您想了解死锁是如何发生的,只需要一个包含两行的表。这足以解释死锁的基本原理。
以下是一些易于使用的示例数据:
1
test=# CREATE TABLE t_data (id int, data int); 2
CREATE TABLE 3
test=# INSERT INTO t_data VALUES (1, 100), (2, 200); 4
INSERT 0 2 5
test=# TABLE t_data; 6
id | data 7
----+------ 8
1 | 100 9
2 | 200 10
(2 rows) 复制
问题的关键是,如果数据以不同的顺序更新,事务可能必须等待彼此完成。打比方,如果事务1必须等待事务2,那完全可以。但是,如果事务1必须等待事务2,而事务2必须等待事务1,会发生什么情况呢?在这种情况下,系统有两种选择:
无限地等待
中止一个事务并提交另一个事务。
由于不能无限等待,PostgreSQL将在一段时间后中止其中一个事务(死锁超时)。发生的情况如下:
事务1 | 事务2 | 解释 |
---|---|---|
BEGIN; | BEGIN; | |
UPDATE t_data SET data = data * 10 WHERE id = 1 RETURNING *; | UPDATE t_data SET data = data * 10 WHERE id = 2 RETURNING *; | 没有问题 |
UPDATE t_data SET data = data * 10 WHERE id = 2 RETURNING *; | 必须等待事务2释放包含id=2的行上的锁 | |
… 等待 … | UPDATE t_data SET data = data * 10 WHERE id = 1 RETURNING *; | 想要锁定由事务id锁定的行:现在两个都应该等待 |
… 死锁超时 … | … 死锁超时 … | PostgreSQL等待(死锁超时)并在此超时后触发死锁检测(不是立即) |
update proceeds: “UPDATE 1” | ERROR: deadlock detected | 事物2终止 |
COMMIT; | 其余的正常提交 |
我们将看到的错误消息是:
1
ERROR: deadlock detected 2
DETAIL: Process 70725 waits for ShareLock on transaction 891717; blocked by process 70713. 3
Process 70713 waits for ShareLock on transaction 891718; blocked by process 70725. 4
HINT: See server log for query details. 5
CONTEXT: while updating tuple (0,1) in relation "t_data" 复制
原因是事务必须等待对方。如果两个事务发生冲突,PostgreSQL不会立即解决问题,而是等待死锁超时,然后触发死锁检测算法来解决问题。
为什么PostgreSQL需要等待一段时间才介入并修复问题?原因是死锁检测非常耗资源,因此不立即触发死锁是有意义的。这里的默认值是1秒,它足够高,可以避免无意义的死锁检测,但仍然足够有效,可以及时地解决问题。
如何修复和避免死锁
最重要的是要知道:没有神奇的配置参数来解决这个问题。问题不取决于配置。这取决于操作的执行顺序。换句话说,如果不了解应用及其底层操作,就无法根本地修复它。
唯一可以解决此问题的方法是更改执行顺序,如下一个清单所示:
1
test=# SELECT * FROM t_data ; 2
id | data 3
----+------ 4
1 | 1000 5
2 | 2000 6
(2 rows) 复制
这是在测试之前应该看到的数据。我们可以看到如果两个事务以不同的顺序执行,会发生什么:
事务1 | 事务2 | 解释 |
---|---|---|
BEGIN; | ||
UPDATE t_data SET data = data * 10 WHERE id = 1 RETURNING *; id | data--+------ 1 | 10000 | BEGIN; | |
UPDATE t_data SET data = data * 10 WHERE id = 1 RETURNING *; | ||
UPDATE t_data SET data = data * 10 WHERE id = 2 RETURNING *; id | data--+------- 2 | 20000 | … 等待… | |
COMMIT; | … 等待… | |
UPDATE t_data SET data = data * 10 WHERE id = 1 RETURNING *; id | data--+-------- 1 | 100000 | 重新读取该值并使用新提交的条目 | |
UPDATE t_data SET data = data * 10 WHERE id = 2 RETURNING *; id | data--+-------- 2 | 200000 | 重新读取该值并使用新提交的条目 | |
COMMIT; |
在这种情况下,不存在死锁。然而,在实际工作场景中,很难简单地交换执行顺序。这就是为什么这更多的是一个理论上的解决方案,而不是一个实际的解决方案。但是,没有其他解决死锁问题的方法。在出现死锁的情况下,了解如何预防死锁是最好的治疗方法。
最后 ...
锁真的很重要。在这方面,死锁并不是唯一的问题。性能可能同样重要,因此处理与性能相关的锁也是有意义的。