数据库事务泄漏
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” 错误——你可能只是数据库逻辑中出现了泄漏。