🚀 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 拓扑示意
┌─────────────┐│ redis-master││ 6379 │└─────┬───────┘│┌─────────┴─────────┐│ │
┌─────────────┐ ┌─────────────┐
│ redis-slave1│ │ redis-slave2│
│ 6380 │ │ 6381 │
└─────────────┘ └─────────────┘▲ ▲│ │
┌─────┴─────┐ ┌─────┴─────┐
│ sentinel1 │ │ sentinel2 │
│ 26379 │ │ 26380 │
└───────────┘ └───────────┘▲│┌───────────┐│ sentinel3 ││ 26381 │└───────────┘
2. Mermaid 彩色架构图
🔹 主节点(红色)、从节点(绿色)、哨兵(蓝色),箭头表示数据同步和监控方向。
📦 三、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 了。
❓ 七、为什么这样配置
- 哨兵模式:自动故障转移,保证高可用
- announce-ip 配置 IP:避免容器名解析问题,防止 UnknownHostException
- StringRedisTemplate:避免序列化异常,方便开发调试
- Docker Compose:快速搭建一主两从 + 三哨兵环境,便于测试
🏁 八、总结
-
Redis Sentinel + Spring Boot 可以轻松实现高可用读写
-
注意:
- 哨兵返回 IP 避免主机名解析问题
- 数据序列化需与存储类型匹配
-
本方案适合开发、测试和小型生产环境