Mongoose 如何管理连接和处理错误?
Mongoose 连接管理和错误处理涉及两个核心问题:如何建立和维护稳定的数据库连接,以及如何对不同类型的错误进行分类处理。面试中常从连接生命周期、错误分类、重连策略三个角度考察。
连接建立与生命周期
Mongoose 通过 mongoose.connect() 建立连接,返回 Promise。连接建立后,Mongoose 内部会缓冲所有模型操作,因此即使连接尚未完成也可以定义模型和执行查询。
javascript// 基本连接 await mongoose.connect("mongodb://127.0.0.1:27017/mydb"); // 生产环境推荐配置 await mongoose.connect("mongodb://127.0.0.1:27017/mydb", { maxPoolSize: 50, // 连接池最大连接数 minPoolSize: 5, // 连接池最小连接数 serverSelectionTimeoutMS: 5000, // 服务器选择超时 socketTimeoutMS: 45000, // Socket 超时 heartbeatFrequencyMS: 10000, // 心跳频率 retryWrites: true // 重试写入 });
Mongoose 连接有四种状态,通过 mongoose.connection.readyState 获取:
0(disconnected):已断开1(connected):已连接2(connecting):正在连接3(disconnecting):正在断开
连接生命周期会触发以下事件,需要分别监听处理:
javascriptconst db = mongoose.connection; db.on("connected", () => console.log("Mongoose connected")); db.on("error", (err) => console.error("Connection error:", err.message)); db.on("disconnected", () => console.warn("Mongoose disconnected")); db.on("reconnected", () => console.log("Mongoose reconnected")); db.on("close", () => console.log("Connection closed"));
关键点:disconnected 事件不一定伴随 error 事件触发。Mongoose 失去连接时可能只是断开而不报错,所以必须同时监听 disconnected 才能可靠检测连接丢失。
两类连接错误的区别
这是面试高频考点。Mongoose 将连接错误分为两类,处理方式完全不同:
初始连接错误:mongoose.connect() 首次连接失败时,Promise 被 reject,Mongoose 不会自动重连。必须手动处理:
javascript// 方式一:try-catch async function connectDB() { try { await mongoose.connect(process.env.MONGODB_URI); console.log("Connected to MongoDB"); } catch (error) { console.error("Initial connection failed:", error.message); process.exit(1); // 初始连接失败通常应终止进程 } } // 方式二:Promise.catch mongoose.connect(process.env.MONGODB_URI) .catch(err => { console.error("Initial connection failed:", err); process.exit(1); });
已建立连接后的错误:连接建立成功后如果发生中断,MongoDB 驱动会自动尝试重连,同时触发 error 事件。此时不应退出进程,而是记录日志并等待重连:
javascriptmongoose.connection.on("error", (err) => { // 不退出进程,只记录日志 console.error("Post-connection error:", err.message); });
查询与写入的错误分类
CastError — 类型转换错误
当传入的值无法转换为 Schema 定义的类型时触发,最常见于 ObjectId 格式错误:
javascriptasync function findUser(id) { try { if (!mongoose.Types.ObjectId.isValid(id)) { throw new Error("Invalid ID format"); } return await User.findById(id); } catch (error) { if (error.name === "CastError") { throw { status: 400, message: `Invalid ${error.path}: ${error.value}` }; } throw error; } }
ValidationError — 验证错误
Schema 校验失败时触发,包含每个字段的详细错误信息:
javascriptasync function createUser(data) { try { return await User.create(data); } catch (error) { if (error.name === "ValidationError") { const fields = Object.keys(error.errors); const messages = fields.map(f => `${f}: ${error.errors[f].message}`); throw { status: 422, message: "Validation failed", details: messages }; } throw error; } }
DuplicateKeyError — 唯一索引冲突
错误码 11000,通常由 unique 索引冲突引起:
javascriptasync function register(email) { try { return await User.create({ email }); } catch (error) { if (error.code === 11000) { const field = Object.keys(error.keyPattern)[0]; throw { status: 409, message: `${field} already exists` }; } throw error; } }
超时错误
查询超过 maxTimeMS 或连接超时时触发:
javascript// 设置查询超时 const users = await User.find({ status: "active" }) .maxTimeMS(5000) // 查询级超时 .catch(err => { if (err.name === "MongooseError" && err.message.includes("timed out")) { throw { status: 504, message: "Database query timeout" }; } throw err; });
重连策略
注意:Mongoose 7+ 版本已移除 autoReconnect、reconnectTries、reconnectInterval 选项。新版 MongoDB 驱动内置了自动重连机制,不再需要手动配置这些参数。
如果需要自定义重连逻辑(如限制重试次数、退避策略),可以在 disconnected 事件中实现:
javascriptlet retryCount = 0; const MAX_RETRIES = 5; const BASE_DELAY = 1000; mongoose.connection.on("disconnected", async () => { if (retryCount >= MAX_RETRIES) { console.error("Max retries reached, shutting down"); process.exit(1); } const delay = BASE_DELAY * Math.pow(2, retryCount); // 指数退避 retryCount++; console.log(`Reconnecting in ${delay}ms (attempt ${retryCount}/${MAX_RETRIES})`); setTimeout(async () => { try { await mongoose.connect(process.env.MONGODB_URI); retryCount = 0; // 重连成功,重置计数 } catch (err) { console.error("Reconnect failed:", err.message); } }, delay); });
连接池管理
连接池控制着应用与 MongoDB 之间的连接数量。配置不当会导致连接泄漏或性能瓶颈。
javascriptawait mongoose.connect(process.env.MONGODB_URI, { maxPoolSize: 50, // 单个连接池最大连接数,默认 100 minPoolSize: 5, // 最小保持连接数 maxIdleTimeMS: 30000, // 空闲连接最大存活时间 waitQueueTimeoutMS: 5000 // 等待可用连接的超时时间 });
监控连接池状态应使用官方 API 而非内部私有属性:
javascript// 正确方式:使用 serverStatus 命令 const status = await mongoose.connection.db.admin().serverStatus(); const poolInfo = status.connections; // current: 当前连接数 // available: 可用连接数 // totalCreated: 总创建连接数
优雅关闭
应用收到终止信号时,应先关闭数据库连接再退出进程,避免数据丢失和连接泄漏:
javascriptasync function gracefulShutdown(signal) { console.log(`Received ${signal}, closing MongoDB connection...`); try { await mongoose.connection.close(); console.log("MongoDB connection closed"); process.exit(0); } catch (error) { console.error("Error during shutdown:", error); process.exit(1); } } process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); process.on("SIGINT", () => gracefulShutdown("SIGINT"));
在容器化部署中,Kubernetes 发送 SIGTERM 后默认等待 30 秒,应确保 mongoose.connection.close() 在此时间内完成。如果存在进行中的长事务,可以设置强制关闭超时:
javascript// 强制关闭超时保护 const shutdownTimeout = setTimeout(() => { console.error("Forced shutdown after timeout"); process.exit(1); }, 25000); // 25秒后强制退出 async function gracefulShutdown(signal) { console.log(`Received ${signal}, shutting down...`); try { await mongoose.connection.close(); clearTimeout(shutdownTimeout); process.exit(0); } catch (error) { clearTimeout(shutdownTimeout); process.exit(1); } }
统一错误处理中间件
在生产项目中,推荐用中间件统一处理 Mongoose 错误,避免在每个路由中重复 try-catch:
javascriptfunction handleMongooseError(err, req, res, next) { if (err.name === "ValidationError") { const errors = Object.values(err.errors).map(e => e.message); return res.status(422).json({ error: "Validation failed", details: errors }); } if (err.code === 11000) { const field = Object.keys(err.keyPattern)[0]; return res.status(409).json({ error: `${field} already exists` }); } if (err.name === "CastError") { return res.status(400).json({ error: `Invalid ${err.path}` }); } if (err.name === "MongooseError" && err.message.includes("timed out")) { return res.status(504).json({ error: "Database timeout" }); } next(err); } app.use(handleMongooseError);
这种写法将错误处理从业务逻辑中抽离出来,路由代码更简洁,错误响应格式也更统一。