当前位置: 首页 > news >正文

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 {@Overrideprotected 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 {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {@Overridepublic Expression getTenantId() {Long tid = Optional.ofNullable(TenantContext.get()).orElse(0L);return new LongValue(tid);}@Overridepublic String getTenantIdColumn() { return "tenant_id"; }@Overridepublic 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 {@Overridepublic void insertFill(MetaObject metaObject) {Long tid = Optional.ofNullable(TenantContext.get()).orElse(0L);this.strictInsertFill(metaObject, "tenantId", Long.class, tid);}@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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 {@Overrideprotected 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. 复盘/汇报要点

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

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

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

http://www.dtcms.com/a/546532.html

相关文章:

  • 专注网站建设与制作网站图片的暗纹是怎么做的
  • 给周杰伦做网站网站做百度推广划算吗
  • k8s——实战入门(资源)
  • 茶叶网站建设策划方案u001f代做毕设自己专门网站
  • 找建设网站网站建设搜索优化app推广新闻营销
  • 山东集团网站建设做网站免费的域名
  • 做平面什么网站的素材不侵权北京公司提供注册地址
  • Linux学习笔记--中断子系统
  • Selenium常用方法
  • 哈尔滨网站建设推广服务人力资源网站
  • screen命令指南
  • SAP-ABAP:穿越时空的ABAP基石:深入理解WRITE语句的奥秘与技巧实例详解
  • 做网站产品图片素材前端后端分别是什么意思
  • 做网站销售的昆明百度小程序
  • LeeCode 137. 只出现一次的数字II
  • AOI设备在消费电子领域的检测应用
  • 网站制作 成都土巴兔官网
  • 如何 做网站跳转建设网站企业网上银行
  • 基于需求驱动的自动驾驶感知任务数据集缺口识别与缓解方法
  • 上海文明城市建设网站如何做网站商铺
  • 怎么自己给自己的网站做推广做旅游网站毕设任务书
  • 丹阳网站建设价位网站建设的中期目标
  • html 网站链接cms编码是什么
  • 【原理揭秘】Nginx 匹配规则优先级详解
  • DeepSeek正在探索一种可能显著提升AI“记忆力”的新方法:用图像而非传统的文本token来存储信息
  • 网站数据库怎么备份网站跟换域名
  • 深入理解 Rust 的 VecDeque:环形缓冲区的高效设计与实践
  • wordpress只能本地访问网站优化公司多少钱
  • 网站二级域名周村区建设网站
  • 广告设计公司网站源码登陆网站空间