- 发布时间
使用mongoose实现多租户应用系统
- 作者

- 作者名字
- Kavin Wang
本文为一个翻译的内容,原文参考本文末。
多租户是指软件的操作架构和部署模式,其中一个软件的多个运行实例,或者多个应用程序共享同一个运行环境。这些租户或者实例,它们在逻辑上是互相隔离的,而在物理上是放在一起的。
软件多租户:是指一种软件架构,代表运行在服务器上的一套软件,同时为多个租户提供服务。

构建多租户系统是复杂的,其复杂度远远超过多用户系统的构建。
构建多租户系统有两种模型:实例复制和数据隔离。
在实例复制模型中,系统为每个租户创建一个新实例。这种做法很容易实现,但很难扩展。比如一旦有上百名租户同时登录,做起来简直是一场噩梦。
在数据隔离模型中,租户之间共享同一套应用程序,但每个租户的数据存储在单独的存储空间中。单独的存储空间可能是单独的数据库,也可能是同一数据库中的不同的数据schema。

数据隔离有两种类型:
- 同一个数据库中,为不同的租户提供不同的schema。
- 为每个不同的租户,都提供一个独立的数据库。
独立的schema,就是每个租户在一个数据库中有不同schema的同样的套表。

本文中,我们将讨论数据隔离的多数据库模式,就是为每个租户提供一个独立的数据库。
下面的代码创建一个mongodb的连接,并导出这个连接,以备将来的代码使用。
const mongoose = require('mongoose');
const mongoOptions = {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true,
useFindAndModify: false,
autoIndex: true,
poolSize: 10,
bufferMaxEntries: 0,
connectTimeoutMS: 10000,
socketTimeoutMS: 30000,
};
const connect = () => mongoose.createConnection(process.env.MONGODB_URL, mongoOptions);
const connectToMongoDB = () => {
const db = connect(process.env.MONGODB_URL);
db.on('open', () => {
console.info(`Mongoose connection open to ${JSON.stringify(process.env.MONGODB_URL)}`);
});
db.on('error', (err) => {
console.info(`Mongoose connection error: ${err} with connection info ${JSON.stringify(process.env.MONGODB_URL)}`);
process.exit(0);
});
return db;
};
exports.mongodb = (connectToMongoDB)();
为每一个租户创建一个mongodb连接:
const { mongodb } = require('./mongodb');
/**
* Creating New MongoDb Connection obect by Switching DB
*/
const getTenantDB = (tenantId, modelName, schema) => {
const dbName = `tenant_${tenantId}`;
if (mongodb) {
// useDb will return new connection
const db = mongodb.useDb(dbName, { useCache: true });
console.info(`DB switched to ${dbName}`);
db.model(modelName, schema);
return db;
}
throw new Error("something error!")
};
/**
* Return Model as per tenant
*/
exports.getModelByTenant = (tenantId, modelName, schema) => {
console.info(`getModelByTenant tenantId : ${tenantId}.`);
const tenantDb = getTenantDB(tenantId, modelName, schema);
return tenantDb.model(modelName);
};
Mongoose提供了useDb函数,用于在一个已经存在的连接对象上创建一个新的连接对象,它有两个参数,第一个是DB名称,另一个是选项,参考文档是这样描述的:
Connection.prototype.useDb()Parameters:
name «String» The database name
{options} «Object» Configsoptions = {useCache: false} «Boolean» False by defaultIf set true, cache results so calling useDb() multiple times with the same name only creates 1 connection object.Returns:
«Connection» New Connection Object
我们使用同一个连接池的连接,来创建一个新的连接对象,这个对象会使用新的数据库名称,并使用{useCache: true}这个选项,为同一个租户创建一个单独的连接,同一个租户后面的多次数据库调用,都会使用这个连接。每次提供不同的租户,都会创建一个独立的针对这个租户的数据库连接。
如果要取得租户数据库的表模型,我们得为相应的表创建一个模型schema。然后就可以使用这个模型来进行数据库操作了。
const mongoose = require('mongoose');
const signatureSchema = new mongoose.Schema({
nodeId: {
type: mongoose.Schema.Types.ObjectId,
required: true,
},
requestBy: {
type: String,
required: true,
},
requestTime: {
type: Date,
default: Date.now,
required: true,
},
isActive: {
type: Boolean,
default: true,
required: true,
},
labId: {
type: String,
required: true,
},
comment: String,
});
module.exports = signatureSchema;
在数据存取层面,我们要先取得租户的数据库连接,然后进一步取得表的模型,然后用这个模型进行CRUD操作。所以,每次调用,都必须提供tenantId,这个tenantId在底层来讲,就是为了切换要操作使用的数据库。
const { getModelByTenant } = require('.//multitenancy');
const { signatureSchema } = require('./signatureModal');
exports.addSignature = async (signaturesBody, tenantId) => {
log.info(`Add signature called with body ${signaturesBody}`);
const Signature = getModelByTenant(tenantId, 'signature', signatureSchema);
// Execute takes a promise and return {null,response} if resolved and return {err,null} if rejected.
const { err, response } = await execute(Signature.create(signaturesBody));
if (err || !response) {
log.error(`Signature creation failed signatureDao.js ${err.message}`);
throwError(500, codes.signatureAddFailed);
}
log.info(`Signature created with response : ${response}`);
return response;
};
在这种方法中,我们使用的是在应用程序初始化时创建的相同连接池。具有不同租户的多个请求将创建不同的连接对象,如果没有来自同一租户的新请求,这些连接对象将在30秒后超时。
我们还可以实施另一种方法,这将对有限的租户有所帮助。在此,我们可以创建一个全局对象,其中键为tenantId/tenantName,值为连接对象。在每个请求之前,我们将检查此全局对象中是否存在连接,否则我们将创建一个连接并将其存储在那里,以便在下一个来自该租户的请求中重用。
const { Mongoose } = require('mongoose');
const mongoConfigs = require('../../../config/mongo.conf');
const multitenantPool = {};
const getTenantDB = function getConnections(tenantId, modelName, schema) {
// Check connections lookup
const mCon = multitenantPool[tenantId];
if (mCon) {
if (!mCon.modelSchemas[modelName]) {
mCon.model(modelName, schema);
}
return mCon;
}
const mongoose = new Mongoose();
const url = process.env.MONGODB_URL.replace(/brightlab/, `brightlab_${tenantId}`);
mongoose.connect(url, mongoConfigs);
multitenantPool[tenantId] = mongoose;
mongoose.model(modelName, schema);
mongoose.connection.on('error', err => console.debug(err));
mongoose.connection.once('open', () => console.info(`mongodb connected to ${url}`));
return mongoose;
};
exports.getModelByTenant = (tenantId, modelName, schema) => {
console.info(`getModelByTenant tenantId : ${tenantId}.`);
const tenantDb = getTenantDB(tenantId, modelName, schema);
return tenantDb.model(modelName);
};
在上述实现中,如果租户数量增加,可能会出现CPU使用率问题,因为应用程序将始终有大量打开的连接。这将降低性能,但它可以在租户较少的情况下使用,并且永远不会动态增长。
翻译不准确处,请参考 原文