1. 多租户模式选择
yudao 支持以下多租户模式,需根据业务需求选择:
1.1 共享数据库 + 共享表(Schema per Tenant)
- 描述:所有租户共享同一数据库,通过
tenant_id字段隔离数据。 - 适用场景:中小型系统,租户数量较少,数据量可控。
- 实现方式:
- 业务表添加
tenant_id字段。 - SQL 自动追加
WHERE tenant_id = ?过滤条件。
- 业务表添加
1.2 独立数据库(Database per Tenant)
- 描述:每个租户拥有独立数据库,物理隔离。
- 适用场景:大型企业客户,对数据隔离性要求高。
- 实现方式:
- 动态数据源切换(基于租户标识路由到不同数据库)。
- 需管理多数据源连接池。
1.3 混合模式
- 核心数据独立库 + 非核心数据共享库。
- 需结合业务模块灵活设计。
2. 数据隔离核心实现
2.1 SQL 自动过滤
- MyBatis 拦截器:自动注入
tenant_id条件。public class TenantInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) { // 动态添加 tenant_id 条件 MetaObject metaObject = SystemMetaObject.forObject(invocation.getArgs()[0]); if (metaObject.hasGetter("tenantId")) { metaObject.setValue("tenantId", getCurrentTenantId()); } return invocation.proceed(); } }
2.2 手动过滤场景
- Service 层显式传递:
public List<User> listUsers() { return userMapper.selectList(Wrappers.lambdaQuery(User.class) .eq(User::getTenantId, TenantContext.getCurrentTenantId())); }
3. 租户标识传递与存储
3.1 标识传递方式
- HTTP 请求头:
X-Tenant-Id: 1001 - JWT Token:Payload 中嵌入租户 ID。
- 子域名:
tenant1.example.com解析租户 ID。
3.2 上下文存储
- ThreadLocal 存储:
public class TenantContext { private static final ThreadLocal<Long> CURRENT_TENANT = new ThreadLocal<>(); public static void setTenantId(Long tenantId) { CURRENT_TENANT.set(tenantId); } public static Long getCurrentTenantId() { return CURRENT_TENANT.get(); } }
4. 数据库设计与数据源管理
4.1 表结构设计
- 必加字段:所有租户隔离表需包含
tenant_id(BIGINT 类型)。 - 公共表:如字典表、配置表无需
tenant_id。
4.2 动态数据源(独立数据库模式)
- AbstractRoutingDataSource 实现动态路由:
public class TenantDataSourceRouter extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return TenantContext.getCurrentTenantId(); } } - 配置示例:
spring: datasource: dynamic: primary: master datasource: master: # 默认数据源 tenant_1001: # 租户 1001 的数据源 tenant_1002: # 租户 1002 的数据源
5. 缓存与中间件隔离
5.1 Redis 缓存
- Key 设计:包含租户 ID。
String key = "user:info:" + tenantId + ":" + userId;
5.2 消息队列(MQ)
- Topic 隔离:每个租户独立 Topic 或消息 Tag。
- 消息头携带租户 ID:
Message message = new Message(); message.putUserProperty("tenantId", "1001");
6. 权限与安全控制
6.1 RBAC 扩展
- 角色权限按租户隔离:
sys_role表增加tenant_id。 - 数据权限控制:基于租户 ID 过滤可访问数据。
6.2 安全校验
- 防越权攻击:校验操作对象所属租户。
void updateUser(Long userId) { User user = userMapper.selectById(userId); if (!user.getTenantId().equals(TenantContext.getCurrentTenantId())) { throw new ForbiddenException("无权操作其他租户数据"); } // 后续逻辑 }
7. 系统扩展性设计
7.1 租户注册流程
- 自动创建数据库或初始化表结构(独立数据库模式)。
- 调用
TenantInitializeService初始化基础数据。
7.2 租户配置管理
- 扩展
sys_tenant_config表存储租户个性化配置。CREATE TABLE sys_tenant_config ( tenant_id BIGINT NOT NULL, config_key VARCHAR(50), config_value VARCHAR(100) );
8. 事务与分布式处理
8.1 跨库事务
- 使用 Seata 等分布式事务框架(独立数据库模式)。
8.2 异步任务
- 任务调度器(如 XXL-JOB)按租户分片执行。
9. 测试与调试
9.1 单元测试
- Mock 租户上下文:
@Test public void testUserService() { TenantContext.setTenantId(1001L); // 执行测试逻辑 TenantContext.clear(); }
9.2 日志追踪
- MDC 记录租户 ID:
MDC.put("tenantId", TenantContext.getCurrentTenantId().toString());
10. 最佳实践与陷阱规避
- 禁止操作:
- 避免编写未过滤
tenant_id的 SQL。 - 禁止跨租户数据导入/导出(除非明确授权)。
- 避免编写未过滤
- 工具类推荐:
- 使用
TenantUtils获取当前租户信息。 - 使用
DynamicDataSourceHolder切换数据源(独立库模式)。
- 使用
附录:yudao 多租户核心配置示例
yudao:
tenant:
enable: true
ignore-tables: sys_config, sys_dict # 不进行租户过滤的表
column: tenant_id # 租户字段名