RedisTemplate配置读写分离

一、前言:为什么需要 Redis 读写分离?

在高并发场景下,所有请求都打到 Redis 主节点,容易导致:

  • ❌ 主节点 CPU/网络带宽打满
  • ❌ 写操作阻塞读请求(虽然 Redis 单线程,但网络和客户端处理有开销)
  • ❌ 无法利用从节点的读扩展能力

而 Redis 主从架构天然支持读写分离

  • 写操作 → 主节点
  • 读操作 → 从节点

但 Spring Boot 默认的 RedisTemplate 所有操作都走主节点

本文将教你如何通过 Lettuce 客户端 + 自定义配置 ,实现 RedisTemplate透明读写分离


二、技术选型:为什么用 Lettuce?

客户端 是否支持读写分离 线程模型 Spring Boot 默认
Jedis ❌(需手动管理连接池) 多线程(非线程安全) 否(2.x+ 默认 Lettuce)
Lettuce ✅(内置主从/哨兵读写分离) 单连接多路复用(线程安全)

Lettuce 优势

  • 原生支持 READONLY 模式
  • 与 Spring Data Redis 深度集成
  • 连接资源占用低

三、前提条件

  1. 已搭建 Redis 主从集群 (1 主 + N 从)

    bash 复制代码
    # 示例:主 6379,从 6380
    redis-server --port 6379
    redis-server --port 6380 --replicaof 127.0.0.1 6379
  2. Spring Boot 2.7+(默认使用 Lettuce)

  3. 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:从节点数据延迟导致读到旧值

  • 场景:刚写入主,立即读从 → 可能读不到
  • 解决方案
    1. 业务容忍短暂不一致(最终一致性)

    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;

✅ 最佳实践

  • 普通查询 → 读从
  • 强一致性读(如支付后查余额)→ 读主
  • 监控从节点 lagINFO REPLICATIONslave0:lag=0

九、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
冰暮流星2 小时前
javascript如何实现删除数组里面的重复元素
开发语言·前端·javascript
网络点点滴3 小时前
透传属性$attrs
前端·javascript·vue.js
90后的晨仔3 小时前
OpenClaw macOS 完整安装指南
前端
Moment3 小时前
尤雨溪宣布 Vite+ 正式开源,前端工具链要大一统了
前端·javascript·面试
sunny_4 小时前
📖 2026年 大厂前端面试手写题库已开源(2.3k star)
前端·面试·github
IT_陈寒4 小时前
Vue组件复用率提升300%?这5个高阶技巧让你的代码焕然一新!
前端·人工智能·后端
We་ct4 小时前
LeetCode 79. 单词搜索:DFS回溯解法详解
前端·算法·leetcode·typescript·深度优先·个人开发·回溯
阿梦Anmory4 小时前
Redis配置远程访问(绑定0.0.0.0):从配置到安全实战
redis·安全·bootstrap
小奶包他干奶奶4 小时前
将svg对象化,并动态修改svg图标的颜色、尺寸等
前端·html