Spring Boot + MyBatis-Plus 插件(多租户架构实战)
Spring Boot + MyBatis-Plus 多租户架构实战
·
Spring Boot + MyBatis-Plus 多租户
一、多租户架构概述
多租户(Multi-Tenancy)是 SaaS(软件即服务)模式的核心技术,旨在通过单一应用实例为多个租户提供服务,同时保证数据隔离。其实现方式主要分为三种:
- 独立数据库:每个租户拥有独立数据库,隔离性最强但成本高。
- 共享数据库独立 Schema:共享数据库实例但逻辑分离(如 PostgreSQL 的 Schema),平衡安全性与成本。
- 共享数据库共享表:通过
tenant_id字段区分数据,成本最低但需依赖应用层过滤。
二、字段隔离模式(共享表)
1. 核心原理
在每张表中添加 tenant_id 字段,通过 MyBatis-Plus 的 TenantLineInnerInterceptor 插件自动注入租户条件。所有 SQL 操作自动附加 tenant_id = ? 过滤条件,实现数据隔离。
2. 实现步骤
(1) 添加依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
(2) 定义租户上下文(注意使用TransmittableThreadLocal)
注意使用 @Async 执行异步任务时,由于异步任务运行在新线程或线程池线程中,ThreadLocal 变量的值无法自动传递到子线程,导致获取到的值为 null。阿里巴巴开源的 TransmittableThreadLocal 支持线程池场景下的上下文传递。
(2.1) 整合TransmittableThreadLocal,实现异步传递
- 引入依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>
- TTL兼容的异步线程池,通过 ThreadPoolTaskExecutor 结合 Executors 装饰线程池:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("ttl-async-");
executor.initialize();
// 使用 TTL 装饰线程池
return TtlExecutors.getTtlExecutor(executor.getThreadPoolExecutor());
}
}
(2.2) 定义租户上下文
public class TenantContext {
private static final TransmittableThreadLocal<String> CURRENT_TENANT= new TransmittableThreadLocal();
public static void setTenantId(String tenantId) {
CURRENT_TENANT.set(tenantId);
}
public static String getTenantId() {
return CURRENT_TENANT.get();
}
public static void clear() {
CURRENT_TENANT.remove();
}
}
(3) 配置拦截器获取租户 ID
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String tenantId = request.getHeader("X-Tenant-ID");
TenantContext.setTenantId(tenantId);
return true;
}
});
}
}
(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() {
return new StringValue(TenantContext.getTenantId());
}
@Override
public String getTenantIdColumn() {
return "tenant_id"; // 数据库字段名
}
@Override
public boolean ignoreTable(String tableName) {
return Arrays.asList("sys_config").contains(tableName); // 忽略系统表
}
}));
return interceptor;
}
}
(5) 实体类标记租户字段
@Data
public class User {
private Long id;
private String name;
@TableField(value = "tenant_id", fill = FieldFill.INSERT)
private String tenantId; // 自动填充租户ID
}
(6) 测试接口
@RestController
public class UserController {
@Autowired
private UserMapper userMapper;
@GetMapping("/users")
public List<User> listUsers() {
return userMapper.selectList(new QueryWrapper<>());
}
}
效果:查询 SELECT * FROM user WHERE tenant_id = 'tenant1',插入时自动填充 tenant_id。
三、高级配置与优化
1. 混合模式支持
结合字段隔离和动态数据源,例如:
- 主业务表使用独立数据库(动态数据源)
- 日志表使用共享表(字段隔离)
2. 忽略租户过滤
通过 @InterceptorIgnore(tenantLine = "true") 注解跳过特定方法:
@InterceptorIgnore(tenantLine = "true")
public List<User> selectAll() {
return userMapper.selectList(null);
}
3. 多租户数据源自动加载
从数据库加载租户数据源配置:
@Bean
public DataSource initialDataSource() {
// 查询租户表获取数据源配置
List<Tenant> tenants = tenantMapper.selectList(null);
Map<Object, Object> dataSources = tenants.stream()
.collect(Collectors.toMap(Tenant::getId, tenant -> createDataSource(tenant)));
dynamicDataSource.setTargetDataSources(dataSources);
}
4. 性能优化
- 连接池管理:使用 Druid 或 HikariCP 配置连接池,避免资源泄漏。
- 缓存机制:缓存租户数据源配置,减少数据库查询频率。
五、常见问题与解决方案
| 问题 | 解决方案 |
|---|---|
| 多表关联查询未注入租户条件 | 升级 MyBatis-Plus 至 3.5.0+,修复关联查询的租户条件注入问题 |
插入时 tenant_id 重复 |
检查实体类 tenant_id 字段的自动填充策略,避免手动赋值 |
| 动态数据源切换失败 | 确保 DynamicDataSourceContextHolder 在异步线程中传递 |
| 租户ID未传递导致空指针 | 在拦截器中添加租户ID校验,返回明确错误提示 |
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)