多租户(Multi-Tenancy)是 SaaS(软件即服务)模式的核心技术,旨在通过单一应用实例为多个租户提供服务,同时保证数据隔离。其实现方式主要分为三种:
tenant_id
字段区分数据,成本最低但需依赖应用层过滤。在每张表中添加 tenant_id
字段,通过 MyBatis-Plus 的 TenantLineInnerInterceptor
插件自动注入租户条件。所有 SQL 操作自动附加 tenant_id = ?
过滤条件,实现数据隔离。
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.3.1version>
dependency>
注意使用 @Async 执行异步任务时,由于异步任务运行在新线程或线程池线程中,ThreadLocal 变量的值无法自动传递到子线程,导致获取到的值为 null。阿里巴巴开源的 TransmittableThreadLocal 支持线程池场景下的上下文传递。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>
@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());
}
}
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();
}
}
@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;
}
});
}
}
@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;
}
}
@Data
public class User {
private Long id;
private String name;
@TableField(value = "tenant_id", fill = FieldFill.INSERT)
private String tenantId; // 自动填充租户ID
}
@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
。
结合字段隔离和动态数据源,例如:
通过 @InterceptorIgnore(tenantLine = "true")
注解跳过特定方法:
@InterceptorIgnore(tenantLine = "true")
public List<User> selectAll() {
return userMapper.selectList(null);
}
从数据库加载租户数据源配置:
@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);
}
问题 | 解决方案 |
---|---|
多表关联查询未注入租户条件 | 升级 MyBatis-Plus 至 3.5.0+,修复关联查询的租户条件注入问题 |
插入时 tenant_id 重复 |
检查实体类 tenant_id 字段的自动填充策略,避免手动赋值 |
动态数据源切换失败 | 确保 DynamicDataSourceContextHolder 在异步线程中传递 |
租户ID未传递导致空指针 | 在拦截器中添加租户ID校验,返回明确错误提示 |