如何用Mongoose实现MongoDB多租户数据库?连接异常求助
MongoDB + Mongoose 多租户(独立库)方案问题解决指南
核心问题根源:connect() 和 createConnection() 的冲突
mongoose.connect() 是创建默认全局连接,所有未指定连接的模型都会自动绑定到这个连接上;而 mongoose.createConnection() 是创建独立的连接实例,和全局默认连接完全隔离。
你之前的操作逻辑是:用createConnection()连主库并挂载Tenant模型,然后在/auth路由用connect()连业务库——这会直接覆盖全局默认连接。后续其他路由如果使用全局mongoose模型(比如mongoose.model('User')),会自动使用新的业务库连接,但主库的Tenant模型是绑定在之前的createConnection实例上的,业务库又没有这个模型,自然会导致路由失效、操作报错。
解决方案:统一用连接池管理所有独立连接
1. 维护连接池,复用租户连接
创建一个全局对象作为连接池,存储已建立的租户连接,key为businessId,value为对应的mongoose连接实例。这样可以避免重复创建连接,还能快速校验是否已连接目标库。
// 全局连接池 const connectionPool = {}; // 主库连接(单独维护) const masterConnection = mongoose.createConnection(process.env.MASTER_DB_URI); // 挂载主库专属模型(比如Tenant) masterConnection.model('Tenant', require('./models/tenant'));
2. 批量挂载业务模型,简化操作
写一个通用函数,给指定连接实例批量挂载所有业务模型,不用逐个手动绑定:
// 批量注册业务模型到指定连接 function registerBusinessModels(connection) { connection.model('User', require('./models/user')); connection.model('Order', require('./models/order')); // 其他业务模型依次添加 }
3. /auth 路由的正确流程
登录时先通过主库查询租户信息,再从连接池获取/创建对应业务库连接,最后用该连接的模型执行登录逻辑:
app.post('/auth', async (req, res) => { const { businessId, username, password } = req.body; // 1. 主库查询租户对应的数据库信息(假设Tenant模型存了dbUri字段) const Tenant = masterConnection.model('Tenant'); const tenant = await Tenant.findOne({ businessId }); if (!tenant) return res.status(404).json({ msg: '租户不存在' }); // 2. 检查连接池是否已有该租户的连接 let businessConnection = connectionPool[businessId]; if (!businessConnection) { // 新建连接并挂载模型 businessConnection = mongoose.createConnection(tenant.dbUri); registerBusinessModels(businessConnection); // 记录连接最后使用时间,用于后续闲置清理 businessConnection.lastUsed = Date.now(); // 存入连接池 connectionPool[businessId] = businessConnection; } else { // 更新最后使用时间 businessConnection.lastUsed = Date.now(); } // 3. 用业务库连接的User模型执行登录 const User = businessConnection.model('User'); const user = await User.findOne({ username }); // 密码校验等逻辑... // 4. 把businessId存入会话,供后续路由使用 req.session.businessId = businessId; req.session.userId = user._id; res.json({ msg: '登录成功', user }); });
4. 后续路由的连接复用
所有需要操作业务库的路由,先从会话获取businessId,再从连接池拿到对应连接,用该连接的模型操作数据:
app.get('/orders', async (req, res) => { const { businessId, userId } = req.session; if (!businessId) return res.status(401).json({ msg: '未登录' }); // 从连接池获取业务连接 const businessConnection = connectionPool[businessId]; if (!businessConnection) return res.status(500).json({ msg: '租户连接失效' }); // 更新最后使用时间 businessConnection.lastUsed = Date.now(); const Order = businessConnection.model('Order'); const orders = await Order.find({ userId }); res.json(orders); });
5. 登出与连接校验
登出时只需清除会话中的businessId,不需要立即销毁连接(避免频繁创建销毁连接影响性能)。如果需要优化资源,可以定期清理闲置连接:
app.post('/logout', (req, res) => { // 清除会话中的租户和用户标识 delete req.session.businessId; delete req.session.userId; res.json({ msg: '登出成功' }); }); // 可选:定时清理闲置连接(比如超过24小时未使用) setInterval(() => { const now = Date.now(); for (const [businessId, conn] of Object.entries(connectionPool)) { if (now - conn.lastUsed > 86400000) { conn.close(); delete connectionPool[businessId]; } } }, 3600000); // 每小时检查一次
关键注意事项
- 永远不要混用
mongoose.connect()和createConnection()的全局/实例模型,所有模型操作都要明确绑定到对应的连接实例上。 - 给连接池添加错误监听:如果某个租户连接断开,及时从连接池移除,避免后续请求报错。
- 主库连接要单独维护,不要和业务库连接混在一起,避免主库操作受业务库连接波动影响。
内容的提问来源于stack exchange,提问作者diegodlfp
相关产品推荐
相关产品推荐

