基于SpringBoot + Lettuce实现Redis集群读写分离
作者:IT_Octopus
一、背景
项目基于 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 自带 Lettuce 5.1.x,枚举名是 SLAVE_PREFERRED;Lettuce 5.2+ 才更名为 REPLICA_PREFERRED,语义完全相同。直接照抄高版本代码会编译报错。
核心思路是:构建一个独立的副本 LettuceConnectionFactory,只把 ReadFrom 设为 SLAVE_PREFERRED,其余配置复用主库,再基于它注册一组"副本 RedisTemplate",在工具类里提供带 FromReplica 后缀的专用读方法,由调用方按需选择。
这种"新增方法、不动现有方法"的做法好处是:零 风险、可灰度——只改需要分流的调用点,其余代码一行不动。
三、实现
3.1 副本连接工厂 + 副本模板
新建 ReplicaRedisConfig,关键点:
- 复用
RedisProperties(集群节点、密码、max-redirects、连接池、超时)手动构建集群配置 ReadFrom.SLAVE_PREFERRED优先从库- 注册与主库序列化器一致的副本模板,保证读写数据兼容
import io.lettuce.core.ReadFrom;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.time.Duration;
/**
* Redis 从读(Replica Read)配置
*
* 副本连接工厂 ReadFrom = SLAVE_PREFERRED(优先从库,从库不可用回退主库)。
* 复用主库的集群拓扑/密码/连接池/超时,仅 ReadFrom 不同。
* 注意:副本模板仅用于读命令;集群模式下写命令始终由主节点处理。
*/
@Configuration
public class ReplicaRedisConfig {
@Bean
public LettuceConnectionFactory replicaConnectionFactory(RedisProperties redisProperties) {
if (redisProperties.getCluster() == null
|| redisProperties.getCluster().getNodes() == null
|| redisProperties.getCluster().getNodes().isEmpty()) {
throw new IllegalStateException(
"ReplicaRedisConfig 需要集群配置 spring.redis.cluster.nodes,当前未配置");
}
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
for (String node : redisProperties.getCluster().getNodes()) {
String[] parts = node.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<?> poolConfig = new GenericObjectPoolConfig<>();
RedisProperties.Pool pool = redisProperties.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.LettucePoolingClientConfigurationBuilder builder =
LettucePoolingClientConfiguration.builder()
.readFrom(ReadFrom.SLAVE_PREFERRED) // 关键:优先从库
.poolConfig(poolConfig);
Duration timeout = redisProperties.getTimeout();
if (timeout != null) {
builder.commandTimeout(timeout);
}
LettuceConnectionFactory factory = new LettuceConnectionFactory(clusterConfig, builder.build());
factory.afterPropertiesSet();
return factory;
}
/** 副本纯字符串序列化模板 */
@Bean
public RedisTemplate<String, Object> customStringRedisTemplateReplica(
LettuceConnectionFactory replicaConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(replicaConnectionFactory);
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
template.setValueSerializer(RedisSerializer.string());
template.setHashValueSerializer(RedisSerializer.string());
template.afterPropertiesSet();
return template;
}
// 其他副本模板(gzip / Protobuf+Zstd)序列化器与主库保持一致,略
}
3.2 工具类新增 FromReplica 读方法
在 RedisUtils 中注入副本模板(用 @Qualifier 按名注入,避免与主库模板冲突),新增一组后缀为 FromReplica 的读方法:
@Autowired
@Qualifier("customStringRedisTemplateReplica")
private RedisTemplate<String, Object> customStringRedisTemplateReplica;
@Autowired
@Qualifier("gzipRedisTemplateReplica")
private RedisTemplate<String, Object> gzipRedisTemplateReplica;
@Autowired
@Qualifier("menuRedisTemplateReplica")
private RedisTemplate<String, Map<String, Menu>> menuRedisTemplateReplica;
// ==================== 从读(Replica Read)方法 ====================
// 走副本连接工厂(ReadFrom = SLAVE_PREFERRED),仅用于可容忍主从复制延迟的读场景。
// 现有同名(不带 FromReplica)方法仍走主库,语义不变。
public Object getStrFromReplica(String key) {
return key == null ? null : customStringRedisTemplateReplica.opsForValue().get(key);
}
public Object hgetStringFromReplica(String key, String field) throws IOException {
final Object object = customStringRedisTemplateReplica.opsForHash().get(key, field);
if (object == null) {
log.debug("data is null");
return null;
}
return object;
}
public boolean existsFromReplica(String key) {
return customStringRedisTemplateReplica.hasKey(key);
}
public Map<String, Menu> loadCompressedMenuByProtobufZstdFromReplica(String key) {
try {
Map<String, Menu> value = menuRedisTemplateReplica.opsForValue().get(key);
return value == null ? null : value;
} catch (Exception e) {
throw new RuntimeException("Failed to load compressed menu from replica", e);
}
}
3.3 调用方改造
把原来调主库的地方换成 xxxFromReplica 即可,一行改动:
// 改造前:走主库 Object object = redisUtils.getStr(redisKey); // 改造后:走从库优先 Object object = redisUtils.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方法,调用方显式选择
优点是渐进式、可控、零回归风险:想分流哪个调用点就改哪个,出问题随时回退到主库方法,不影响其他业务。
以上就是基于SpringBoot + Lettuce实现Redis集群读写分离的详细内容,更多关于SpringBoot Lettuce Redis集群读写分离的资料请关注脚本之家其它相关文章!
