多租户后端管理系统设计方案

131 阅读9分钟

多租户后端管理系统设计方案

一、设计概述

多租户系统是一种软件架构模式,允许多个租户(客户)共享同一套应用程序实例,同时保证数据隔离和安全性。以下是一套完整的多租户后端管理系统设计方案。

二、隔离方案选型

基于不同业务场景和需求,多租户系统有三种主要隔离方案:

隔离方案描述优势劣势适用场景
独立数据库每个租户独立数据库实例隔离级别最高,安全性好,数据恢复简单成本高,维护复杂金融、医疗等高安全要求行业
共享数据库独立Schema共享数据库但独立Schema隔离性中等,支持较多租户故障恢复复杂对安全有一定要求的中型应用
共享数据库共享模式共享数据库和表结构,通过tenant_id区分成本低,支持租户数量最多隔离性最弱,性能可能受影响初创企业,对成本敏感的应用

推荐隔离方案

本设计采用混合隔离策略

  • 普通租户:共享数据库共享模式
  • 高价值/高安全要求租户:独立数据库或独立Schema

三、系统架构设计

3.1 整体架构

客户端层 → API网关(租户路由) → 服务层 → 数据访问层 → 多租户数据源

3.2 核心组件

  1. 租户识别模块

    • 基于域名/子域名识别
    • 基于请求头识别
    • 基于API密钥识别
  2. 租户上下文

    • 存储当前操作租户信息
    • 线程本地变量传播
  3. 数据访问层增强

    • 自动添加租户过滤条件
    • 动态数据源路由
  4. 资源隔离与配额管理

    • 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;

七、性能优化策略

  1. 索引优化:为所有表的tenant_id字段创建索引
  2. 缓存策略:使用Redis缓存租户配置信息和热点数据
  3. 读写分离:对大型租户实施读写分离
  4. 数据库分库分表:当租户数量或数据量增长到一定规模时,考虑分库分表
  5. 连接池优化:为不同租户设置独立连接池

八、扩展建议

  1. 租户自助服务门户:提供租户自助注册、配置管理功能
  2. 多租户监控系统:监控各租户资源使用情况和性能指标
  3. 计费系统:基于租户资源使用情况实现计费功能
  4. 租户数据备份恢复:支持按租户备份和恢复数据
  5. 多语言支持:支持不同租户使用不同语言

通过上述设计,系统可以灵活支持不同规模和安全需求的租户,同时保持良好的性能和扩展性。