0Pricing
SQL Academy · 课时

死锁:检测与避免

了解死锁如何发生、Postgres 如何检测死锁,并设计能够避免死锁的锁顺序规则

死锁:检测与避免 是 CoddyKit 上的免费 SQL Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 SQL Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 SQL Academy 课程共包含 4 节课。

什么是死锁

两个事务各自持有对方想要的锁,因此谁都无法继续。数据库会检测到这个循环,并中止其中一个事务。

经典死锁

事务 A 锁定第 1 行,事务 B 锁定第 2 行。A 请求第 2 行,B 请求第 1 行。双方陷入僵局。

-- Tx A:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- waiting for B...

-- Tx B:
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
-- waiting for A...

-- ERROR: deadlock detected

PostgreSQL 会检测死锁

每隔 deadlock_timeout(默认 1 秒),PostgreSQL 就会检查锁循环。如果发现循环,它会因错误代码 40P01 中止其中一个事务。

ERROR:  deadlock detected
DETAIL:  Process 1234 waits for ShareLock on transaction 5678 ...

锁顺序规则

解决方法是:在所有代码路径中始终按相同顺序获取锁。

-- Always update the lower id first:
UPDATE accounts SET balance = balance - 100 WHERE id = LEAST(:from, :to);
UPDATE accounts SET balance = balance + 100 WHERE id = GREATEST(:from, :to);

热点行死锁

对同一热点行进行快速 UPDATE 通常会触发锁等待,而不是死锁。您可以使用排队机制、拆分热点行,或在应用代码中将 UPDATE 串行化。

FOR UPDATE 锁定读取的行

在读取时获取写锁,避免稍后出现意外:

BEGIN;
SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
-- both rows locked in id order
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

跳过已锁定的行

对于队列表,使用“获取任意可用行”的模式:

SELECT * FROM jobs
WHERE status = 'pending'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;
-- skips rows other workers have locked

NOWAIT

立即失败,而不是等待:

SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;
-- ERROR: could not obtain lock on row in relation "accounts"

诊断死锁

提高 log_lock_waits 的级别,并在日志中记录死锁上下文。日志条目会显示两个事务及其查询。

应用重试循环

死锁是可以恢复的——重试被中止的事务:

for (let attempt = 0; attempt < 3; attempt++) {
  try {
    await runTransaction();
    break;
  } catch (e) {
    if (e.code === '40P01') continue;     // deadlock
    throw e;
  }
}

缩小锁的范围

缩短事务持续时间——每个被访问的行都会一直保持锁定,直到 COMMIT。不要在事务中执行 HTTP 调用或长时间计算。

为外键建立索引以避免锁升级

删除父行时,会检查每个子行。没有 FK 索引时,这会导致全表扫描 AND 行锁定。请为每个 FK 列建立索引。

回顾

死锁难以完全避免,因此应通过设计尽量减少它们。

  • 按一致的顺序获取锁
  • 尽早使用 FOR UPDATE 表明操作意图
  • 队列使用 SKIP LOCKED
  • 发生死锁错误(40P01)时重试
  • 保持事务简短

快速检查

预防死锁最可靠的设计原则是什么?

常见问题解答

「死锁:检测与避免」课时是免费的吗?

是的 — 「死锁:检测与避免」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 SQL Academy 课程的其余内容,请升级到 CoddyKit PRO。 SQL Academy 课程共包含 4 节课。

「死锁:检测与避免」这节课中我会学到什么?

了解死锁如何发生、Postgres 如何检测死锁,并设计能够避免死锁的锁顺序规则 你通过在浏览器中直接运行的动手代码来练习 SQL Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 SQL Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 SQL Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「死锁:检测与避免」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 SQL Academy 课中编写并运行代码吗?

能。每节 SQL Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. ACID 属性与异常
  2. 隔离级别:READ COMMITTED、REPEATABLE READ、SERIALIZABLE
  3. 死锁:检测与避免
  4. 乐观锁与悲观锁
← 返回 SQL Academy