原因:集群模式不支持管道(pipeline),所以写了一个多套redis主从使用(类似数据库分库)
基于 ThreadLocal 动态选择不同 Redis 主从实例 的完整解决方案。
核心思想:为每个业务(租户)配置独立的 RedisConnectionFactory,然后通过自定义 RoutingRedisConnectionFactory 根据当前线程中的租户标识动态路由到对应的工厂,最后使用一个统一的 RedisTemplate 完成操作。
1. ThreadLocal 上下文管理
java
public class RedisContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
CONTEXT.set(tenantId);
}
public static String getTenantId() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
2. 自定义路由连接工厂
java
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisConnectionFactoryUtils;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.lang.Nullable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class RoutingRedisConnectionFactory implements RedisConnectionFactory {
private final Map<String, RedisConnectionFactory> targetConnectionFactories = new ConcurrentHashMap<>();
// 默认工厂(当找不到对应租户时使用)
private RedisConnectionFactory defaultConnectionFactory;
public RoutingRedisConnectionFactory(Map<String, RedisConnectionFactory> factories, RedisConnectionFactory defaultFactory) {
this.targetConnectionFactories.putAll(factories);
this.defaultConnectionFactory = defaultFactory;
}
/**
* 根据当前 ThreadLocal 中的 tenantId 决定实际使用的工厂
*/
private RedisConnectionFactory determineTargetConnectionFactory() {
String tenantId = RedisContextHolder.getTenantId();
RedisConnectionFactory factory = targetConnectionFactories.get(tenantId);
if (factory == null) {
factory = defaultConnectionFactory;
if (factory == null) {
throw new IllegalStateException("No RedisConnectionFactory found for tenant: " + tenantId);
}
}
return factory;
}
@Override
public RedisConnection getConnection() {
return determineTargetConnectionFactory().getConnection();
}
@Override
public RedisConnection getSentinelConnection() {
return determineTargetConnectionFactory().getSentinelConnection();
}
@Override
public void destroy() {
targetConnectionFactories.values().forEach(RedisConnectionFactoryUtils::destroyConnectionFactory);
if (defaultConnectionFactory != null) {
RedisConnectionFactoryUtils.destroyConnectionFactory(defaultConnectionFactory);
}
}
@Override
public boolean getValidateConnection() {
return determineTargetConnectionFactory().getValidateConnection();
}
@Override
public void setValidateConnection(boolean validate) {
throw new UnsupportedOperationException("setValidateConnection not supported on RoutingRedisConnectionFactory");
}
@Override
public boolean getConvertPipelineAndTxResults() {
return determineTargetConnectionFactory().getConvertPipelineAndTxResults();
}
@Override
public void setConvertPipelineAndTxResults(boolean convertPipelineAndTxResults) {
throw new UnsupportedOperationException("setConvertPipelineAndTxResults not supported");
}
}
3. 配置多个具体的连接工厂及路由工厂
假设已有两个租户:tenantA 和 tenantB,分别对应上一节中的 businessA 和 businessB 主从配置。
java
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RoutingRedisConfig {
// 假设已经通过之前的 MultiRedisConfig 定义了以下两个 Bean
@Bean("tenantARedisConnectionFactory")
public RedisConnectionFactory tenantARedisConnectionFactory() {
// 复用之前创建 businessA 连接工厂的逻辑
// 为简洁,此处省略具体代码,请参考上一节 createLettuceConnectionFactory 方法
return new LettuceConnectionFactory();
}
@Bean("tenantBRedisConnectionFactory")
public RedisConnectionFactory tenantBRedisConnectionFactory() {
return new LettuceConnectionFactory();
}
/**
* 构建路由工厂:将租户ID映射到对应的真实工厂
*/
@Bean
public RoutingRedisConnectionFactory routingRedisConnectionFactory(
@Qualifier("tenantARedisConnectionFactory") RedisConnectionFactory factoryA,
@Qualifier("tenantBRedisConnectionFactory") RedisConnectionFactory factoryB) {
Map<String, RedisConnectionFactory> factories = new HashMap<>();
factories.put("tenantA", factoryA);
factories.put("tenantB", factoryB);
// 设置默认工厂,当租户ID未设置或找不到时使用 tenantA
return new RoutingRedisConnectionFactory(factories, factoryA);
}
/**
* 暴露一个统一的 RedisTemplate,内部使用路由连接工厂
*/
@Bean
@Primary
public RedisTemplate<String, Object> redisTemplate(RoutingRedisConnectionFactory routingFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(routingFactory);
// 统一序列化方式
StringRedisSerializer stringSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
4. 使用示例(结合 AOP 或拦截器自动设置/清理租户)
方式一:在业务代码中手动设置
java
@Service
public class OrderService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void processOrder(String tenantId, String orderId) {
try {
RedisContextHolder.setTenantId(tenantId);
// 后续所有 Redis 操作都会自动路由到对应租户的主从实例
redisTemplate.opsForValue().set("order:" + orderId, "processing");
} finally {
RedisContextHolder.clear(); // 务必清理,避免线程污染
}
}
}
方式二:使用过滤器 / 拦截器(推荐)
java
@Component
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从请求头中获取租户ID(也可从 JWT、Session 等获取)
String tenantId = request.getHeader("X-Tenant-Id");
if (tenantId == null) {
tenantId = "tenantA"; // 默认租户
}
RedisContextHolder.setTenantId(tenantId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
RedisContextHolder.clear();
}
}
注册拦截器:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TenantInterceptor tenantInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tenantInterceptor).addPathPatterns("/**");
}
}
5. 要点总结
- ThreadLocal 保证了线程隔离,适合 Web 请求或任务执行链路。
- RoutingRedisConnectionFactory 实现了动态路由,内部维护租户ID到真实连接工厂的映射。
- 统一 RedisTemplate 对外透明,业务代码无需感知多数据源切换。
- 务必清理 ThreadLocal,推荐使用拦截器或 AOP 的
@After通知,防止内存泄漏或数据错乱。 - 支持主从实例:每个
LettuceConnectionFactory可以独立配置哨兵模式或集群模式,读写分离等策略在各个工厂内部实现。
此方案可以灵活扩展到任意数量的租户,只需将新的租户ID和对应的 RedisConnectionFactory 加入路由映射即可。