ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Node.js 数据库连接报错排查:connect 被调用多次时如何用 TaoToken 统一配置通道

Node.js 数据库连接报错排查:connect 被调用多次时如何用 TaoToken 统一配置通道 1. Node.js 里 connect 被调用多次到底是谁在偷偷重连如果你在 Node.js 项目里看到控制台反复打印「数据库连接成功」或者日志里connect被调用了好几次甚至报出connection.model() should not be run with new这类异常那基本可以确定你的连接对象被重复创建了。这个问题在同时连接多个数据库、或者用mongoose.createConnection()管理多库时特别常见。核心原因有两个方向。第一是模块加载层面Node.js 的require有缓存机制但如果你在多个文件里各自require了一个「导出连接实例」的模块而那个模块本身在顶层就执行了createConnection那么每次被不同路径引用时可能触发多次初始化。第二是连接池与生命周期层面mongoose.connect()是全局单例连接只能连一个库要连多个库必须用createConnection()但很多人把createConnection写在了路由处理函数或定时任务里请求一来就新建一个连接连接数暴涨connect自然被反复调用。这篇内容适合正在用 Node.js Mongoose 或原生驱动连数据库、遇到重复连接报错、想统一管理 API 通道和 Key 的开发者。我会从复现步骤开始带你定位根因然后给出可复制的配置骨架最后用 TaoToken 把模型调用和数据库配置的 Key 通道统一收口避免散落在各个文件里。实测下来把连接创建收敛到单一入口再配合统一配置重复 connect 的问题基本一次解决。2. 先复现createConnection 被重复触发的典型写法要排查问题先得能稳定复现。下面这段代码是很多人踩过的坑把连接创建写在一个「工具模块」里然后在多个业务文件里引用。// db.js —— 问题写法 const mongoose require(mongoose); const db mongoose.createConnection(mongodb://localhost/electricity_user_database); db.on(error, console.error.bind(console, connect error)); db.once(open, function () { console.log(数据库连接成功); }); module.exports db;然后在userService.js和orderService.js里都require(./db)。表面上看require有缓存db.js只会执行一次。但如果你在项目里用了动态require、或者打包工具把模块拆成了不同 chunk、又或者你在测试环境里反复delete require.cache那createConnection就会被执行多次connect被调用多次。更隐蔽的一种情况你在app.js里连了一个库又在某个中间件里用createConnection连了第二个库两个连接对象都注册了open事件日志就重复了。复现命令很简单先装依赖npm init -y npm install mongoose然后建两个文件分别引用同一个db.js启动后观察控制台。如果看到两次「数据库连接成功」说明复现成功。注意mongoose.connect()是全局连接只能连一个数据库。要连多个库必须用createConnection()并且每个库对应一个独立的连接实例。这是官方设计不是 bug。3. 用 TaoToken 统一配置通道Key 和 API 收口在解决连接重复问题之前先把配置管理理顺。很多项目里数据库地址、模型 API Key、第三方服务地址散落在.env、config.js、甚至硬编码在业务文件里改一个地方要翻半天。我习惯用 TaoToken 作为统一的 API 通道入口把模型调用和配置读取收口到一处。TaoToken 的官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。你可以在控制台创建 Key然后通过统一的 base URL 调用模型对话、Coding Plan 等能力。对于 Node.js 项目来说这意味着你不需要在代码里维护多个厂商的 endpoint只需要一个 Key 和一个 base URL。具体操作先到控制台生成 API Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。生成后把 Key 写进环境变量不要提交到 Git。# .env TAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api DB_URImongodb://localhost/electricity_user_database然后在项目里用一个config.js统一读取// config.js require(dotenv).config(); module.exports { taotoken: { apiKey: process.env.TAOTOKEN_API_KEY, baseUrl: process.env.TAOTOKEN_BASE_URL, }, db: { uri: process.env.DB_URI, }, };这样做的价值在于数据库连接配置和模型 API 配置走同一套读取逻辑后续要换环境、换 Key只改.env一个地方。如果你需要查看接入文档可以访问 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的参数说明。4. 可复制的 settings.json / config.toml 骨架与连接修复接下来是重点把连接创建收敛到单一入口并用配置文件管理。先给一个settings.json骨架适合放在项目根目录供 Node.js 读取{ database: { connections: [ { name: electricity_user, uri: mongodb://localhost/electricity_user_database, options: { maxPoolSize: 10, serverSelectionTimeoutMS: 5000 } }, { name: electricity_order, uri: mongodb://localhost/electricity_order_database, options: { maxPoolSize: 10, serverSelectionTimeoutMS: 5000 } } ] }, taotoken: { baseUrl: https://taotoken.net/api, apiKeyEnv: TAOTOKEN_API_KEY } }如果你更习惯 TOML可以用config.toml[taotoken] base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY [[database.connections]] name electricity_user uri mongodb://localhost/electricity_user_database max_pool_size 10 [[database.connections]] name electricity_order uri mongodb://localhost/electricity_order_database max_pool_size 10然后写一个连接管理器保证每个库只创建一次连接// connectionManager.js const mongoose require(mongoose); const settings require(./settings.json); const connections new Map(); function getConnection(name) { if (connections.has(name)) { return connections.get(name); } const config settings.database.connections.find((c) c.name name); if (!config) { throw new Error(未找到数据库配置: ${name}); } const conn mongoose.createConnection(config.uri, config.options); conn.on(error, (err) console.error([${name}] connect error, err)); conn.once(open, () console.log([${name}] 数据库连接成功)); connections.set(name, conn); return conn; } module.exports { getConnection };关键点用Map做缓存同一个 name 第二次调用直接返回已有连接不会重复createConnection。业务代码里这样用const { getConnection } require(./connectionManager); const userDb getConnection(electricity_user); const userSchema new mongoose.Schema({ name: String }); const UserModel userDb.model(User, userSchema); // 注意用 db.model不是 new mongoose.model module.exports { UserModel };这里就解决了 excerpt 里提到的connection.model() should not be run with new问题用createConnection得到的连接实例创建模型时必须用db.model()不能用new mongoose.model()。5. 验证请求与成功结果日志确认只连一次配置改完后要验证connect只被调用一次。写一个测试脚本模拟多次调用getConnection// verify.js const { getConnection } require(./connectionManager); const a getConnection(electricity_user); const b getConnection(electricity_user); const c getConnection(electricity_order); console.log(同一连接是否复用:, a b); console.log(不同库是否独立:, a ! c); setTimeout(() { console.log(验证结束); process.exit(0); }, 3000);运行node verify.js预期输出[electricity_user] 数据库连接成功 [electricity_order] 数据库连接成功 同一连接是否复用: true 不同库是否独立: true 验证结束如果你看到「数据库连接成功」只打印了两次对应两个库而不是四次或更多说明连接复用生效了。如果还是重复打印检查是不是在别的文件里又直接调用了mongoose.createConnection。再验证一下 TaoToken 通道是否可用。用 Node.js 发一个模型对话请求// testTaotoken.js const config require(./config); async function main() { const res await fetch(${config.taotoken.baseUrl}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${config.taotoken.apiKey}, }, body: JSON.stringify({ model: claude-3-5-sonnet, messages: [{ role: user, content: 回复 ok 即可 }], }), }); const data await res.json(); console.log(TaoToken 响应:, JSON.stringify(data).slice(0, 200)); } main();运行后如果能看到正常返回说明 Key 和 base URL 配置正确。你也可以直接在模型对话页面测试地址是 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 不用写代码就能验证通道。6. 本篇常见错排查connect 多次调用的五个坑第一个坑在路由处理函数里调用createConnection。每个请求进来都新建连接连接池很快耗尽。修复方式就是上面的getConnection缓存。第二个坑require路径大小写不一致。在 macOS 上不敏感到了 Linux 服务器上会当成两个不同模块导致db.js被执行两次。检查所有require(./db)和require(./DB)是否统一。第三个坑测试框架里delete require.cache。Jest 的resetModules或者手动清缓存会让模块重新执行连接也跟着重建。测试环境建议用beforeAll只初始化一次。第四个坑mongoose.connect()和createConnection()混用。全局连接和独立连接同时存在事件监听重复注册。统一用createConnection管理多库或者全局只用一个connect。第五个坑db.model()和new mongoose.model()混用。前者用于独立连接后者用于全局连接。用错了就报should not be run with new。记住createConnection返回的实例创建模型用db.model()。如果你在排查过程中需要生成更多测试用例或者让模型帮你分析日志可以用 Coding Plan 长期跑编码任务入口是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。需要管理多个 Key 时到 API Keys 页面操作https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。最后说一个我踩过的坑连接管理器里的Map缓存在热更新nodemon时不会自动清空旧连接还挂着。开发环境可以在SIGUSR2信号里手动关闭所有连接或者干脆接受 nodemon 重启进程。生产环境用 PM2 的话进程重启会自然清理不用额外处理。把连接创建收敛到connectionManager.js一个文件配合settings.json和 TaoToken 统一 Key 通道重复 connect 的问题就不会再出现了。
返回列表