一、核心设计思想
通过线程上下文自动传递租户ID,结合动态数据源路由和中间件连接工厂,实现MySQL、Redis、RocketMQ的完全自动化资源隔离。关键设计如下:
二、关键实现方案
- 租户上下文管理(核心)
public class TenantContext {
// 使用TransmittableThreadLocal支持异步传递
private static final TransmittableThreadLocal<String> CONTEXT =
new TransmittableThreadLocal<>();
public static void set(String tenantId) {
CONTEXT.set(tenantId);
DynamicDataSourceContextHolder.push(tenantId); // 同步MySQL上下文
}
public static String get() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
DynamicDataSourceContextHolder.poll(); // 清理MySQL上下文
}
}
- MySQL动态数据源配置
@Configuration
@MapperScan(basePackages = "com.example.mapper")
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.tenantA")
public DataSource tenantADatasource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.tenantB")
public DataSource tenantBDatasource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource routingDataSource(
@Qualifier("masterDataSource") DataSource master,
@Qualifier("tenantADatasource") DataSource tenantA,
@Qualifier("tenantBDatasource") DataSource tenantB) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("tenantA", tenantA);
targetDataSources.put("tenantB", tenantB);
DynamicRoutingDataSource routingDataSource =
new DynamicRoutingDataSource();
routingDataSource.setDefaultTargetDataSource(master);
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
}
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}
- Redis连接工厂隔离
@Configuration
public class RedisConfig {
@Bean
public RedisConnectionFactory tenantARedisFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setDatabase(1); // 租户A专用DB
return new LettuceConnectionFactory(config);
}
@Bean
public RedisConnectionFactory tenantBRedisFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setDatabase(2); // 租户B专用DB
return new LettuceConnectionFactory(config);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(
@Qualifier("tenantARedisFactory") RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new TenantAwareStringRedisSerializer());
return template;
}
}
public class TenantAwareStringRedisSerializer implements StringRedisSerializer {
@Override
public String serialize(String string) {
String tenantId = TenantContext.get();
return (tenantId == null) ? string : tenantId + ":" + string;
}
}
- RocketMQ生产者/消费者自动绑定
@Configuration
public class RocketMQConfig {
@Bean
public DefaultMQProducer tenantProducer() throws MQClientException {
DefaultMQProducer producer = new DefaultMQProducer("tenant_producer");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
return producer;
}
@Bean
public RocketMQListener<String> tenantListener() {
return new TenantAwareListener();
}
}
public class TenantAwareListener implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
String tenantId = TenantContext.get();
// 自动路由到租户专属Topic
String targetTopic = "topic_" + tenantId;
// 处理消息...
}
}
三、自动化上下文传播
- 请求拦截器
@Component
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String tenantId = request.getHeader("X-Tenant-ID");
TenantContext.set(tenantId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
TenantContext.clear();
}
}
- 异步任务支持
@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(new ContextCopyingDecorator());
executor.initialize();
return executor;
}
private static class ContextCopyingDecorator
implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
String tenantId = TenantContext.get();
return () -> {
try {
TenantContext.set(tenantId);
runnable.run();
} finally {
TenantContext.clear();
}
};
}
}
}
四、完整工作流程
-
请求入口
网关拦截请求 → 解析X-Tenant-ID
→ 设置到TenantContext
-
数据访问层
@Service public class OrderService { @Autowired private OrderMapper orderMapper; public Order getOrder(Long id) { // 自动路由到当前租户的数据源 return orderMapper.selectById(id); } }
-
Redis操作
@Service public class CacheService { @Autowired private RedisTemplate<String, Object> redisTemplate; public void setValue(String key, Object value) { // 自动添加租户前缀 redisTemplate.opsForValue().set(key, value); } }
-
消息发送
@Service public class MessageService { @Autowired private DefaultMQProducer producer; public void sendOrderCreated(Order order) { Message msg = new Message("order_created", order.toJson()); producer.send(msg); // 自动绑定租户Topic } }
五、技术优势对比
方案 | 上下文传递方式 | 中间件隔离级别 | 代码侵入性 | 线程安全 |
---|---|---|---|---|
手动切换 | 显式代码调用 | 应用层 | 高 | 需要处理 |
本方案 | 线程上下文自动传播 | 物理隔离 | 低 | 原生支持 |
动态注解 | AOP切面 | 逻辑隔离 | 中 | 需要配置 |
六、生产环境优化点
-
连接池监控
spring: datasource: tenantA: hikari: maximum-pool-size: 20 leak-detection-threshold: 30000
-
RocketMQ消费者隔离
@RocketMQMessageListener( topic = "tenant_#{tenantId}_topic", consumerGroup = "tenant_#{tenantId}_group" ) public class TenantConsumer implements RocketMQListener<String> { // 自动注入当前租户的Consumer }
-
Redis集群支持
@Bean public RedisClusterConfiguration tenantARedisCluster() { RedisClusterConfiguration config = new RedisClusterConfiguration(); config.setClusterNodes(Arrays.asList( "127.0.0.1:7001", "127.0.0.1:7002" )); config.setPassword("tenantA@2024"); return config; }
通过本方案,可实现完全自动化的多租户资源隔离,核心优势在于:
- 零代码侵入:通过上下文自动传播实现资源隔离
- 物理级隔离:每个租户拥有独立数据库/消息队列/缓存集群
- 动态扩展:新增租户只需添加配置,无需修改代码