当前位置: 首页 > news >正文

Spring Boot + Redis Sentinel (一主两从)测试案例

🚀 Spring Boot + Redis Sentinel 完整测试案例

🏷️ 标签:Redis 、Redis Sentinel、Spring Boot 实战


📚 目录导航

  1. 📝 前言
  2. 🏗️ Redis Sentinel 架构说明
  3. 📦 Docker Compose 搭建 Redis 哨兵环境
  4. ⚙️ Spring Boot 配置
    • 📌 Maven 依赖
    • 📝 application.yml 配置
    • 🔧 Redis 配置类
  5. 🧪 测试 Controller
  6. 🚀 运行测试
  7. 为什么这样配置
  8. 🏁 总结

📝 一、前言

在生产环境中,Redis 通常部署为 一主多从 + 哨兵(Sentinel) 架构,以保证高可用性和数据安全性。
使用 Spring Boot 连接 Redis 哨兵时,开发者可能会遇到以下问题:

  • 哨兵返回主节点名称(如 redis-master)无法被客户端解析
  • 数据序列化和反序列化不一致导致 StreamCorruptedException

本文演示如何通过 Docker Compose 搭建 Redis 哨兵环境,并使用 Spring Boot 完成数据写入和读取操作。


🏗️ 二、Redis Sentinel 架构说明

1. ASCII 拓扑示意

          ┌─────────────┐│ redis-master││    6379     │└─────┬───────┘│┌─────────┴─────────┐│                   │
┌─────────────┐     ┌─────────────┐
│ redis-slave1│     │ redis-slave2│
│    6380     │     │    6381     │
└─────────────┘     └─────────────┘▲                   ▲│                   │
┌─────┴─────┐       ┌─────┴─────┐
│ sentinel1 │       │ sentinel2 │
│  26379    │       │  26380    │
└───────────┘       └───────────┘▲│┌───────────┐│ sentinel3 ││  26381    │└───────────┘

2. Mermaid 彩色架构图

redis-master:6379
redis-slave1:6380
redis-slave2:6381
sentinel1:26379
sentinel2:26380
sentinel3:26381

🔹 主节点(红色)、从节点(绿色)、哨兵(蓝色),箭头表示数据同步和监控方向。


📦 三、Docker Compose 搭建 Redis 哨兵环境

Docker Compose 搭建Redis哨兵


⚙️ 四、Spring Boot 配置

📌 1. Maven 依赖

    <dependencies><!-- Web 模块 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Data Redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Jackson --><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>

📝 2. application.yml 配置

spring:data:redis:sentinel:nodes:- 192.168.3.150:26379- 192.168.3.150:26380- 192.168.3.150:26381master: mymastertimeout: 3000mslettuce:shutdown-timeout: 100mspool:max-active: 8max-idle: 8min-idle: 0max-wait: -1
logging:level:io.lettuce.core: DEBUGorg.springframework.data.redis: DEBUG

🔧 3. Redis 配置类

package com.example.demo.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(factory);template.setKeySerializer(template.getStringSerializer());template.setHashKeySerializer(template.getStringSerializer());template.afterPropertiesSet();return template;}@Beanpublic StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {return new StringRedisTemplate(factory);}
}

使用 StringRedisTemplate 避免 Java 默认序列化问题。


🧪 五、测试 Controller

package com.example.demo.controller;import com.example.demo.service.RedisService;
import org.springframework.web.bind.annotation.*;import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;@RestController
@RequestMapping("/redis")
public class RedisController {private final RedisService redisService;// 构造函数注入,Spring 会自动注入 RedisService Beanpublic RedisController(RedisService redisService) {this.redisService = redisService;}// String 操作示例@PostMapping("/string/set")public String setString(@RequestParam String key, @RequestParam String value) {redisService.set(key, value, 60L, TimeUnit.SECONDS);return "String set successfully";}// String获取Key@GetMapping("/string/get")public Object getString(@RequestParam String key) {return redisService.get(key);}
}

🚀 六、运行测试

1. 启动 Docker Compose:

docker compose up -d

2. 启动 Spring Boot 应用

3. 测试写入:

curl "http://localhost:9090/redis/set?key=test&value=HelloRedis"

在这里插入图片描述

4. 测试读取:

curl "http://localhost:9090/redis/get?key=test"

在这里插入图片描述

5. 验证主从同步:

docker exec -it redis-slave1 redis-cli GET test
docker exec -it redis-slave2 redis-cli GET test

在这里插入图片描述

数据应在主从节点一致。

6.可能出现的问题:

2025-08-14T15:51:45.540+08:00 DEBUG 9436 --- [Spring-Redis-Sentinel] [ioEventLoop-4-1] i.lettuce.core.protocol.DefaultEndpoint  : [channel=0xec254466, /192.168.3.36:49466 -> /192.168.3.150:26379, epid=0x2] closeAsync()
2025-08-14T15:51:45.543+08:00 DEBUG 9436 --- [Spring-Redis-Sentinel] [ioEventLoop-4-1] io.lettuce.core.RedisClient              : Resolved SocketAddress redis-master/<unresolved>:6379 using redis-sentinel://192.168.3.150,192.168.3.150:26380,192.168.3.150:26381?sentinelMasterId=mymaster&timeout=3s
2025-08-14T15:51:45.543+08:00 DEBUG 9436 --- [Spring-Redis-Sentinel] [ioEventLoop-4-1] io.lettuce.core.AbstractRedisClient      : Connecting to Redis at redis-master/<unresolved>:6379
2025-08-14T15:51:45.545+08:00 DEBUG 9436 --- [Spring-Redis-Sentinel] [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler   : [channel=0xec254466, /192.168.3.36:49466 -> /192.168.3.150:26379, epid=0x2, chid=0x1] channelInactive()
2025-08-14T15:51:45.546+08:00 DEBUG 9436 --- [Spring-Redis-Sentinel] [ioEventLoop-4-1] i.lettuce.core.protocol.DefaultEndpoint  : [channel=0xec254466, /192.168.3.36:49466 -> /192.168.3.150:26379, epid=0x2] deactivating endpoint handler
2025-08-14T15:51:45.546+08:00 DEBUG 9436 --- [Spring-Redis-Sentinel] [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler   : [channel=0xec254466, /192.168.3.36:49466 -> /192.168.3.150:26379, epid=0x2, chid=0x1] channelInactive() done
2025-08-14T15:51:45.547+08:00 DEBUG 9436 --- [Spring-Redis-Sentinel] [ioEventLoop-4-1] i.l.core.protocol.ConnectionWatchdog     : [channel=0xec254466, /192.168.3.36:49466 -> /192.168.3.150:26379, last known addr=/192.168.3.150:26379] channelInactive()
2025-08-14T15:51:45.547+08:00 DEBUG 9436 --- [Spring-Redis-Sentinel] [ioEventLoop-4-1] i.l.core.protocol.ConnectionWatchdog     : [channel=0xec254466, /192.168.3.36:49466 -> /192.168.3.150:26379, last known addr=/192.168.3.150:26379] Reconnect scheduling disabled
2025-08-14T15:51:45.547+08:00 DEBUG 9436 --- [Spring-Redis-Sentinel] [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler   : [channel=0xec254466, /192.168.3.36:49466 -> /192.168.3.150:26379, epid=0x2, chid=0x1] channelUnregistered()
2025-08-14T15:51:47.799+08:00 DEBUG 9436 --- [Spring-Redis-Sentinel] [ioEventLoop-4-2] io.lettuce.core.AbstractRedisClient      : Connecting to Redis at redis-master/<unresolved>:6379: {}java.net.UnknownHostException: 不知道这样的主机。 (redis-master)at java.base/java.net.Inet6AddressImpl.lookupAllHostAddr(Native Method) ~[na:na]at java.base/java.net.InetAddress$PlatformNameService.lookupAllHostAddr(InetAddress.java:933) ~[na:na]at java.base/java.net.InetAddress.getAddressesFromNameService(InetAddress.java:1543) ~[na:na]at java.base/java.net.InetAddress$NameServiceAddresses.get(InetAddress.java:852) ~[na:na]at java.base/java.net.InetAddress.getAllByName0(InetAddress.java:1532) ~[na:na]at java.base/java.net.InetAddress.getAllByName(InetAddress.java:1384) ~[na:na]at java.base/java.net.InetAddress.getAllByName(InetAddress.java:1305) ~[na:na]at java.base/java.net.InetAddress.getByName(InetAddress.java:1255) ~[na:na]at io.netty.util.internal.SocketUtils$8.run(SocketUtils.java:156) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at io.netty.util.internal.SocketUtils$8.run(SocketUtils.java:153) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at java.base/java.security.AccessController.doPrivileged(AccessController.java:569) ~[na:na]at io.netty.util.internal.SocketUtils.addressByName(SocketUtils.java:153) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at io.netty.resolver.DefaultNameResolver.doResolve(DefaultNameResolver.java:41) ~[netty-resolver-4.1.123.Final.jar:4.1.123.Final]at io.netty.resolver.SimpleNameResolver.resolve(SimpleNameResolver.java:61) ~[netty-resolver-4.1.123.Final.jar:4.1.123.Final]at io.netty.resolver.SimpleNameResolver.resolve(SimpleNameResolver.java:53) ~[netty-resolver-4.1.123.Final.jar:4.1.123.Final]at io.netty.resolver.InetSocketAddressResolver.doResolve(InetSocketAddressResolver.java:55) ~[netty-resolver-4.1.123.Final.jar:4.1.123.Final]at io.netty.resolver.InetSocketAddressResolver.doResolve(InetSocketAddressResolver.java:31) ~[netty-resolver-4.1.123.Final.jar:4.1.123.Final]at io.netty.resolver.AbstractAddressResolver.resolve(AbstractAddressResolver.java:106) ~[netty-resolver-4.1.123.Final.jar:4.1.123.Final]at io.netty.bootstrap.Bootstrap.doResolveAndConnect0(Bootstrap.java:220) ~[netty-transport-4.1.123.Final.jar:4.1.123.Final]at io.netty.bootstrap.Bootstrap.access$000(Bootstrap.java:47) ~[netty-transport-4.1.123.Final.jar:4.1.123.Final]at io.netty.bootstrap.Bootstrap$1.operationComplete(Bootstrap.java:189) ~[netty-transport-4.1.123.Final.jar:4.1.123.Final]at io.netty.bootstrap.Bootstrap$1.operationComplete(Bootstrap.java:175) ~[netty-transport-4.1.123.Final.jar:4.1.123.Final]at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:603) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:570) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:505) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at io.netty.util.concurrent.DefaultPromise.setValue0(DefaultPromise.java:649) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at io.netty.util.concurrent.DefaultPromise.setSuccess0(DefaultPromise.java:638) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at io.netty.util.concurrent.DefaultPromise.trySuccess(DefaultPromise.java:118) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at io.netty.channel.DefaultChannelPromise.trySuccess(DefaultChannelPromise.java:84) ~[netty-transport-4.1.123.Final.jar:4.1.123.Final]at io.netty.channel.AbstractChannel$AbstractUnsafe.safeSetSuccess(AbstractChannel.java:988) ~[netty-transport-4.1.123.Final.jar:4.1.123.Final]at io.netty.channel.AbstractChannel$AbstractUnsafe.register0(AbstractChannel.java:515) ~[netty-transport-4.1.123.Final.jar:4.1.123.Final]at io.netty.channel.AbstractChannel$AbstractUnsafe.access$200(AbstractChannel.java:428) ~[netty-transport-4.1.123.Final.jar:4.1.123.Final]at io.netty.channel.AbstractChannel$AbstractUnsafe$1.run(AbstractChannel.java:485) ~[netty-transport-4.1.123.Final.jar:4.1.123.Final]at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:173) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:166) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:569) ~[netty-transport-4.1.123.Final.jar:4.1.123.Final]at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:998) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.123.Final.jar:4.1.123.Final]at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na]
1. 为什么会这样

Docker 内部可以通过容器名 redis-master 互相访问(因为有自定义网络和 DNS)。

但是你的 Spring Boot 是在宿主机运行(不是在 Docker 内部),宿主机默认并不认识 redis-master 这个名字。

哨兵返回的主节点地址是它内部配置的 redis-master(来自 sentinel.conf 或 docker-compose 服务名),但宿主机解析不了。

2. 解决方案

在SpringBoot 主机 C:\Windows\System32\drivers\etc 加映射

如果 redis-master 容器的 IP 是 192.168.3.150(或者你用的是桥接 IP):

在这里插入图片描述
这样宿主机就能解析 redis-master 了。


❓ 七、为什么这样配置

  1. 哨兵模式:自动故障转移,保证高可用
  2. announce-ip 配置 IP:避免容器名解析问题,防止 UnknownHostException
  3. StringRedisTemplate:避免序列化异常,方便开发调试
  4. Docker Compose:快速搭建一主两从 + 三哨兵环境,便于测试

🏁 八、总结

  • Redis Sentinel + Spring Boot 可以轻松实现高可用读写

  • 注意:

    • 哨兵返回 IP 避免主机名解析问题
    • 数据序列化需与存储类型匹配
  • 本方案适合开发、测试和小型生产环境

http://www.dtcms.com/a/331544.html

相关文章:

  • Docker pull拉取镜像命令的入门教程
  • 蓝耘元生代上线 Baichuan-M2-32B,医疗推理能力卓越,高效部署,成本超低
  • Varjo XR虚拟现实军用车辆驾驶与操作培训
  • SaltStack部署应用
  • MCP入门:Python开发者的模型上下文协议实战指南
  • VSCode打开新的文件夹之后当前打开的文件夹被覆盖
  • 百度智能云x中科大脑:「城市智能体」如何让城市更会思考
  • 如何启动本机mysql数据库
  • 软件架构需求过程:构建高质量系统的基石
  • C#面试题及详细答案120道(01-10)-- 基础语法与数据类型
  • 【Android】RecyclerView复用CheckBox的异常状态
  • 容器方式安装Prometheus以及Grafana
  • 《疯狂Java讲义(第3版)》学习笔记ch4
  • C# 贪吃蛇游戏
  • js加密逆向
  • Chrome插件开发实战:从零开发高效Chrome插件,提升浏览器生产力
  • 通过 USB 配置闭环驱动器——易格斯igus
  • glTF-教程/glb-教程
  • tlias智能学习辅助系统--Maven 高级-私服介绍与资源上传下载
  • AI硬件小众赛道崛起:垂直场景的价值重构与增长密码。
  • Java高级流
  • 公链开发竞争白热化:如何设计下一代高性能、可扩展的区块链基础设施?
  • 云手机的存储功能怎么样?
  • 一次 Unity ↔ Android 基于 RSA‑OAEP 的互通踩坑记
  • Android ADB 常用指令全解析
  • ADB服务端调试
  • markdown格式中table表格不生效,没有编译的原因
  • Mybatis Plus 分页插件报错`GOLDILOCKS`
  • 视频号主页的企业信息如何设置?
  • 深入了解linux系统—— 线程概念