在构建期间处理 10,000+ 数据库查询:Node.js 连接池故事
Source: Dev.to
请提供您希望翻译的具体内容(文章正文、代码块除外),我将为您翻译成简体中文并保留原有的格式和技术术语。
概览
- PostgreSQL 数据库? ✅
- Prisma ORM? ✅
- Node.js 静态站点生成? ✅
FATAL: too many connections for role "role_xxxxx"
Prisma Accelerate 内置了连接池功能,可防止此类错误。
我的构建过程在 生成 70+ 页面,每个页面都需要多个数据库查询。计算非常简单且残酷:
70 pages × 4 queries per page × concurrent execution = connection‑pool explosion.
问题:Node.js 事件循环 vs. 数据库连接
大多数教程不会告诉你:Node.js 在并发方面 太强大 了。当你生成静态页面时,Node.js 并不会礼貌地等一个页面完成后才开始下一个——它会 同时触发所有页面的生成。每个页面需要:
- 从数据库查询产品
- 查询博客文章以获取相关内容
- 查询分类数据
- 生成动态站点地图条目
这可能导致 280+ 并发数据库连接,而连接池仅限制在 5‑10 个连接。
结果: 构建失败、超时错误,以及大量的挫败感。
为什么标准解决方案失败
尝试 1:增加连接限制
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=20"
结果: 仍然失败。问题不在于限制,而是 连接创建的速率。
尝试 2:手动连接管理
await prisma.$connect();
const data = await prisma.product.findMany();
await prisma.$disconnect();
结果: 让情况更糟。我在快速创建和销毁连接,压垮了数据库服务器。
尝试 3:Prisma 的内置连接池
const prisma = new PrismaClient();
结果: 不够。Prisma 的单例模式非常适合运行时请求,但静态生成的并发特性绕过了它。
解决方案:在 Node.js 中进行查询排队
我需要在构建期间 序列化 数据库操作,同时在运行时保持并发。有效的模式如下:
// lib/db.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
queryQueue: Promise<any> | undefined;
};
const createPrismaClient = () => {
return new PrismaClient({
log:
process.env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'],
});
};
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
/**
* Serialize queries during the production build.
* In dev/runtime the function runs immediately.
*/
const queueQuery = async (fn: () => Promise<any>): Promise<any> => {
if (
process.env.NEXT_PHASE === 'phase-production-build' ||
process.env.NODE_ENV === 'production'
) {
const previousQuery = globalForPrisma.queryQueue ?? Promise.resolve();
const currentQuery = previousQuery
.then(() => fn())
.catch(() => fn()); // ensure the chain continues on error
globalForPrisma.queryQueue = currentQuery;
return currentQuery;
}
return fn();
};
export async function withPrisma(
callback: (prisma: PrismaClient) => Promise<any>
): Promise<any> {
return queueQuery(async () => {
try {
return await callback(prisma);
} catch (error) {
console.error('Database operation failed:', error);
throw error;
}
});
}
关键洞察
使用 promise 链 作为队列。每个查询在 构建期间 等待前一个查询完成,但在 运行时 则立即执行。
工作原理
| 上下文 | 执行流程 |
|---|---|
| 构建(生产) | Query 1 → Query 2 → Query 3 → … (serial) |
| 运行时(服务器) | Query 1 ↘Query 2 → Query 3 → … (concurrent) |
globalForPrisma.queryQueue 充当检查点。每个新查询:
- 等待前一个 promise 解析。
- 执行其数据库操作。
- 成为下一个查询的新检查点。
实现跨代码库
站点地图生成
// app/sitemap.ts
export default async function sitemap() {
const baseUrl = process.env.BASE_URL ?? 'https://example.com';
const staticPages = [
{ url: `${baseUrl}/`, priority: 1 },
// …other static routes
];
// ---- Products -------------------------------------------------
let productPages: { url: string; lastModified: Date; priority: number }[] =
[];
try {
const products = await getAllAmazonProducts({ limit: 1000 });
productPages = products.map((p) => ({
url: `${baseUrl}/products/${p.slug}`,
lastModified: p.updatedAt,
priority: p.featured ? 0.85 : 0.75,
}));
} catch (error) {
console.error('Error fetching products for sitemap:', error);
}
// ---- Blog posts -----------------------------------------------
let blogPages: { url: string; lastModified: Date; priority: number }[] = [];
try {
const posts = await prisma.blogPost.findMany({
where: { status: 'published' },
});
blogPages = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: post.updatedAt,
priority: 0.65,
}));
} catch (error) {
console.error('Error fetching blog posts for sitemap:', error);
}
return [...staticPages, ...productPages, ...blogPages];
}
每个 try‑catch 块都能优雅地处理失败——如果数据库调用失败,该部分会被直接跳过,而不会导致整个构建崩溃。
生产结果
| 指标 | 之前 | 之后 |
|---|---|---|
| 构建成功率 | ~30 % | 100 % |
| 平均构建时间 | ~2 min (不稳定) | 45 s (稳定) |
| 连接超时错误 | 经常 | 零 |
| 生成的页面数 | 70 + (经常失败) | 70 + (全部成功) |
您可以在 elyvora.us 查看此架构处理生产流量——70 + 个基于数据库的页面,全部在没有任何连接错误的情况下构建。
经验教训
- 环境特定行为很重要 – 不要假设构建时的行为与运行时行为相同。Node.js 在不同上下文中表现不同。
- 全局状态并非总是有害 – 使用
globalThis在模块边界之间维护一个 promise 队列,解决了静态生成期间的“多实例”问题。 - 在构建期间序列化数据库工作可以帮你省事 – 简单的 promise 链队列可以防止连接池耗尽,同时不牺牲运行时并发。
祝构建愉快,愿你的连接池保持健康!
优雅降级胜于完美
我的站点地图具有回退逻辑。如果商品查询失败,它仍然会生成静态页面。部分成功 > 完全失败。
在构建期间记录所有信息
console.log(`✅ Sitemap generated with ${allPages.length} URLs`);
console.log(` - Product pages: ${productPages.length}`);
console.log(` - Blog pages: ${blogPages.length}`);
这些日志为你节省了数小时的调试时间。你无法在构建过程中附加调试器,所以 stdout 是你最好的伙伴。
何时使用此模式
- 静态站点生成,使用数据库支持的内容
- 页面数量多(50 页以上)
- 受限的数据库连接池(共享主机、免费层)
- 每页多个查询(复杂的数据关系)
对于仅运行时的应用程序(纯 API 服务器),请坚持使用标准连接池。
结论
Node.js 的异步特性通常是一个超能力。但在带有数据库依赖的静态生成过程中,它会变成一个挑战。解决方案不是与 Node.js 的并发作斗争,而是 控制并发发生的时机。通过在构建期间将查询串行化,而在运行时保持并发,你可以兼顾两者的优势:构建可靠,服务器响应快速。
更新: 该模式已在 elyvora.us 的生产环境中运行超过 3 周,未出现任何连接问题。每小时重建 70 + 页,轻松无压力。