根据ThreadLocal使用不同的redis主从实例

5 阅读3分钟

原因:集群模式不支持管道(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 加入路由映射即可。