多租户后端管理系统设计方案
一、设计概述
多租户系统是一种软件架构模式,允许多个租户(客户)共享同一套应用程序实例,同时保证数据隔离和安全性。以下是一套完整的多租户后端管理系统设计方案。
二、隔离方案选型
基于不同业务场景和需求,多租户系统有三种主要隔离方案:
| 隔离方案 | 描述 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| 独立数据库 | 每个租户独立数据库实例 | 隔离级别最高,安全性好,数据恢复简单 | 成本高,维护复杂 | 金融、医疗等高安全要求行业 |
| 共享数据库独立Schema | 共享数据库但独立Schema | 隔离性中等,支持较多租户 | 故障恢复复杂 | 对安全有一定要求的中型应用 |
| 共享数据库共享模式 | 共享数据库和表结构,通过tenant_id区分 | 成本低,支持租户数量最多 | 隔离性最弱,性能可能受影响 | 初创企业,对成本敏感的应用 |
推荐隔离方案
本设计采用混合隔离策略:
- 普通租户:共享数据库共享模式
- 高价值/高安全要求租户:独立数据库或独立Schema
三、系统架构设计
3.1 整体架构
客户端层 → API网关(租户路由) → 服务层 → 数据访问层 → 多租户数据源
3.2 核心组件
-
租户识别模块
- 基于域名/子域名识别
- 基于请求头识别
- 基于API密钥识别
-
租户上下文
- 存储当前操作租户信息
- 线程本地变量传播
-
数据访问层增强
- 自动添加租户过滤条件
- 动态数据源路由
-
资源隔离与配额管理
- API调用频率限制
- 数据存储配额控制
四、数据库表结构设计
4.1 租户管理模块
-- 租户表
table tenant (
id bigint primary key auto_increment comment '租户ID',
tenant_name varchar(100) not null unique comment '租户名称',
tenant_code varchar(50) not null unique comment '租户编码',
subdomain varchar(100) unique comment '租户子域名',
contact_person varchar(50) comment '联系人',
contact_email varchar(100) comment '联系邮箱',
contact_phone varchar(20) comment '联系电话',
status tinyint default 1 comment '状态:0-禁用,1-启用',
isolation_type tinyint not null comment '隔离类型:1-共享模式,2-独立Schema,3-独立数据库',
db_config json comment '数据库配置(JSON格式)',
created_at datetime not null default current_timestamp,
updated_at datetime not null default current_timestamp on update current_timestamp,
deleted_at datetime comment '删除时间'
);
-- 租户配置表
table tenant_config (
id bigint primary key auto_increment,
tenant_id bigint not null comment '租户ID',
config_key varchar(100) not null comment '配置键',
config_value text comment '配置值',
config_desc varchar(200) comment '配置描述',
created_at datetime not null default current_timestamp,
updated_at datetime not null default current_timestamp on update current_timestamp,
foreign key (tenant_id) references tenant(id) on delete cascade,
unique key uk_tenant_config (tenant_id, config_key)
);
-- 租户配额表
table tenant_quota (
id bigint primary key auto_increment,
tenant_id bigint not null comment '租户ID',
max_users int default 100 comment '最大用户数',
max_api_calls int default 10000 comment '每日API调用上限',
storage_quota_mb int default 1024 comment '存储配额(MB)',
created_at datetime not null default current_timestamp,
updated_at datetime not null default current_timestamp on update current_timestamp,
foreign key (tenant_id) references tenant(id) on delete cascade,
unique key uk_tenant (tenant_id)
);
4.2 用户管理模块
-- 用户表(多租户设计)
table user (
id bigint primary key auto_increment,
tenant_id bigint not null comment '租户ID',
username varchar(50) not null comment '用户名',
password varchar(100) not null comment '密码(加密)',
real_name varchar(50) comment '真实姓名',
email varchar(100) comment '邮箱',
phone varchar(20) comment '手机号',
status tinyint default 1 comment '状态:0-禁用,1-启用',
created_at datetime not null default current_timestamp,
updated_at datetime not null default current_timestamp on update current_timestamp,
deleted_at datetime comment '删除时间',
unique key uk_tenant_username (tenant_id, username),
key idx_tenant (tenant_id)
);
-- 角色表(多租户设计)
table role (
id bigint primary key auto_increment,
tenant_id bigint not null comment '租户ID',
role_name varchar(50) not null comment '角色名称',
role_code varchar(50) not null comment '角色编码',
description varchar(200) comment '角色描述',
created_at datetime not null default current_timestamp,
updated_at datetime not null default current_timestamp on update current_timestamp,
unique key uk_tenant_role (tenant_id, role_code),
key idx_tenant (tenant_id)
);
-- 用户角色关联表
table user_role (
id bigint primary key auto_increment,
tenant_id bigint not null comment '租户ID',
user_id bigint not null comment '用户ID',
role_id bigint not null comment '角色ID',
created_at datetime not null default current_timestamp,
unique key uk_user_role (tenant_id, user_id, role_id),
foreign key (user_id) references user(id) on delete cascade,
foreign key (role_id) references role(id) on delete cascade,
key idx_tenant (tenant_id)
);
4.3 权限管理模块
-- 权限表
table permission (
id bigint primary key auto_increment,
tenant_id bigint comment '租户ID,null表示系统权限',
perm_code varchar(50) not null comment '权限编码',
perm_name varchar(100) not null comment '权限名称',
perm_type tinyint not null comment '权限类型:1-菜单,2-按钮,3-API',
parent_id bigint default 0 comment '父权限ID',
resource_path varchar(200) comment '资源路径',
description varchar(200) comment '权限描述',
created_at datetime not null default current_timestamp,
updated_at datetime not null default current_timestamp on update current_timestamp,
unique key uk_perm_code (tenant_id, perm_code),
key idx_tenant (tenant_id)
);
-- 角色权限关联表
table role_permission (
id bigint primary key auto_increment,
tenant_id bigint not null comment '租户ID',
role_id bigint not null comment '角色ID',
permission_id bigint not null comment '权限ID',
created_at datetime not null default current_timestamp,
unique key uk_role_perm (tenant_id, role_id, permission_id),
foreign key (role_id) references role(id) on delete cascade,
foreign key (permission_id) references permission(id) on delete cascade,
key idx_tenant (tenant_id)
);
4.4 系统管理模块
-- 系统配置表
table system_config (
id bigint primary key auto_increment,
config_key varchar(100) not null unique comment '配置键',
config_value text comment '配置值',
config_desc varchar(200) comment '配置描述',
created_at datetime not null default current_timestamp,
updated_at datetime not null default current_timestamp on update current_timestamp
);
-- 操作日志表(多租户设计)
table operation_log (
id bigint primary key auto_increment,
tenant_id bigint comment '租户ID',
user_id bigint comment '操作用户ID',
username varchar(50) comment '操作用户名',
operation_type varchar(50) comment '操作类型',
operation_desc varchar(200) comment '操作描述',
request_params text comment '请求参数',
response_result text comment '响应结果',
ip_address varchar(50) comment 'IP地址',
user_agent varchar(200) comment '用户代理',
execution_time int comment '执行时间(毫秒)',
success tinyint default 1 comment '是否成功:0-失败,1-成功',
error_msg text comment '错误信息',
created_at datetime not null default current_timestamp,
key idx_tenant_user (tenant_id, user_id),
key idx_created_at (created_at)
);
-- 登录日志表(多租户设计)
table login_log (
id bigint primary key auto_increment,
tenant_id bigint comment '租户ID',
user_id bigint comment '用户ID',
username varchar(50) comment '用户名',
login_type varchar(50) comment '登录类型',
ip_address varchar(50) comment 'IP地址',
user_agent varchar(200) comment '用户代理',
success tinyint default 1 comment '是否成功:0-失败,1-成功',
error_msg varchar(200) comment '错误信息',
created_at datetime not null default current_timestamp,
key idx_tenant_user (tenant_id, user_id),
key idx_created_at (created_at)
);
五、多租户核心实现
5.1 租户识别与上下文
// 租户上下文类
public class TenantContext {
private static final ThreadLocal<Long> CURRENT_TENANT = new ThreadLocal<>();
private static final ThreadLocal<String> CURRENT_TENANT_CODE = new ThreadLocal<>();
public static void setTenantId(Long tenantId) {
CURRENT_TENANT.set(tenantId);
}
public static Long getTenantId() {
return CURRENT_TENANT.get();
}
public static void setTenantCode(String tenantCode) {
CURRENT_TENANT_CODE.set(tenantCode);
}
public static String getTenantCode() {
return CURRENT_TENANT_CODE.get();
}
public static void clear() {
CURRENT_TENANT.remove();
CURRENT_TENANT_CODE.remove();
}
}
// 租户拦截器
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从请求头获取租户信息
String tenantCode = request.getHeader("X-Tenant-Code");
if (StringUtils.isEmpty(tenantCode)) {
// 尝试从子域名获取
tenantCode = extractTenantFromDomain(request.getServerName());
}
if (!StringUtils.isEmpty(tenantCode)) {
// 查找租户信息
Tenant tenant = tenantService.findByCode(tenantCode);
if (tenant != null && tenant.isEnabled()) {
TenantContext.setTenantId(tenant.getId());
TenantContext.setTenantCode(tenant.getCode());
return true;
}
}
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 清理租户上下文
TenantContext.clear();
}
}
5.2 数据访问增强
// MyBatis拦截器实现租户过滤
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class TenantInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Long tenantId = TenantContext.getTenantId();
if (tenantId == null) {
return invocation.proceed();
}
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
// 判断是否需要租户过滤
if (needTenantFilter(ms)) {
// 获取SQL命令类型
SqlCommandType sqlCommandType = ms.getSqlCommandType();
if (sqlCommandType == SqlCommandType.SELECT) {
// 修改查询SQL,添加租户过滤条件
args[0] = processSelectStatement(ms, tenantId);
} else if (sqlCommandType == SqlCommandType.INSERT) {
// 为插入操作添加租户ID
args[1] = processInsertStatement(args[1], tenantId);
} else if (sqlCommandType == SqlCommandType.UPDATE ||
sqlCommandType == SqlCommandType.DELETE) {
// 修改更新/删除SQL,添加租户过滤条件
args[0] = processUpdateOrDeleteStatement(ms, tenantId);
}
}
return invocation.proceed();
}
// 其他辅助方法...
}
5.3 动态数据源
// 动态数据源路由
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
Long tenantId = TenantContext.getTenantId();
if (tenantId == null) {
return "defaultDataSource";
}
// 获取租户数据源配置
Tenant tenant = tenantService.getById(tenantId);
if (tenant != null) {
// 根据租户隔离类型选择数据源
switch (tenant.getIsolationType()) {
case 1: // 共享模式,使用默认数据源
return "defaultDataSource";
case 2: // 独立Schema
return "schema_" + tenantId;
case 3: // 独立数据库
return "db_" + tenantId;
}
}
return "defaultDataSource";
}
}
六、关键业务流程示例
6.1 租户创建流程
-- 1. 创建租户
INSERT INTO tenant (tenant_name, tenant_code, subdomain, contact_person,
contact_email, contact_phone, isolation_type)
VALUES ('测试租户', 'test_tenant', 'test', '张三', 'zhangsan@example.com',
'13800138000', 1);
-- 2. 设置租户配额
INSERT INTO tenant_quota (tenant_id, max_users, max_api_calls, storage_quota_mb)
VALUES (0, 50, 5000, 512);
-- 3. 创建租户管理员
INSERT INTO user (tenant_id, username, password, real_name, email, phone, status)
VALUES (0, 'admin', '加密后的密码', '管理员', 'admin@example.com',
'13800138000', 1);
-- 4. 创建租户管理员角色
INSERT INTO role (tenant_id, role_name, role_code, description)
VALUES ((SELECT tenant_id FROM user WHERE id = 0), '系统管理员',
'ADMIN', '租户系统管理员角色');
-- 5. 关联用户和角色
INSERT INTO user_role (tenant_id, user_id, role_id)
VALUES ((SELECT tenant_id FROM user WHERE id = 0 - 1),
0 - 1, 0);
6.2 租户数据查询示例
-- 查询特定租户的所有用户
SELECT * FROM user WHERE tenant_id = 1 AND deleted_at IS NULL;
-- 查询特定租户的角色权限
SELECT r.role_name, p.perm_name, p.perm_code
FROM role r
JOIN role_permission rp ON r.id = rp.role_id
JOIN permission p ON rp.permission_id = p.id
WHERE r.tenant_id = 1;
-- 统计租户资源使用情况
SELECT
(SELECT COUNT(*) FROM user WHERE tenant_id = 1) AS user_count,
(SELECT SUM(DATA_LENGTH + INDEX_LENGTH)/1024/1024 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_database') AS storage_used_mb,
(SELECT COUNT(*) FROM operation_log WHERE tenant_id = 1
AND DATE(created_at) = CURRENT_DATE) AS today_api_calls;
七、性能优化策略
- 索引优化:为所有表的tenant_id字段创建索引
- 缓存策略:使用Redis缓存租户配置信息和热点数据
- 读写分离:对大型租户实施读写分离
- 数据库分库分表:当租户数量或数据量增长到一定规模时,考虑分库分表
- 连接池优化:为不同租户设置独立连接池
八、扩展建议
- 租户自助服务门户:提供租户自助注册、配置管理功能
- 多租户监控系统:监控各租户资源使用情况和性能指标
- 计费系统:基于租户资源使用情况实现计费功能
- 租户数据备份恢复:支持按租户备份和恢复数据
- 多语言支持:支持不同租户使用不同语言
通过上述设计,系统可以灵活支持不同规模和安全需求的租户,同时保持良好的性能和扩展性。