ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot + Lettuce 实现 Redis 集群读写分离(从库优先读)

2026/9/1 15:10:06 拓冰建站 浏览量
Spring Boot + Lettuce 实现 Redis 集群读写分离(从库优先读) Spring Boot Lettuce 实现 Redis 集群读写分离从库优先读一、背景项目基于Spring Boot 2.1.5 Redis 集群 Lettuce 客户端所有读写默认都走主库ReadFrom MASTER。随着读流量增大主库压力上升需要把部分可容忍主从延迟的读分流到从节点同时保证写操作和强一致读仍走主库。目标很明确读多写少的场景如用户标签、菜单缓存读取走从库分布式锁、写后立即读等强一致场景仍走主库对现有代码侵入最小不改变既有方法的语义二、方案选型Lettuce 原生支持ReadFrom策略常见取值ReadFrom行为MASTER只读主库默认MASTER_PREFERRED优先主库主库不可用读从库SLAVE_PREFERRED/REPLICA_PREFERRED优先从库从库不可用回退主库SLAVE/REPLICA只读从库⚠️版本坑Spring Boot 2.1.5 自带 Lettuce5.1.x枚举名是SLAVE_PREFERREDLettuce5.2才更名为REPLICA_PREFERRED语义完全相同。直接照抄高版本代码会编译报错。核心思路是构建一个独立的副本LettuceConnectionFactory只把ReadFrom设为SLAVE_PREFERRED其余配置复用主库再基于它注册一组副本 RedisTemplate在工具类里提供带FromReplica后缀的专用读方法由调用方按需选择。这种新增方法、不动现有方法的做法好处是零风险、可灰度——只改需要分流的调用点其余代码一行不动。三、实现3.1 副本连接工厂 副本模板新建ReplicaRedisConfig关键点复用RedisProperties集群节点、密码、max-redirects、连接池、超时手动构建集群配置ReadFrom.SLAVE_PREFERRED优先从库注册与主库序列化器一致的副本模板保证读写数据兼容importio.lettuce.core.ReadFrom;importorg.apache.commons.pool2.impl.GenericObjectPoolConfig;importorg.springframework.boot.autoconfigure.data.redis.RedisProperties;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.connection.RedisClusterConfiguration;importorg.springframework.data.redis.connection.RedisPassword;importorg.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;importorg.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.serializer.RedisSerializer;importjava.time.Duration;/** * Redis 从读Replica Read配置 * * 副本连接工厂 ReadFrom SLAVE_PREFERRED优先从库从库不可用回退主库。 * 复用主库的集群拓扑/密码/连接池/超时仅 ReadFrom 不同。 * 注意副本模板仅用于读命令集群模式下写命令始终由主节点处理。 */ConfigurationpublicclassReplicaRedisConfig{BeanpublicLettuceConnectionFactoryreplicaConnectionFactory(RedisPropertiesredisProperties){if(redisProperties.getCluster()null||redisProperties.getCluster().getNodes()null||redisProperties.getCluster().getNodes().isEmpty()){thrownewIllegalStateException(ReplicaRedisConfig 需要集群配置 spring.redis.cluster.nodes当前未配置);}RedisClusterConfigurationclusterConfignewRedisClusterConfiguration();for(Stringnode:redisProperties.getCluster().getNodes()){String[]partsnode.split(:);clusterConfig.clusterNode(parts[0].trim(),Integer.parseInt(parts[1].trim()));}clusterConfig.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());if(redisProperties.getPassword()!null!redisProperties.getPassword().isEmpty()){clusterConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));}GenericObjectPoolConfig?poolConfignewGenericObjectPoolConfig();RedisProperties.PoolpoolredisProperties.getLettuce().getPool();if(pool!null){poolConfig.setMaxTotal(pool.getMaxActive());poolConfig.setMaxIdle(pool.getMaxIdle());poolConfig.setMinIdle(pool.getMinIdle());if(pool.getMaxWait()!null){poolConfig.setMaxWaitMillis(pool.getMaxWait().toMillis());}}LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilderbuilderLettucePoolingClientConfiguration.builder().readFrom(ReadFrom.SLAVE_PREFERRED)// 关键优先从库.poolConfig(poolConfig);DurationtimeoutredisProperties.getTimeout();if(timeout!null){builder.commandTimeout(timeout);}LettuceConnectionFactoryfactorynewLettuceConnectionFactory(clusterConfig,builder.build());factory.afterPropertiesSet();returnfactory;}/** 副本纯字符串序列化模板 */BeanpublicRedisTemplateString,ObjectcustomStringRedisTemplateReplica(LettuceConnectionFactoryreplicaConnectionFactory){RedisTemplateString,ObjecttemplatenewRedisTemplate();template.setConnectionFactory(replicaConnectionFactory);template.setKeySerializer(RedisSerializer.string());template.setHashKeySerializer(RedisSerializer.string());template.setValueSerializer(RedisSerializer.string());template.setHashValueSerializer(RedisSerializer.string());template.afterPropertiesSet();returntemplate;}// 其他副本模板gzip / ProtobufZstd序列化器与主库保持一致略}3.2 工具类新增 FromReplica 读方法在RedisUtils中注入副本模板用Qualifier按名注入避免与主库模板冲突新增一组后缀为FromReplica的读方法AutowiredQualifier(customStringRedisTemplateReplica)privateRedisTemplateString,ObjectcustomStringRedisTemplateReplica;AutowiredQualifier(gzipRedisTemplateReplica)privateRedisTemplateString,ObjectgzipRedisTemplateReplica;AutowiredQualifier(menuRedisTemplateReplica)privateRedisTemplateString,MapString,MenumenuRedisTemplateReplica;// 从读Replica Read方法 // 走副本连接工厂ReadFrom SLAVE_PREFERRED仅用于可容忍主从复制延迟的读场景。// 现有同名不带 FromReplica方法仍走主库语义不变。publicObjectgetStrFromReplica(Stringkey){returnkeynull?null:customStringRedisTemplateReplica.opsForValue().get(key);}publicObjecthgetStringFromReplica(Stringkey,Stringfield)throwsIOException{finalObjectobjectcustomStringRedisTemplateReplica.opsForHash().get(key,field);if(objectnull){log.debug(data is null);returnnull;}returnobject;}publicbooleanexistsFromReplica(Stringkey){returncustomStringRedisTemplateReplica.hasKey(key);}publicMapString,MenuloadCompressedMenuByProtobufZstdFromReplica(Stringkey){try{MapString,MenuvaluemenuRedisTemplateReplica.opsForValue().get(key);returnvaluenull?null:value;}catch(Exceptione){thrownewRuntimeException(Failed to load compressed menu from replica,e);}}3.3 调用方改造把原来调主库的地方换成xxxFromReplica即可一行改动// 改造前走主库ObjectobjectredisUtils.getStr(redisKey);// 改造后走从库优先ObjectobjectredisUtils.getStrFromReplica(redisKey);四、踩坑记录Lettuce 版本与枚举名SB 2.1.x 对应 Lettuce 5.1.x必须用ReadFrom.SLAVE_PREFERRED高版本的REPLICA_PREFERRED编译不过。副本模板只读不写集群模式下写命令SET/HSET 等始终路由到 slot owner主节点用副本模板写没有意义且易混淆应在规范上禁止。连接池配置要同步副本连接工厂是手动 new 出来的不会自动继承spring.redis.lettuce.pool.*必须显式读取RedisProperties里的 pool 配置套用否则会用默认池参数。afterPropertiesSet()别忘手动创建的LettuceConnectionFactory需要调用afterPropertiesSet()完成初始化否则启动时连接不上。主从延迟评估SLAVE_PREFERRED回退主库只在从节点不可达/报错时触发不会因延迟大而回退。对延迟敏感的读写后立即读、分布式锁判断必须用主库方法。五、总结整套方案的核心是双连接工厂 专用方法名主库连接工厂由 Spring Boot 自动装配保持MASTER所有现有方法零改动新建副本连接工厂SLAVE_PREFERRED注册配套副本模板工具类按需暴露FromReplica方法调用方显式选择优点是渐进式、可控、零回归风险想分流哪个调用点就改哪个出问题随时回退到主库方法不影响其他业务。