🚀 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 避免主機名解析問題
- 數據序列化需與存儲類型匹配
-
本方案適合開發、測試和小型生產環境