基于 yudao 框架的多租户开发注意点

1,462 阅读3分钟

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                    # 租户字段名