一、前言:为什么需要 Redis 读写分离?
在高并发场景下,所有请求都打到 Redis 主节点,容易导致:
- ❌ 主节点 CPU/网络带宽打满
- ❌ 写操作阻塞读请求(虽然 Redis 单线程,但网络和客户端处理有开销)
- ❌ 无法利用从节点的读扩展能力
而 Redis 主从架构天然支持读写分离:
- ✅ 写操作 → 主节点
- ✅ 读操作 → 从节点
但 Spring Boot 默认的 RedisTemplate 所有操作都走主节点!
本文将教你如何通过 Lettuce 客户端 + 自定义配置 ,实现 RedisTemplate 的透明读写分离。
二、技术选型:为什么用 Lettuce?
| 客户端 | 是否支持读写分离 | 线程模型 | Spring Boot 默认 |
|---|---|---|---|
| Jedis | ❌(需手动管理连接池) | 多线程(非线程安全) | 否(2.x+ 默认 Lettuce) |
| Lettuce | ✅(内置主从/哨兵读写分离) | 单连接多路复用(线程安全) | 是 |
✅ Lettuce 优势:
- 原生支持
READONLY模式- 与 Spring Data Redis 深度集成
- 连接资源占用低
三、前提条件
-
已搭建 Redis 主从集群 (1 主 + N 从)
bash# 示例:主 6379,从 6380 redis-server --port 6379 redis-server --port 6380 --replicaof 127.0.0.1 6379 -
Spring Boot 2.7+(默认使用 Lettuce)
-
Maven 依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactityId>spring-boot-starter-data-redis</artifactId> </dependency>
四、核心原理:Lettuce 的读模式(ReadFrom)
Lettuce 提供 ReadFrom 枚举,控制读请求路由策略:
| 模式 | 行为 |
|---|---|
MASTER |
所有读写都走主(默认) |
MASTER_PREFERRED |
优先读主,主不可用时读从 |
SLAVE |
只读从节点(适合纯读场景) |
SLAVE_PREFERRED |
优先读从,从不可用时读主(推荐!) |
NEAREST |
读最近节点(延迟最低) |
🔑 我们选择
SLAVE_PREFERRED:写走主,读优先走从,兼顾性能与容错。
五、配置步骤:自定义 RedisConnectionFactory
5.1 创建配置类
java
@Configuration
public class RedisReplicaConfig {
@Value("${spring.redis.host:localhost}")
private String host;
@Value("${spring.redis.port:6379}")
private int port;
@Value("${spring.redis.password:}")
private String password;
@Bean
@Primary // 标记为主 RedisTemplate
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
// 使用 Jackson 序列化(避免乱码)
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
serializer.setObjectMapper(mapper);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
/**
* 配置支持读写分离的 Lettuce 连接工厂
*/
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
// 1. 创建 RedisStandaloneConfiguration(单机主从)
RedisStandaloneConfiguration config =
new RedisStandaloneConfiguration(host, port);
if (StringUtils.hasText(password)) {
config.setPassword(RedisPassword.of(password));
}
// 2. 配置 LettuceClientConfiguration
LettuceClientConfiguration clientConfig =
LettuceClientConfiguration.builder()
.readFrom(ReadFrom.SLAVE_PREFERRED) // 👈 关键!启用读写分离
.commandTimeout(Duration.ofSeconds(10))
.build();
return new LettuceConnectionFactory(config, clientConfig);
}
}
✅ 关键代码 :
.readFrom(ReadFrom.SLAVE_PREFERRED)
六、验证读写分离是否生效
6.1 编写测试 Controller
java
@RestController
public class RedisTestController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/write")
public String write(@RequestParam String key, @RequestParam String value) {
redisTemplate.opsForValue().set(key, value); // 写操作 → 主节点
return "Write OK";
}
@GetMapping("/read")
public Object read(@RequestParam String key) {
return redisTemplate.opsForValue().get(key); // 读操作 → 优先从节点
}
}
6.2 监控 Redis 节点命令
主节点(6379):
bash
# 监控命令
redis-cli -p 6379 MONITOR
→ 应看到 SET 命令,几乎看不到 GET
从节点(6380):
bash
redis-cli -p 6380 MONITOR
→ 应看到大量 GET 命令
✅ 效果:写入走主,读取走从,读写分离成功!
七、进阶:哨兵(Sentinel)或集群(Cluster)模式
7.1 哨兵模式配置
java
@Bean
public LettuceConnectionFactory sentinelConnectionFactory() {
RedisSentinelConfiguration sentinelConfig =
new RedisSentinelConfiguration()
.master("mymaster")
.sentinel("192.168.1.10", 26379)
.sentinel("192.168.1.11", 26379)
.sentinel("192.168.1.12", 26379);
if (StringUtils.hasText(password)) {
sentinelConfig.setPassword(RedisPassword.of(password));
}
LettuceClientConfiguration clientConfig =
LettuceClientConfiguration.builder()
.readFrom(ReadFrom.SLAVE_PREFERRED)
.build();
return new LettuceConnectionFactory(sentinelConfig, clientConfig);
}
7.2 Redis Cluster 模式
⚠️ 注意 :Cluster 模式不支持传统主从读写分离 !
因为 Cluster 本身已分片,每个 master-slave 对负责不同 slot。
若需读从,需显式设置
ReadFrom,但需确保 key 路由正确。
八、常见问题与避坑指南
❌ 问题 1:读操作仍走主节点
- 原因 :未配置
readFrom,或配置了但使用了事务/脚本 - 注意 :以下操作强制走主 (Lettuce 限制):
MULTI/EXEC事务- Lua 脚本(
EVAL) - 写命令(如
SET,HSET)
❌ 问题 2:从节点数据延迟导致读到旧值
- 场景:刚写入主,立即读从 → 可能读不到
- 解决方案 :
-
业务容忍短暂不一致(最终一致性)
-
关键读走主 :提供两个
RedisTemplate(一个读写,一个只读)java@Bean("readOnlyRedisTemplate") public RedisTemplate<String, Object> readOnlyRedisTemplate() { // ... same as above, but ReadFrom.SLAVE }使用时显式指定:
java@Autowired @Qualifier("readOnlyRedisTemplate") private RedisTemplate<String, Object> readOnlyRedisTemplate;
-
✅ 最佳实践
- 普通查询 → 读从
- 强一致性读(如支付后查余额)→ 读主
- 监控从节点 lag (
INFO REPLICATION中slave0:lag=0)
九、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!