数据库事务泄漏

发布: (2026年1月16日 GMT+8 10:08)
5 min read
原文: Dev.to

I’m happy to translate the article for you, but I’ll need the text you’d like translated. Could you please paste the content (or the portion you want translated) here? Once I have the text, I’ll provide the Simplified Chinese translation while preserving the original formatting, markdown, and any code blocks or URLs.

介绍

我们经常谈论内存泄漏,但在后端开发中还有另一种潜在的性能杀手:数据库事务泄漏。

我最近花时间调试一个遗留代码库,发现某个特定模块的测试在单独运行时表现完美,但在作为完整套件的一部分运行时却始终失败。罪魁祸首是什么?一个我最初当作偶然的 “Database connection timeout”。下面是我如何发现我们的代码在“泄漏”数据库连接以及我们是如何修复它的。

症状:“Loner” 测试

在单独运行时,我的 User 模块 测试是绿色的(通过)。然而,当它们与另外五十个测试一起运行时,便会突然超时。

数据库并没有真正变慢;是因为它已耗尽。之前的测试打开了事务却从未关闭,导致连接池中的连接被占用,直到后续的测试没有可用的连接为止。

罪魁祸首 #1:提前返回陷阱

在我们的旧版控制器中,许多函数依赖手动事务管理。多个实例出现了 提前返回 的情况,开发者在函数退出前忘记关闭事务。

有缺陷的代码

const t = await startTransaction();
try {
  if (someCondition) {
    // 提前返回!事务 't' 将永远保持打开状态
    // 直到数据库或服务器终止进程。
    return { status: 400, message: "Invalid Request" };
  }

  await t.commit();
} catch (e) {
  await t.rollback();
}

罪魁祸首 #2:共享事务所有权

第二个问题更为微妙:嵌套事务自杀。父函数创建了一个事务并将其传递给子函数,子函数随后自行提交或回滚该事务。当控制返回父函数时,父函数尝试提交已经关闭的事务。

错误代码

async function childFunction(t) {
  try {
    const data = await db.create({}, { transaction: t });
    await t.commit(); // Child closes the transaction
    return data;
  } catch (e) {
    await t.rollback();
    throw e;
  }
}

async function parentScope() {
  const t = await startTransaction();
  try {
    const data = await childFunction(t);
    await t.commit(); // Error! The transaction is already finished.
    return data;
  } catch (e) {
    await t.rollback();
  }
}

为什么这没有导致生产环境崩溃?

你可能会想:如果我们在泄漏连接,为什么生产服务器每小时都不崩溃?

答案是 PM2。我们的生产环境使用 PM2 来管理 Node.js 进程。当连接池耗尽,应用开始卡顿或崩溃时,PM2 会自动重启实例。这会清除“泄漏”的连接,充当了一个临时(且危险)的补丁。用户只会感受到“应用偶尔变慢”。

解决方案:正确的事务管理

1. 显式生命周期管理

始终确保每条可能的代码路径(尤其是提前返回)都处理事务。

const t = await startTransaction();
try {
  if (someCondition) {
    await t.rollback(); // Always clean up before returning!
    return { status: 400 };
  }
  await t.commit();
} catch (e) {
  await t.rollback();
}

2. “单一所有者”原则

一个好的经验法则:创建事务的函数应该负责关闭它。如果将事务传递给子函数,子函数可以使用它,但绝不能自行提交或回滚。

async function childFunction(t) {
  // Use the transaction 't', but don't commit/rollback here
  return await db.create({}, { transaction: t });
}

async function parentScope() {
  const t = await startTransaction();
  try {
    await childFunction(t);
    await t.commit(); // Only the creator manages the lifecycle
  } catch (e) {
    await t.rollback();
  }
}

结论

通过修复这些事务泄漏,我们的测试套件从不稳定且缓慢变为稳定且快速。如果你的测试单独运行时通过,但在组合运行时失败,请不要忽视那些 “Connection Timeout” 错误——你可能只是数据库逻辑中出现了泄漏。

Back to Blog

相关文章

阅读更多 »