Spring Cloud 多租户实现(MySQL + MyBatis + MyBatis-Plus 实战)

Spring Cloud 多租户实现(MySQL + MyBatis + MyBatis-Plus)

技术栈:Spring Cloud、Spring Boot 3.x、MySQL 8、MyBatis、MyBatis-Plus、Spring Cloud Gateway、OpenFeign
目标:在单套微服务中实现多租户数据/权限隔离,可从“字段级共享库”平滑演进到“库级独库”。


0. 隔离级别选型与路线

隔离级别方案说明适用成本
表字段级每表增加 tenant_id + MyBatis-Plus 租户插件单库多租户,改造小中小体量,10^2~10^4 租户
库/Schema级每租户独库/独 Schema + 动态数据源隔离更强,性能可隔离中/大体量,合规强隔离★★★
实例级独立数据库实例最强隔离,运维成本高金融/政企★★★★★

建议:字段级快速落地 → 大租户或合规租户迁移到库级;业务代码尽量保持无感。


1. 统一约定

  • 租户标识tenantId。来源优先级:二级域名 xxx.example.com → Header X-Tenant → JWT Claim tenantId

  • 跨服务传递:Gateway 统一解析并透传;Feign 拦截器统一添加 Header;消息(MQ)Header 也携带。

  • 超管租户tenantId=0 代表平台态,默认不开放跨租户查询。


2. 字段级方案:MyBatis-Plus 租户插件

2.1 数据库改造

-- 示例:对 user 表添加租户列与索引
ALTER TABLE `user` ADD COLUMN `tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID' AFTER `id`;
CREATE INDEX idx_user_tenant ON `user`(`tenant_id`);
-- 业务关键表统一添加 tenant_id 与组合索引(tenant_id, biz_key...)

2.2 上下文载体(线程本地)

// TenantContext.java
public class TenantContext {
    private static final ThreadLocal<Long> TL = new ThreadLocal<>();
    public static void set(Long tenantId) { TL.set(tenantId); }
    public static Long get() { return TL.get(); }
    public static void clear() { TL.remove(); }
}

2.3 Web 入口解析(Filter)

@Component
public class TenantContextFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {
        try {
            String host = req.getServerName();
            Long fromDomain = parseTenantFromDomain(host); // 自定义解析
            String header = req.getHeader("X-Tenant");
            Long tenant = fromDomain != null ? fromDomain : (header != null ? Long.valueOf(header) : parseFromJwt(req));
            if (tenant != null) TenantContext.set(tenant);
            chain.doFilter(req, res);
        } finally { TenantContext.clear(); }
    }
}

2.4 MyBatis-Plus 租户插件配置

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                Long tid = Optional.ofNullable(TenantContext.get()).orElse(0L);
                return new LongValue(tid);
            }
            @Override
            public String getTenantIdColumn() { return "tenant_id"; }
            @Override
            public boolean ignoreTable(String tableName) {
                return Set.of("sys_dict", "tenant").contains(tableName); // 白名单:不参与租户隔离
            }
        }));
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

2.5 自动填充与幂等

@Component
public class TenantMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        Long tid = Optional.ofNullable(TenantContext.get()).orElse(0L);
        this.strictInsertFill(metaObject, "tenantId", Long.class, tid);
    }
    @Override
    public void updateFill(MetaObject metaObject) { /* 可选 */ }
}
@Data
@TableName("order")
public class OrderEntity {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    private Long tenantId; // 与列 tenant_id 对应
    private String orderNo;
    private Integer status;
    @Version private Integer version;
    @TableLogic private Integer deleted;
}

幂等:写请求携带 requestId,使用去重表/Redis Set 以租户粒度去重:dedup:{tenantId}:{requestId}

2.6 Feign & MQ 透传

@Configuration
public class FeignTenantInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        Long tid = TenantContext.get();
        if (tid != null) template.header("X-Tenant", String.valueOf(tid));
    }
}

MQ 发送时在 Header 附带 X-Tenant;消费端拿到后先 TenantContext.set() 再执行业务。

2.7 Gateway 统一透传(可选)

@Component
public class TenantGatewayFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest req = exchange.getRequest();
        String host = req.getHeaders().getFirst("Host");
        String tenant = resolveTenant(host, req.getHeaders().getFirst("X-Tenant"));
        ServerHttpRequest mutated = req.mutate().header("X-Tenant", tenant).build();
        return chain.filter(exchange.mutate().request(mutated).build());
    }
    @Override public int getOrder() { return -100; }
}

2.8 常见坑位

  • 联表/子查询:MP 对主表和子查询可注入租户条件,复杂 SQL 需手动补 AND t.tenant_id = ?

  • 分页 count:某些复杂 count SQL 需手写,避免被插件改写造成性能问题。

  • 缓存:Key 加租户前缀,如 cache:{tenantId}:user:{id}

  • 定时任务:按租户分片执行,或在任务体内循环各租户。

  • 日志/追踪:将 tenantId 放入 MDC 与 Trace,日志格式如 %X{tenantId}

MDC.put("tenantId", String.valueOf(Optional.ofNullable(TenantContext.get()).orElse(0L)));

3. 库级方案:动态数据源路由

当大租户需要更强隔离/独立扩容时,迁移到独库。

3.1 路由设计

  • 方案 A:AbstractRoutingDataSource 自定义路由。

  • 方案 B:引入 dynamic-datasource-spring-boot-starter,按租户动态注册/获取数据源。

  • 路由 Key=tenantId;数据源配置可从 Nacos/DB 获取并做 懒加载

public class TenantRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() { return TenantContext.get(); }
}
public DataSource createDs(TenantConfig cfg){
    HikariDataSource ds = new HikariDataSource();
    ds.setJdbcUrl(cfg.getUrl());
    ds.setUsername(cfg.getUser());
    ds.setPassword(cfg.getPwd());
    ds.setMaximumPoolSize(20);
    return ds;
}

3.2 与 MP 的关系

  • 库级 与 字段级 二选一:迁移到独库后可移除租户插件;如存在共享库与独库并存,可保留插件(独库仍写入自身 tenant_id),以降低迁移成本。

3.3 Schema 演进

  • 使用 Flyway/Liquibase 管理 DDL;按租户批量迁移,失败回滚可追踪。


4. 权限与安全

  • RBAC 分层tenant -> role -> user -> permission,授权时强校验 tenantId

  • 越权防护:Controller 层切面校验用户的 tenantId 与请求中的一致;平台态接口默认关闭。

  • 导出与审计:导出任务与对象存储路径均加租户前缀;操作日志带 tenantId 便于稽核。


5. 运维与 SRE

  1. 租户开通:生成 tenantId → 分配域名 → 初始化库/Schema 或共享库记录 → 发放管理员。

  2. 监控:按租户维度统计 QPS、RT、错误率、慢 SQL、连接池水位。

  3. 限流:Gateway/Sentinel 按 tenantId 维度限流与熔断。

  4. 备份/归档:共享库按 tenant_id 过滤;独库按库备份;定期校验恢复演练。

  5. 关停/迁移:数据导出、对账、回收域名/数据源。


6. 验收清单(上线前自查)

  • 所有业务表有 tenant_id(或已独库)。

  • Feign/MQ 透传 X-Tenant,端到端链路可观测。

  • 复杂联表 SQL 已补充 tenant_id 条件并有用例覆盖。

  • 缓存 Key、定时任务、导出路径均带租户前缀/分片。

  • 超管能力关闭或内网白名单;审计日志包含 tenantId


7. 复盘/汇报要点

  • 两步走:共享库字段级先跑通 → 大租户独库化,成本与收益可量化。

  • 稳定性:日志/追踪/指标均可按租户切片定位问题。

  • 合规性:跨租户串读串写有强校验,导出/备份可按租户审计。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愤怒的代码

如果您有受益,欢迎打赏博主😊

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值