首页 / 资讯中心 / 文章详情

SpringDataRedis自定义多个RedisTemplate实现多数据源切换

SpringDataRedis自定义多个RedisTemplate实现多数据源切换 ★ FEATURED ARTICLE
SpringDataRedis 中自定义多个 RedisTemplate 实现 Redis 多数据源切换使用这些年做后端服务跟 Redis 打交道的时间确实不少。刚开始那会儿一个 Spring Boot 服务里就一个 Redis 实例配置好StringRedisTemplate或者默认的RedisTemplate存个缓存、顶个分布式锁感觉挺省心。后来业务越拆越细就发现事情没那么简单了同一个服务里可能要同时访问多个 Redis 实例——比如一个是负责用户登录态和基础缓存的主库一个是专门存业务埋点数据、报表统计数据的数据仓库还可能有一个是独立部署、给某个高频接口用的专用缓存库。这时候再指望 Spring Boot 默认帮我们自动配置的那一份 RedisTemplate就完全不够用了。这篇文章我会从实际项目踩坑的角度把“在 SpringDataRedis 里自定义多个 RedisTemplate 实现 Redis 多数据源切换使用”这套做法完整地捋一遍。我们会聊到为什么默认的单模板设计撑不住多数据源的场景、怎么从底层连接工厂开始定制一套专属于你自己的 RedisTemplate 体系、序列化方案怎么选才不出乱码、多数据源下事务边界怎么处理、以及项目上线后那些最容易踩的坑和排查思路。如果你手头正好在做一个需要分库、分实例存储的中大型项目或者你只是想把 Spring Data Redis 的配置机制吃得更透这篇文章能帮你省下不少走弯路的时间。1. 内容整体设计与思路拆解1.1 为什么默认的 RedisTemplate 不够用记得我刚开始做多数据源的时候第一反应是觉得“这有什么难的多配置几个连接信息不就行了”。后来发现自己还是太年轻——Spring Boot 的自动配置机制其实是按照“单 Redis 实例”这个假设来设计的。它会在应用启动的时候读取spring.redis.*这一组配置然后自动创建一个连接工厂、一个默认的RedisTemplate和一个StringRedisTemplate。这套机制的好处是开箱即用但如果你的服务里同时存在多个不同地址、不同库索引、甚至不同密码的 Redis 实例自动配置生成的这份单例模板就有点尴尬了。我们就拿一个典型的业务场景来说。我在之前参与过的一个电商类模拟项目里核心服务需要同时访问两个 Redis 实例第一个实例存用户的购物车数据和商品缓存用的是0号库第二个实例单独存运营活动相关的计数器、限流数据用的是2号库。起初为了图省事直接在代码里把两个数据源都塞进同一个RedisTemplate靠切换数据库下标来区分。结果线上跑了没多久就出问题了——连接工厂内部的连接池是共用的一套某个数据源的慢查询会影响到另一个数据源连接占用一多整个服务访问 Redis 的延迟直接飙升。更要命的是这种方案只适用于同一个 Redis 实例的不同库如果两个 Redis 实例部署在不同的机器上那就完全没法用同一个连接工厂来连接了。另外默认的RedisTemplate在序列化上也有坑。Spring Boot 自动配置的 RedisTemplate 默认用的是 JdkSerializationRedisSerializer存进去的 key 会带上\xac\xed\x00\x05t\x00...这种让人头大的一长串前缀在命令行里看 key 名简直怀疑人生。而且 JDK 序列化出来的数据跨语言读取能力很差对于需要和其他系统共享同一份缓存数据的场景来说简直是灾难。所以当我们决定走向“自定义多个 RedisTemplate”这条路的时候其实不只是为了“多一个 Bean”更是要把连接管理、序列化策略、事务边界这些都重新理一遍。1.2 多数据源切换的几种主流方案对比在正式撸代码之前我还是想把方案选型这件事讲透。目前业界处理 Redis 多数据源的大方向也就那么几类第一种是“多个 RedisTemplate”方案也就是这篇文章的主角。思路很直接——手动定义多个连接工厂再基于每个连接工厂各自造出一个独立的 RedisTemplate Bean每个 Bean 起一个语义清晰的名字比如userRedisTemplate、activityRedisTemplate。这个方案最大的优点就是代码可读性好类型安全调用方按需注入不需要在运行时通过字符串判断走哪条路。缺点嘛也很明显——每个数据源都要维护一套独立的连接池配置资源开销会大一些但对应地隔离性也是真好。第二种是“动态路由”方案有点类似多数据源的动态数据源路由思路。核心做法是把多个连接工厂预先建好塞到一个 Map 里面再通过一个自定义的路由器去决定当前线程走哪个数据源。这类方案的优点是调用方不需要感知底层有几个 Redis代码看起来很统一缺点是隐式状态切换容易翻车——如果你在方法 A 里切到了数据源 B结果方法 A 没执行完就抛异常了当前线程的数据源标记没清干净下一个请求继续用这个线程池里的线程就可能拿到错误的数据源。第三种是直接在业务代码里注入两个StringRedisTemplate通过Qualifier区分使用。这个方案其实是第一种的简化版适合两个数据源、每个数据源的数据操作都比较简单的场景。但一旦数据源数量变多、序列化策略各有不同这种写法会让配置类变得很杂乱。我个人的建议是如果数据源数量是固定的、可枚举的比如两到四个直接上“多个 RedisTemplate”方案是最省心的如果数据源数量会动态变化且切换逻辑极其复杂再考虑路由方案。大部分团队遇到的多数据源需求其实都停留在“两个 Redis 实例、两套配置、互不干扰”这种层面完全没必要一上来就整分布式路由那一套复杂机制。1.3 从依赖和配置层面做好准备工作方案定了下一步就是先把基础工程铺好。我用的是 Spring Boot 2.7.x 版本数据库这块引入的是spring-boot-starter-data-redis连接池用的是 Apache Commons Pool 2。这里有一个容易忽略的细节Spring Boot 2.x 默认的 Redis 客户端驱动是 Lettuce它自身是线程安全的并且默认就开启了连接池所以只要不手动排除依赖基本不需要额外操心客户端选型。但如果你项目里历史原因用了 Jedis也完全没问题整体思路是一样的只是连接工厂的具体实现类不同而已。在我的模拟项目里我建了一个独立的配置模块专门用来放多数据源的配置类。配置类我拆成了两个部分一部分是一个RedisProperties风格的属性类用来承载application.yml里各个数据源的连接信息另一部分是真正的Configuration配置类通过读取属性类去创建连接工厂和对应的 RedisTemplate。属性这一层不要贪便宜直接复用 Spring Boot 默认的RedisProperties因为那个类只支持单实例强行复用会搞得很难看。自己写一个ConfigurationProperties(prefix spring.redis.multi)的属性类配置结构一清二楚后续加数据源只需动配置文件和配置类。配置文件的写法大概是这种结构spring: redis: multi: user: host: 127.0.0.1 port: 6379 database: 0 password: user-pass timeout: 3000ms lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 activity: host: 127.0.0.1 port: 6380 database: 2 password: activity-pass timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 4 min-idle: 1注意这里我特意给两个数据源起了名字叫user和activity对应后续创建的userRedisTemplate和activityRedisTemplate。配置结构看起来简洁但买菜还是要算清楚——两个实例分别独立的连接池参数是因为它们的负载特性完全不同用户数据访问特别频繁连接池要放大一点活动数据主要是运营后台低频写入连接池小一些就够了没必要浪费资源。2. 核心细节解析与实操要点2.1 连接工厂的定制为什么要管连接池和序列化很多人自定义 RedisTemplate 的时候会犯一个比较典型的错误——只复制了一个 RedisTemplate 的壳没有真正理解连接工厂的含义。RedisTemplate 本身只是一个封装层它负责把业务层的操作指令翻译成底层的 Redis 协议命令再交给连接工厂去建立连接、执行命令、返回结果。所以你想让两个数据源真正做到物理隔离最关键的一步是让每个 RedisTemplate 都持有一个自己专属的RedisConnectionFactoryBean。我见过一些人图省事从 Spring 容器里把默认的连接工厂直接注入到自定义配置类里然后基于同一个工厂创建多个 RedisTemplate。这个做法表面上看也是多个模板实际上底层连接还是走同一个连接池相当于什么都没隔离。正确的方式一定是每个数据源都创建一套独立的连接工厂配置。Bean(userConnectionFactory) public RedisConnectionFactory userConnectionFactory(RedisProperties multiProperties) { RedisStandaloneConfiguration configuration new RedisStandaloneConfiguration(); configuration.setHostName(multiProperties.getUser().getHost()); configuration.setPort(multiProperties.getUser().getPort()); configuration.setDatabase(multiProperties.getUser().getDatabase()); configuration.setPassword(RedisPassword.of(multiProperties.getUser().getPassword())); LettuceClientConfiguration clientConfig LettuceClientConfiguration.builder() .commandTimeout(multiProperties.getUser().getTimeout()) .build(); return new LettuceConnectionFactory(configuration, clientConfig); }这里有几个点要单独拎出来说。第一个是RedisPassword.of(...)这个方法如果密码为空字符串或者 null不要硬传否则会直接报连接认证错误。第二个是commandTimeout我建议每个数据源单独设置有些业务场景要求 Redis 响应极快超时时间就要设短一点比如 500ms快速失败比一直阻塞线程池强。第三个是连接池大小Lettuce 连接池底层用的是 commons-pool2如果你不显式配置生产环境很容易遇到连接不够用的情况。连接工厂定义好了之后RedisTemplate 本身也需要精心“调校”。坦白讲Spring Data Redis 提供的序列化器选择真的能直接决定你的缓存数据是“人话”还是“乱码”。我们项目中用的组合是key 序列化器StringRedisSerializer保证 key 是普通字符串方便运维排查。value 序列化器GenericJackson2JsonRedisSerializer它在 JSON 序列化的基础上多存一个class类型信息字段反序列化的时候能自动还原成对应的 Java 对象。hash key 序列化器StringRedisSerializer。hash value 序列化器GenericJackson2JsonRedisSerializer。为什么用GenericJackson2JsonRedisSerializer而不是默认的 JDK 序列化道理其实很朴素JSON 格式的数据可读性好、跨语言兼容性强而且 Jackson 序列化的体积比 JDK 序列化小很多并且GenericJackson2JsonRedisSerializer能通过存储类型信息解决反序列化时丢失泛型的问题。不过它也不是没有代价——JSON 里多出来的class字段会占用额外空间而且这个序列化器对 Lambda 表达式和匿名内部类的序列化支持很差所以我在用的时候有个原则只往 Redis 里塞普通的 POJO、List、Map 这类数据绝不缓存带复杂泛型嵌套的匿名对象。2.2 把多个 RedisTemplate 的 Bean 定义写清楚既然要定义多个 RedisTemplateBean 的命名和定义方式就需要格外规整不然项目一多人就乱了。我的做法是每个模板单独写一个Bean方法显式指定 Bean 名称并且在配置类的类级别加上Configuration保证这些 Bean 能被 Spring 容器统一管理。Bean(userRedisTemplate) public RedisTemplateString, Object userRedisTemplate(Qualifier(userConnectionFactory) RedisConnectionFactory connectionFactory) { return createRedisTemplate(connectionFactory); } Bean(activityRedisTemplate) public RedisTemplateString, Object activityRedisTemplate(Qualifier(activityConnectionFactory) RedisConnectionFactory connectionFactory) { return createRedisTemplate(connectionFactory); }这里有一个值得注意的细节Qualifier不能省。Spring 容器里有多个连接工厂的时候如果你只靠参数类型去注入它会直接报NoUniqueBeanDefinitionException告诉你找到多个同类型的 Bean不知道选哪个。显式指定 Bean 名称是最稳妥的做法。另外我写了一个私有方法createRedisTemplate来统一创建模板这样以后要加第三个数据源的时候只需要多写一个两行的Bean方法公共逻辑不需要改非常方便扩展。2.3 运行时动态切换不只靠注入说到“切换”这两个字很多人的第一反应是在方法里手写 if-else判断某个业务场景走哪个 RedisTemplate。但真正的多数据源切换我认为有两种层次第一种是启动期静态绑定。在配置类里直接默认某个 RedisTemplate 为Primary或者干脆放弃自动注入在业务代码里通过构造器显式注入指定的 RedisTemplate Bean。这种方式适合数据源与业务模块的绑定关系非常清晰固定的情况。比如用户模块只碰用户数据源运营模块只碰活动数据源模块之间没有交叉。第二种是运行时动态切换。某些业务场景下同一个服务方法可能要按请求参数去决定访问哪个数据源。比如一个租户系统租户 A 的数据在 Redis 实例 A 上租户 B 的数据在 Redis 实例 B 上所有租户共用一套代码。这时候就需要一个轻量级的切换器。我在模拟项目里做了一个简单的动态切换方案扩展了 Spring Data Redis 的StringRedisTemplate自定义了一个RoutingRedisTemplate内部维护一个MapString, RedisTemplate和ThreadLocal保存当前线程要用的数据源 key。每次执行操作的时候从 ThreadLocal 里拿出 key路由到真实的模板上执行。核心逻辑其实就几十行但比在每个业务方法里写 if-else 要优雅得多。public class RoutingRedisTemplate extends StringRedisTemplate { private final MapString, RedisTemplateString, Object templateMap; private final ThreadLocalString currentLookupKey new ThreadLocal(); public RoutingRedisTemplate(MapString, RedisTemplateString, Object templateMap) { this.templateMap templateMap; } public void setCurrentLookupKey(String lookupKey) { currentLookupKey.set(lookupKey); } public void clear() { currentLookupKey.remove(); } public RedisTemplateString, Object determineTemplate() { String lookupKey currentLookupKey.get(); Assert.hasText(lookupKey, 请先通过 setCurrentLookupKey 设置数据源 key); RedisTemplateString, Object template templateMap.get(lookupKey); Assert.notNull(template, 无法找到对应的 RedisTemplate: lookupKey); return template; } Override public ValueOperationsString, Object opsForValue() { return determineTemplate().opsForValue(); } }这种方案唯一需要你绷紧一根弦的就是清理 ThreadLocal。尤其是用了线程池之后线程是复用的当前线程的 key 没有清干净下个任务就可能串数据源。我在这块吃过一次亏线上有一次活动数据莫名其妙写到了用户数据源排查了很久才发现是线程池里的线程残留了上一次请求设置的 key。后来我的解决思路是要么在拦截器/过滤器里统一在 finally 里调用clear()要么用TransmittableThreadLocal配合任务提交时的包装来规避。这个点非常关键后面专门讲。3. 实操过程与核心环节实现3.1 从属性类到配置类的完整实现纸上谈兵告一段落现在直接进入工程落地。按照我们第一步规划的配置结构我先把属性类写出来。这个类的作用是解析application.yml里以spring.redis.multi开头的配置并且把每个数据源的信息单独封装成一个内部类而不是用一个 Map 去接所有配置——Map 的好处是灵活坏处是写起来没有类型提示很容易把 key 名敲错。Data ConfigurationProperties(prefix spring.redis.multi) public class MultiRedisProperties { private RedisProperties user new RedisProperties(); private RedisProperties activity new RedisProperties(); Data public static class RedisProperties { private String host; private int port; private int database; private String password; private Duration timeout Duration.ofSeconds(3); private Pool pool new Pool(); Data public static class Pool { private int maxActive 8; private int maxIdle 8; private int minIdle 0; } } }这里我把user和activity固定写成两个内嵌属性而不是用MapString, RedisProperties。这是有意为之的因为项目里数据源就是固定两个将来就算增加数据源也是改代码而不是靠动态配置来驱动。固定属性可以让代码在编译期就发现问题配置少写一个键都过不了编译这比运行时才发现配置缺失要安全得多。然后是核心配置类。这个类要做的事情有三件根据MultiRedisProperties为每个数据源创建独立的连接工厂。基于每个连接工厂创建对应的 RedisTemplate。把所有的 RedisTemplate 统一收集到一个Map里方便后续做动态路由时使用。Configuration EnableConfigurationProperties(MultiRedisProperties.class) public class RedisMultiDataSourceConfig { Bean(userConnectionFactory) public RedisConnectionFactory userConnectionFactory(MultiRedisProperties properties) { return createConnectionFactory(properties.getUser()); } Bean(activityConnectionFactory) public RedisConnectionFactory activityConnectionFactory(MultiRedisProperties properties) { return createConnectionFactory(properties.getActivity()); } Bean(userRedisTemplate) public RedisTemplateString, Object userRedisTemplate( Qualifier(userConnectionFactory) RedisConnectionFactory connectionFactory) { return createRedisTemplate(connectionFactory); } Bean(activityRedisTemplate) public RedisTemplateString, Object activityRedisTemplate( Qualifier(activityConnectionFactory) RedisConnectionFactory connectionFactory) { return createRedisTemplate(connectionFactory); } Bean(routingRedisTemplate) public RoutingRedisTemplate routingRedisTemplate( Qualifier(userRedisTemplate) RedisTemplateString, Object userTemplate, Qualifier(activityRedisTemplate) RedisTemplateString, Object activityTemplate) { MapString, RedisTemplateString, Object templateMap new HashMap(); templateMap.put(user, userTemplate); templateMap.put(activity, activityTemplate); return new RoutingRedisTemplate(templateMap); } private RedisConnectionFactory createConnectionFactory(MultiRedisProperties.RedisProperties props) { RedisStandaloneConfiguration config new RedisStandaloneConfiguration(); config.setHostName(props.getHost()); config.setPort(props.getPort()); config.setDatabase(props.getDatabase()); if (StringUtils.hasText(props.getPassword())) { config.setPassword(RedisPassword.of(props.getPassword())); } GenericObjectPoolConfigObject poolConfig new GenericObjectPoolConfig(); poolConfig.setMaxTotal(props.getPool().getMaxActive()); poolConfig.setMaxIdle(props.getPool().getMaxIdle()); poolConfig.setMinIdle(props.getPool().getMinIdle()); LettucePoolingClientConfiguration clientConfig LettucePoolingClientConfiguration.builder() .commandTimeout(props.getTimeout()) .poolConfig(poolConfig) .build(); return new LettuceConnectionFactory(config, clientConfig); } private RedisTemplateString, Object createRedisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); StringRedisSerializer stringSerializer new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashKeySerializer(stringSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }需要特别注意的是template.afterPropertiesSet()这一行。RedisTemplate在设置了连接工厂之后内部很多初始化的动作是在afterPropertiesSet()方法里完成的。如果是让 Spring 容器直接管理 BeanSpring 会自动调用这个方法但如果你像我一样在配置类的方法里手动new出来的模板就必须自己手动调用否则可能出现序列化器生效了但连接工厂没有被正确关联的诡异问题。这个问题藏得挺深我第一次写的时候漏了这行结果在运行时报了一个连接工厂为 null 的错误排查了好久才发现是初始化时序的问题。3.2 实操中的注意细节配置校验与多环境变量多数据源配置和普通单数据源配置最大的区别是配置项翻倍出错概率也跟着翻倍。我在设计配置类的时候加了一层简单的配置校验逻辑在创建连接工厂之前先检查host、port等关键信息是否存在避免某个环境忘记配导致启动后运行报错。这里有一个很重要的实践经验多数据源的配置一定要纳入配置中心管理不要直接写在application.yml的明文里面。尤其是一个服务里既有生产 Redis 又有测试 Redis 的时候很容易把环境配混。我之前在某公司的模拟项目里遇到过类似的情况开发环境里连的是测试 Redis但配置文件里把其中一个数据源的端口错写成了生产端口结果开发调试的时候一会儿连上测试库、一会儿连上生产库数据整个乱掉了。后来我强制要求团队所有 Redis 连接参数全部从配置中心拉取并且不同环境通过 profile 做严格隔离才算彻底杜绝了这类低级错误。另外我再给一个非常实用的建议把自定义 RedisTemplate 的创建过程抽成一个独立的 starter 模块。如果你所在的团队维护着多个微服务每个服务都要接 Redis 多数据源直接在每个服务里复制粘贴这一套配置类会很痛苦。更优雅的做法是把配置类、属性类、路由模板全部放进一个redis-multi-spring-boot-starter依赖里业务服务只需要引入依赖并在application.yml里填好数据源配置即可。这样一来序列化策略、连接池策略、切换逻辑只要在 starter 里维护一份所有接入方自动同步既减少了重复代码也杜绝了各服务之间序列化器配置不一致的乱象。3.3 动态切换的协作者拦截器和上下文上面我们造出了RoutingRedisTemplate但光有它还不够——你得有一个方便的地方去设置和清理当前线程的 lookup key。我习惯用一个拦截器来统一处理定义两个注解一个叫DataSource标注在 service 方法上指定该方法用到哪个数据源 key另一个叫DataSourceCleanup标注在使用完路由模板之后需要清理 ThreadLocal 的方法上。然后通过 Spring AOP 在方法执行前设置 key在方法执行后包括异常情况下清理 key。Aspect Component public class RedisRoutingAspect { Before(annotation(dataSource)) public void setDataSource(DataSource dataSource) { // 把注解里指定的 key 写入 ThreadLocal } AfterReturning(annotation(RedisRoutingAspect)) public void clearDataSource() { // 方法正常返回后清理 } AfterThrowing(annotation(RedisRoutingAspect)) public void clearDataSourceAfterThrowing() { // 方法抛异常后清理 } }其实更稳妥的方案是用Around通知在finally块里清理保证不管方法怎么结束都不会残留 ThreadLocal 状态。代码大概长这样Around(annotation(dataSource)) public Object around(ProceedingJoinPoint joinPoint, DataSource dataSource) throws Throwable { try { routingRedisTemplate.setCurrentLookupKey(dataSource.value()); return joinPoint.proceed(); } finally { routingRedisTemplate.clear(); } }这套做法的好处是业务代码里完全不用关心数据源切换的细节只需要在相应的方法或者类上打上DataSource(user)注解剩下的交给切面去处理。这不仅让代码更干净也更不容易漏掉清理逻辑。在 Spring Boot 中要开启 AOP别忘了在启动类或者配置类上加上EnableAspectJAutoProxy否则切面不会生效。4. 常见问题与排查技巧实录4.1 连不上 Redis 的几种典型报错自定义多个 RedisTemplate 之后最先遇到的一批问题通常都集中在连接阶段。这里我把最常见的三四种情况整理成一张速查表方便兄弟们遇到问题的时候照着排查报错特征大概率原因解决思路Connection refused: no further information目标 Redis 主机的 IP 或端口配错或者端口被防火墙拦截先用命令行工具redis-cli -h host -p port ping手动验证连通性NOAUTH Authentication required配置里没填密码或者填错了检查spring.redis.multi.name.password注意不同环境配置中心里的密码可能不一样Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: DENIED目标 Redis 开启了 ACL 权限控制确认使用的用户名和对应的权限策略ACL 模式下需要显式指定用户名ERR DB index is out of range配置的database下标超过了目标 Redis 实例的最大库数修改database配置或者在服务端调整databases配置排查这类问题的第一步永远是先用一个最简单的 Redis 客户端比如redis-cli在运行的机器上直接测试目标 Redis 的连通性。这个问题能过滤掉一半以上的低级错误——比如配置文件里多了个空格、端口写成了别的服务端口、Redis 密码带特殊字符没转义之类的。我见过有人花了大半天去查代码和序列化配置结果最后发现配置文件里 host 写的是域名但该域名在应用服务器上的 DNS 解析根本没配这就很搞笑了。4.2 RedisTemplate 序列化踩坑实录多 RedisTemplate 还有一个绕不开的问题序列化器没有配对。当你同时操作多个数据源每个数据源的数据结构又不一样时序列化配置稍有不慎就会互相干扰。最典型的两个坑第一个坑是Cacheable注解缓存的数据类型。如果你用的是 Spring Cache 集成 Redis那默认的CacheManager和 RedisTemplate 之间是有一套独立序列化逻辑的。默认情况下RedisCacheManager使用的缓存 value 序列化器跟你自己定义的模板不一定一致。如果你的业务代码里一部分缓存走Cacheable另一部分缓存走自定义模板手动读写很容易出现“我写进去了但用模板读出来是乱码”的现象。解决思路是要么把RedisCacheManager也换成自定义的序列化器要么干脆统一不用Cacheable全部走模板手动读写避免序列化方式分叉。第二个坑是反序列化类型转换异常。我用GenericJackson2JsonRedisSerializer存数据的时候如果存的是一个ListActivityInfo读出来的时候类型信息是保留在 JSON 里的所以强转成ListActivityInfo一般没问题。但如果你存的是一个泛型嵌套非常深的对象比如PageResultUserVOJSON 里只会记录最外层PageResult的类型内部泛型参数在反序列化时可能会解析成一个LinkedHashMap而不是你期望的UserVO集合强转就会触发ClassCastException。这个坑在我之前的模拟项目里真的遇到过后来针对这类场景我自己写了一个包装类去实现类型转换而不是依赖通用的GenericJackson2JsonRedisSerializer硬扛。关于这一点我的建议是能存简单对象就存简单对象复杂的嵌套结构先把它转成 JSON 字符串或者拆成几个简单的 key 分别存储别跟泛型死磕。4.3 本地调试与多环境切换的经验之谈最后再聊一个很容易被忽略的实操点本地调试场景下的多数据源。很多团队本地开发环境网络策略比较严格开发者的电脑可能访问不到测试环境里的 Redis 实例这个时候如果你本地把spring.redis.multi指向了测试环境的 Redis首次启动大概率是直接报连接超时。我通常的做法是给每个数据源提供一个本地默认值比如本地启动全部指向一个本机 Redis通过 profile 来隔离本地开发 profile 配置host: 127.0.0.1port: 6379所有数据源共用。测试环境 profile 配置用测试环境专属的 Redis 实例地址。生产环境 profile 配置从配置中心拉取真实的连接信息。这样做的好处非常直接本地启动毫无压力代码改动对数据源隔离的验证逻辑其实跟线上完全一致——因为本质上你还是创建了多个连接工厂和多个模板只是连接到的具体地址不同而已。有一次我在本地调试动态切换逻辑就是让两个数据源都指向本机同一个 Redis 实例然后开两个不同端口的实例去模拟真实的多实例场景排查起来非常方便。5. 多数据源场景下的性能调优与运维观察5.1 线程池与连接池的配合自定义多个 RedisTemplate 以后你可能会发现一个微妙的变化原本一个RedisTemplate对应一个连接工厂连接池的参数只需要调一处现在变成了多个连接工厂每个都要单独调。连接池参数的设定不能拍脑袋要结合业务接口的 QPS 和 Redis 操作延迟来算。我拿之前那个模拟项目的实际数据举个例子。用户数据源的接口平均 QPS 大概在 500 左右每次请求平均会执行 2 次 Redis 操作一次操作平均往返延迟是 1ms那么理论上需要的同时活跃连接数是500 × 2 × 0.001 1看起来很小但考虑到峰值流量波动和 Redis 命令排队实际操作中我把max-active设成 32 才觉得稳。为什么不是 8因为线程池里线程数一般是 CPU 核数的若干倍大量线程短暂地同时访问 Redis 时连接池里连接数量不够就会出现获取连接等待从而拖慢接口响应。所以连接池参数一定要和服务的线程池配置联合起来看很多人只调线程池不调连接池接口一上量就卡死在RedisConnectionException: Unable to acquire connection上。5.2 监控与排查观察到底走的哪个数据源多数据源上线后运维监控这块也要跟上。我强烈建议在 RedisTemplate 的封装层或者切面里加一个轻量的日志或者埋点记录当前操作的数据源 key。这样排查问题时你拿着一条日志就知道某个业务操作到底落到了哪个 Redis 实例而不是对着代码猜。在实际项目里我在RoutingRedisTemplate的determineTemplate()方法里加了一个可选的采样日志平时日志级别是DEBUG需要排查问题的时候临时调到INFO或者通过arthas在线修改日志级别就能实时看到当前线程路由到了哪个数据源。这种做法实测下来非常有效尤其是排查那种“数据怎么跑到了另一个库”的诡异问题。另外针对 Lettuce 自带的连接池指标可以通过引入micrometer的 Redis 指标暴露能力把lettuce连接池的 active、idle、pending 等指标接进监控大盘。我维护的一套监控布局是每个数据源对应一组面板重点关注连接数是不是打满、命令执行延迟是否有周期性尖刺、以及连接池是否频繁出现创建销毁连接的抖动。这些指标比单看服务整体耗时能更早地发现多数据源之间的互相影响。5.3 性能调优上的一个被忽视的大坑key 空间设计多数据源好用是好用但也埋了一个隐患之前所有 Redis key 都放在同一个实例上key 前缀可以随便起最多注意一下规范。现在数据源一拆多个 Redis 实例之间没有任何隔离冲突了反而容易让人产生侥幸心理觉得“反正不在同一个 Redis 里key 重复问题不用担心了”。这个想法很危险。因为一旦未来某一天你把某个数据源从独立实例又合并回共享实例或者某个调用方错误地注入了别人的 RedisTemplatekey 前缀不一致就会导致互相覆盖。所以即使在多数据源架构下我依然坚持在每个数据源内部使用统一的前缀规范。比如用户数据源的 key 统一加user:前缀活动数据源的 key 统一加act:前缀。这样做还有一个好处在同一个 Redis 实例里直接按前缀就能区分不同业务的 key用redis-cli --scan --pattern user:*可以一键列出所有用户相关的缓存键排查和清理都方便很多。别把这个当成教条实际上它救了我好几次——有两次线上缓存数据异常我都是靠前缀非常快速地定位到某段脏数据然后精准删掉。6. 写在最后的体会与技巧折腾了这么多我把最核心的体会浓缩成几条给兄弟们做个参考。第一多 RedisTemplate 的核心价值在于“隔离”连接隔离、序列化隔离、连接池隔离。配置一个主数据源和多个辅助数据源时一定要保证每个模板从连接工厂到序列化配置都是独立的不要让它们共用任何可变状态。第二动态切换要谨慎使用 ThreadLocal。这个坑我已经说过很多次了但还是想强调一遍使用了线程池之后ThreadLocal 清理不仅是编码习惯问题而是一个必须通过框架机制比如拦截器 finally来保证的纪律。我的建议是哪怕你觉得某个方法不可能抛异常也把清理逻辑写在 finally 里。Redis 操作本身的失败率比你想象中高得多超时、熔断、连接池耗尽都有可能让你的方法提前结束。第三不要轻易用 Map 动态注入所有数据源。固定枚举出 2~4 个数据源的场景用显式Bean方式定义是最清晰的。如果数据源数量可能动态扩展至少也要保证连接工厂的配置是独立可控的而不是靠一份大 Map 和字符串 key 满天飞。代码可读性和可维护性在长期项目里比“看起来灵活”重要得多。最后再分享一个小技巧。如果你在项目里已经用了 Spring Data Redis 的缓存抽象也就是RedisCacheManager想要让多数据源方案覆盖到缓存注解可以在配置类里分别创建两个RedisCacheManagerBean并做好命名。我之前在某跨平台系统项目里就是把用户数据的缓存和活动数据的缓存分别绑定到各自的CacheManager上通过Cacheable(cacheManager activityCacheManager)指定使用哪个缓存管理器和DataSource(activity)搭配使用整体效果挺好。需要注意的是Cacheable本身并不感知数据源连接工厂它只感知CacheManager所以给缓存注解指定对的管理器是唯一正确的做法。其实多数据源这件事单从工作量上看并不复杂难的是把连接生命周期、动态路由、序列化策略、线程安全这些边边角角都照顾到。希望这篇分享能让你在动手之前就看清整条路上的关键节点少踩几个坑。
阅读完成 · 觉得有帮助?
咨询建站