Spring Boot + MyBatis-Plus 插件(多租户架构实战)

打印 上一主题 下一主题

主题 1859|帖子 1859|积分 5577

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
Spring Boot + MyBatis-Plus 多租户

一、多租户架构概述

多租户(Multi-Tenancy)是 SaaS(软件即服务)模式的核心技术,旨在通过单一应用实例为多个租户提供服务,同时包管数据隔离。其实现方式重要分为三种:

  • 独立数据库:每个租户拥有独立数据库,隔离性最强但成本高。
  • 共享数据库独立 Schema:共享数据库实例但逻辑分离(如 PostgreSQL 的 Schema),平衡安全性与成本。
  • 共享数据库共享表:通过 tenant_id 字段区分数据,成本最低但需依赖应用层过滤。

二、字段隔离模式(共享表)

1. 核心原理

在每张表中添加 tenant_id 字段,通过 MyBatis-Plus 的 TenantLineInnerInterceptor 插件自动注入租户条件。所有 SQL 操作自动附加 tenant_id = ? 过滤条件,实现数据隔离。
2. 实现步骤

(1) 添加依赖

  1. <dependency>
  2.     <groupId>com.baomidou</groupId>
  3.     <artifactId>mybatis-plus-boot-starter</artifactId>
  4.     <version>3.5.3.1</version>
  5. </dependency>
复制代码
(2) 定义租户上下文(注意使用TransmittableThreadLocal)

注意使用 @Async 执行异步任务时,由于异步任务运行在新线程或线程池线程中,ThreadLocal 变量的值无法自动通报到子线程,导致获取到的值为 null。阿里巴巴开源的 TransmittableThreadLocal 支持线程池场景下的上下文通报。
(2.1) 整合TransmittableThreadLocal,实现异步通报



  • 引入依赖:
  1. <dependency>
  2.     <groupId>com.alibaba</groupId>
  3.     <artifactId>transmittable-thread-local</artifactId>
  4.     <version>2.14.2</version>
  5. </dependency>
复制代码


  • TTL兼容的异步线程池,通过 ThreadPoolTaskExecutor 结合 Executors 装饰线程池:
  1. @Configuration
  2. @EnableAsync
  3. public class AsyncConfig implements AsyncConfigurer {
  4.     @Override
  5.     public Executor getAsyncExecutor() {
  6.         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  7.         executor.setCorePoolSize(4);
  8.         executor.setMaxPoolSize(8);
  9.         executor.setQueueCapacity(100);
  10.         executor.setThreadNamePrefix("ttl-async-");
  11.         executor.initialize();
  12.         // 使用 TTL 装饰线程池
  13.         return TtlExecutors.getTtlExecutor(executor.getThreadPoolExecutor());
  14.     }
  15. }
复制代码
(2.2) 定义租户上下文

  1. public class TenantContext {
  2.     private static final TransmittableThreadLocal<String> CURRENT_TENANT= new TransmittableThreadLocal();
  3.     public static void setTenantId(String tenantId) {
  4.         CURRENT_TENANT.set(tenantId);
  5.     }
  6.     public static String getTenantId() {
  7.         return CURRENT_TENANT.get();
  8.     }
  9.     public static void clear() {
  10.         CURRENT_TENANT.remove();
  11.     }
  12. }
复制代码
(3) 配置拦截器获取租户 ID

  1. @Configuration
  2. public class WebConfig implements WebMvcConfigurer {
  3.     @Override
  4.     public void addInterceptors(InterceptorRegistry registry) {
  5.         registry.addInterceptor(new HandlerInterceptor() {
  6.             @Override
  7.             public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
  8.                 String tenantId = request.getHeader("X-Tenant-ID");
  9.                 TenantContext.setTenantId(tenantId);
  10.                 return true;
  11.             }
  12.         });
  13.     }
  14. }
复制代码
(4) 配置 MyBatis-Plus 插件

  1. @Configuration
  2. public class MyBatisPlusConfig {
  3.     @Bean
  4.     public MybatisPlusInterceptor mybatisPlusInterceptor() {
  5.         MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  6.         interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
  7.             @Override
  8.             public Expression getTenantId() {
  9.                 return new StringValue(TenantContext.getTenantId());
  10.             }
  11.             @Override
  12.             public String getTenantIdColumn() {
  13.                 return "tenant_id";  // 数据库字段名
  14.             }
  15.             @Override
  16.             public boolean ignoreTable(String tableName) {
  17.                 return Arrays.asList("sys_config").contains(tableName);  // 忽略系统表
  18.             }
  19.         }));
  20.         return interceptor;
  21.     }
  22. }
复制代码
(5) 实体类标志租户字段

  1. @Data
  2. public class User {
  3.     private Long id;
  4.     private String name;
  5.     @TableField(value = "tenant_id", fill = FieldFill.INSERT)
  6.     private String tenantId;  // 自动填充租户ID
  7. }
复制代码
(6) 测试接口

  1. @RestController
  2. public class UserController {
  3.     @Autowired
  4.     private UserMapper userMapper;
  5.     @GetMapping("/users")
  6.     public List<User> listUsers() {
  7.         return userMapper.selectList(new QueryWrapper<>());
  8.     }
  9. }
复制代码
结果:查询 SELECT * FROM user WHERE tenant_id = 'tenant1',插入时自动填充 tenant_id。

三、高级配置与优化

1. 混淆模式支持

结合字段隔离和动态数据源,比方:


  • 主业务表使用独立数据库(动态数据源)
  • 日记表使用共享表(字段隔离)
2. 忽略租户过滤

通过 @InterceptorIgnore(tenantLine = "true") 注解跳过特定方法:
  1. @InterceptorIgnore(tenantLine = "true")
  2. public List<User> selectAll() {
  3.     return userMapper.selectList(null);
  4. }
复制代码
3. 多租户数据源自动加载

从数据库加载租户数据源配置:
  1. @Bean
  2. public DataSource initialDataSource() {
  3.     // 查询租户表获取数据源配置
  4.     List<Tenant> tenants = tenantMapper.selectList(null);
  5.     Map<Object, Object> dataSources = tenants.stream()
  6.         .collect(Collectors.toMap(Tenant::getId, tenant -> createDataSource(tenant)));
  7.     dynamicDataSource.setTargetDataSources(dataSources);
  8. }
复制代码
4. 性能优化



  • 毗连池管理:使用 Druid 或 HikariCP 配置毗连池,避免资源泄漏。
  • 缓存机制:缓存租户数据源配置,减少数据库查询频率。

五、常见问题与解决方案

问题解决方案多表关联查询未注入租户条件升级 MyBatis-Plus 至 3.5.0+,修复关联查询的租户条件注入问题插入时 tenant_id 重复查抄实体类 tenant_id 字段的自动填充计谋,避免手动赋值动态数据源切换失败确保 DynamicDataSourceContextHolder 在异步线程中通报租户ID未通报导致空指针在拦截器中添加租户ID校验,返回明确错误提示
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

吴旭华

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表