进销存系统多租户架构设计与数据隔离方案
引言
SaaS 模式的进销存系统需要支撑多个企业(租户)同时使用,每个租户的数据必须严格隔离,同时要保障系统资源的高效利用。本文将介绍如何设计一套完整的多租户架构,实现数据隔离、资源管理和成本优化。
多租户架构模式
常见的多租户架构模式对比:
| 模式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 独立数据库 | 数据隔离严格,安全性高 | 资源成本高,维护复杂 | 大型企业客户 |
| 独立 Schema | 隔离性较好,成本适中 | 数据库连接数限制 | 中型企业客户 |
| 共享数据库 | 成本最低,运维简单 | 隔离性较弱,需应用层加固 | 小型客户 |
核心代码实现
多租户数据访问层实现:
// 多租户上下文管理器
class TenantContext {
constructor() {
this.currentTenant = null;
this.tenantCache = new Map();
}
// 设置当前租户
setCurrentTenant(tenantId) {
this.currentTenant = tenantId;
// 设置到请求上下文
async_context.set('tenantId', tenantId);
}
// 获取当前租户
getCurrentTenant() {
if (this.currentTenant) {
return this.currentTenant;
}
return async_context.get('tenantId');
}
// 清除租户上下文
clear() {
this.currentTenant = null;
async_context.delete('tenantId');
}
}
// 租户解析器
class TenantResolver {
constructor(config) {
this.config = config;
this.resolvers = [
this.resolveFromHeader.bind(this),
this.resolveFromCookie.bind(this),
this.resolveFromSubdomain.bind(this),
this.resolveFromPath.bind(this)
];
}
// 从请求头解析租户
resolveFromHeader(request) {
const tenantId = request.headers['x-tenant-id'];
if (tenantId && this.validateTenant(tenantId)) {
return tenantId;
}
return null;
}
// 从 Cookie 解析租户
resolveFromCookie(request) {
const tenantId = request.cookies['tenant_id'];
if (tenantId && this.validateTenant(tenantId)) {
return tenantId;
}
return null;
}
// 从子域名解析租户
resolveFromSubdomain(request) {
const host = request.headers.host;
const match = host.match(/^(\w+)\./);
if (match) {
const subdomain = match[1];
if (subdomain !== 'www' && subdomain !== 'api') {
return this.lookupTenantBySubdomain(subdomain);
}
}
return null;
}
// 从路径解析租户
resolveFromPath(request) {
const match = request.path.match(/^\/tenant\/([^\/]+)/);
if (match) {
return match[1];
}
return null;
}
// 解析租户
async resolve(request) {
for (const resolver of this.resolvers) {
const tenantId = await resolver(request);
if (tenantId) {
return tenantId;
}
}
// 返回默认租户
return this.config.defaultTenant;
}
// 验证租户
async validateTenant(tenantId) {
const tenant = await this.getTenant(tenantId);
return tenant && tenant.status === 'active';
}
// 获取租户信息
async getTenant(tenantId) {
return await TenantCache.get(tenantId);
}
}
// 数据路由策略
class DataRouter {
constructor(tenantContext, config) {
this.tenantContext = tenantContext;
this.config = config;
}
// 获取数据源配置
getDataSourceConfig(tenantId) {
const tenant = this.tenantContext.getTenant(tenantId);
switch (this.config.isolationMode) {
case 'database':
return {
host: `${tenantId}.database.host`,
database: tenantId,
username: `${tenantId}_user`,
password: tenant.dbPassword
};
case 'schema':
return {
host: this.config.sharedDatabase.host,
database: this.config.sharedDatabase.name,
username: `${tenantId}_user`,
password: tenant.dbPassword,
schema: tenantId
};
case 'shared':
return {
host: this.config.sharedDatabase.host,
database: this.config.sharedDatabase.name,
username: this.config.sharedDatabase.username,
password: this.config.sharedDatabase.password,
options: {
tenantId: tenantId
}
};
default:
throw new Error('Unknown isolation mode');
}
}
// 多数据源管理
async getDataSource(tenantId) {
const cacheKey = `datasource_${tenantId}`;
let dataSource = this.dataSourceCache.get(cacheKey);
if (!dataSource) {
const config = this.getDataSourceConfig(tenantId);
dataSource = await this.createDataSource(config);
this.dataSourceCache.set(cacheKey, dataSource);
}
return dataSource;
}
}
// SQL 拦截器 - 添加租户过滤
class TenantSQLInterceptor {
constructor(tenantContext) {
this.tenantContext = tenantContext;
}
// 拦截 SQL 添加租户条件
intercept(sql, params) {
const tenantId = this.tenantContext.getCurrentTenant();
if (!tenantId) {
throw new Error('Tenant context not set');
}
// 分析 SQL 类型
const sqlType = this.getSQLType(sql);
if (sqlType === 'SELECT') {
return this.addTenantCondition(sql, params, 'tenant_id', tenantId);
} else if (sqlType === 'INSERT') {
return this.addTenantField(sql, params, 'tenant_id', tenantId);
} else if (sqlType === 'UPDATE' || sqlType === 'DELETE') {
return this.addTenantCondition(sql, params, 'tenant_id', tenantId);
}
return { sql, params };
}
// 添加租户条件
addTenantCondition(sql, params, tenantField, tenantId) {
// 简单处理:追加 WHERE 条件
if (sql.toLowerCase().includes('where')) {
sql = `${sql} AND ${tenantField} = ?`;
} else {
sql = `${sql} WHERE ${tenantField} = ?`;
}
params.push(tenantId);
return { sql, params };
}
// 添加租户字段
addTenantField(sql, params, tenantField, tenantId) {
// INSERT INTO table (...) VALUES (...)
const match = sql.match(/INSERT\s+INTO\s+(\w+)\s*\(([^)]+)\)\s*VALUES/i);
if (match) {
const fields = match[2].split(',').map(f => f.trim());
fields.push(tenantField);
const newFields = fields.join(', ');
const valuesPlaceholder = params.map(() => '?').join(', ');
sql = sql.replace(
`(${match[2]})`,
`(${newFields})`
).replace(
`VALUES (${valuesPlaceholder})`,
`VALUES (${valuesPlaceholder}, ?)`
);
params.push(tenantId);
}
return { sql, params };
}
}
// 租户资源配额管理
class TenantQuotaManager {
constructor() {
this.quotas = new Map();
this.usage = new Map();
}
// 初始化租户配额
initQuota(tenantId, quota) {
this.quotas.set(tenantId, {
storage: quota.storage || 1024 * 1024 * 1024, // 默认 1GB
apiCalls: quota.apiCalls || 10000,
users: quota.users || 10,
warehouses: quota.warehouses || 3,
products: quota.products || 1000
});
this.usage.set(tenantId, {
storage: 0,
apiCalls: 0,
users: 0,
warehouses: 0,
products: 0
});
}
// 检查配额
checkQuota(tenantId, resource, amount = 1) {
const quota = this.quotas.get(tenantId);
const usage = this.usage.get(tenantId);
if (!quota || !usage) {
return { allowed: false, reason: 'Tenant not found' };
}
const currentUsage = usage[resource] || 0;
const limit = quota[resource];
return {
allowed: currentUsage + amount <= limit,
current: currentUsage,
limit: limit,
remaining: limit - currentUsage
};
}
// 消耗配额
async consumeQuota(tenantId, resource, amount = 1) {
const check = this.checkQuota(tenantId, resource, amount);
if (!check.allowed) {
throw new Error(`Quota exceeded for ${resource}`);
}
const usage = this.usage.get(tenantId);
usage[resource] = (usage[resource] || 0) + amount;
return check;
}
// 释放配额
releaseQuota(tenantId, resource, amount = 1) {
const usage = this.usage.get(tenantId);
if (usage) {
usage[resource] = Math.max(0, usage[resource] - amount);
}
}
// 获取配额使用报告
getQuotaReport(tenantId) {
const quota = this.quotas.get(tenantId);
const usage = this.usage.get(tenantId);
if (!quota || !usage) {
return null;
}
const report = {};
for (const [resource, limit] of Object.entries(quota)) {
const used = usage[resource] || 0;
report[resource] = {
used,
limit,
percentage: ((used / limit) * 100).toFixed(2) + '%',
remaining: limit - used
};
}
return report;
}
}
资源隔离与安全
租户资源隔离配置:
// 租户认证与授权
class TenantAuthService {
constructor(tenantContext) {
this.tenantContext = tenantContext;
}
// 租户登录
async login(tenantDomain, username, password) {
const tenant = await this.getTenantByDomain(tenantDomain);
if (!tenant) {
throw new Error('Tenant not found');
}
// 验证用户
const user = await this.verifyUser(tenant.id, username, password);
if (!user) {
throw new Error('Invalid credentials');
}
// 生成令牌
const token = await this.generateToken({
tenantId: tenant.id,
userId: user.id,
roles: user.roles
});
return { token, tenant, user };
}
// 验证用户
async verifyUser(tenantId, username, password) {
const user = await this.db.query(
`SELECT * FROM tenant_${tenantId}.users
WHERE username = ? AND password = ?`,
[username, this.hashPassword(password)]
);
return user[0];
}
// 生成令牌
async generateToken(payload) {
return jwt.sign(payload, this.secret, {
expiresIn: '24h'
});
}
// 中间件:验证租户权限
middleware() {
return async (ctx, next) => {
const token = ctx.headers.authorization?.replace('Bearer ', '');
if (!token) {
ctx.status = 401;
ctx.body = { error: 'No token provided' };
return;
}
try {
const decoded = jwt.verify(token, this.secret);
this.tenantContext.setCurrentTenant(decoded.tenantId);
ctx.user = decoded;
await next();
} catch (error) {
ctx.status = 401;
ctx.body = { error: 'Invalid token' };
}
};
}
}
// 数据加密服务
class TenantEncryptionService {
constructor() {
this.algorithms = {
'aes-256-gcm': { keySize: 32, ivSize: 16 },
'aes-128-gcm': { keySize: 16, ivSize: 16 }
};
}
// 生成租户密钥
generateTenantKey(tenantId) {
const config = this.algorithms['aes-256-gcm'];
const key = crypto.randomBytes(config.keySize);
this.storeKey(tenantId, key);
return key;
}
// 加密敏感数据
encrypt(tenantId, data) {
const key = this.getKey(tenantId);
const config = this.algorithms['aes-256-gcm'];
const iv = crypto.randomBytes(config.ivSize);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const encrypted = Buffer.concat([
cipher.update(JSON.stringify(data), 'utf8'),
cipher.final()
]);
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('base64'),
data: encrypted.toString('base64'),
authTag: authTag.toString('base64')
};
}
// 解密敏感数据
decrypt(tenantId, encryptedData) {
const key = this.getKey(tenantId);
const iv = Buffer.from(encryptedData.iv, 'base64');
const encrypted = Buffer.from(encryptedData.data, 'base64');
const authTag = Buffer.from(encryptedData.authTag, 'base64');
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
]);
return JSON.parse(decrypted.toString('utf8'));
}
}
// 审计日志
class TenantAuditLogger {
constructor(tenantContext) {
this.tenantContext = tenantContext;
}
// 记录操作日志
async log(action, resource, details = {}) {
const tenantId = this.tenantContext.getCurrentTenant();
const userId = details.userId || 'system';
const log = {
tenantId,
userId,
action,
resource,
details,
timestamp: new Date(),
ip: details.ip || 'unknown',
userAgent: details.userAgent || 'unknown'
};
// 异步写入日志
await this.writeLog(log);
}
// 查询租户日志
async queryLogs(tenantId, filters = {}) {
const query = {
tenantId,
startDate: filters.startDate,
endDate: filters.endDate,
action: filters.action,
userId: filters.userId,
limit: filters.limit || 100
};
return await this.db.queryLogs(query);
}
}
最佳实践建议
- 选择合适的隔离模式:根据客户规模和安全性要求选择数据库隔离级别
- 资源配额管理:为不同租户设置合理的资源配额,防止资源争抢
- 数据加密:敏感数据加密存储,租户密钥分离管理
- 审计日志:完整记录所有租户操作,便于问题追溯
- 成本优化:共享资源时做好隔离,避免单租户影响全局
总结
多租户架构是 SaaS 进销存系统的核心技术:
- 资源隔离:确保租户数据安全,满足合规要求
- 成本效率:通过资源共享降低运维成本
- 可扩展性:架构支持租户数量线性扩展
- 可维护性:统一管理简化运维工作
实际落地需要根据业务规模和发展阶段选择合适的架构方案。