🚀 Spring Boot + Redis Sentinel 完整测试案例
🏷️ 标签:Redis 、Redis Sentinel、Spring Boot 实战
📚 目录导航
- 📝 前言
- 🏗️ Redis Sentinel 架构说明
- 📦 Docker Compose 搭建 Redis 哨兵环境
- ⚙️ Spring Boot 配置
- 📌 Maven 依赖
- 📝 application.yml 配置
- 🔧 Redis 配置类
- 🧪 测试 Controller
- 🚀 运行测试
- ❓ 为什么这样配置
- 🏁 总结
📝 一、前言
在生产环境中,Redis 通常部署为 一主多从 + 哨兵(Sentinel) 架构,以保证高可用性和数据安全性。
使用 Spring Boot 连接 Redis 哨兵时,开发者可能会遇到以下问题:
- 哨兵返回主节点名称(如
redis-master
)无法被客户端解析 - 数据序列化和反序列化不一致导致
StreamCorruptedException
本文演示如何通过 Docker Compose 搭建 Redis 哨兵环境,并使用 Spring Boot 完成数据写入和读取操作。
🏗️ 二、Redis Sentinel 架构说明
1. ASCII 拓扑示意
text
┌─────────────┐
│ 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 哨兵环境
⚙️ 四、Spring Boot 配置
📌 1. Maven 依赖
xml
<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 配置
yaml
spring:
data:
redis:
sentinel:
nodes:
- 192.168.3.150:26379
- 192.168.3.150:26380
- 192.168.3.150:26381
master: mymaster
timeout: 3000ms
lettuce:
shutdown-timeout: 100ms
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1
logging:
level:
io.lettuce.core: DEBUG
org.springframework.data.redis: DEBUG
🔧 3. Redis 配置类
java
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 {
@Bean
public 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;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
return new StringRedisTemplate(factory);
}
}
使用
StringRedisTemplate
避免 Java 默认序列化问题。
🧪 五、测试 Controller
java
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 Bean
public 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:
bash
docker compose up -d
2. 启动 Spring Boot 应用
3. 测试写入:
bash
curl "http://localhost:9090/redis/set?key=test&value=HelloRedis"

4. 测试读取:
bash
curl "http://localhost:9090/redis/get?key=test"

5. 验证主从同步:
bash
docker exec -it redis-slave1 redis-cli GET test
docker exec -it redis-slave2 redis-cli GET test

数据应在主从节点一致。
6.可能出现的问题:
bash
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 了。
❓ 七、为什么这样配置
- 哨兵模式:自动故障转移,保证高可用
- announce-ip 配置 IP:避免容器名解析问题,防止 UnknownHostException
- StringRedisTemplate:避免序列化异常,方便开发调试
- Docker Compose:快速搭建一主两从 + 三哨兵环境,便于测试
🏁 八、总结
-
Redis Sentinel + Spring Boot 可以轻松实现高可用读写
-
注意:
- 哨兵返回 IP 避免主机名解析问题
- 数据序列化需与存储类型匹配
-
本方案适合开发、测试和小型生产环境