SpringBoot 2.x 整合 Redis Sentinel 集群
“?SpringBoot 2.x 整合 Redis Sentinel 集群實現(xiàn)自動故障轉(zhuǎn)移.”
上一篇我們講解了 Redis Sentinel 集群搭建的詳細過程, 作為一個 Java 程序員, 我們還是需要從代碼層面來操作 Redis 的.

1. 環(huán)境準(zhǔn)備
192.168.3.26: Master 實例, Sentinel 1192.168.3.27: Slave1 實例, Sentinel 2192.168.3.28:Slave2 實例, Sentinel 3
1.1 Redis Sentinel 集群(一主二從三個 Sentinel)

Redis Sentinel 集群的搭建過程可以參考帥帥之前的文章 Redis 高可用專題-- Sentinel 哨兵模式
注意: 目前的 master 是
192.168.3.27, 即經(jīng)過故障轉(zhuǎn)移后slave1目前是 Master
1.2 SpringBoot 環(huán)境
SpringBoot 版本: 2.3.2.RELEASE
2. 引入依賴
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
SpringBoot 2.x 的 Redis 客戶端已經(jīng)從
Jedis切換到了Lettuce
3. 配置
在
application.yml配置文件中編輯 Sentinel 信息
spring:
application:
name: sb-redis-sentinel
# redis 配置
redis:
database: 1
# 數(shù)據(jù)節(jié)點密碼
password: javaFamily123!!
sentinel:
master: mymaster
# Sentinel 連接密碼, 如果沒配置就不寫
# password:
nodes:
- 192.168.3.26:26379
- 192.168.3.27:26379
- 192.168.3.28:26379
# 配置 lettuce 客戶端連接池信息
lettuce:
pool:
# 連接池中的最小空閑連接
min-idle: 5
# 連接池中的最大空閑連接
max-idle: 10
# 連接池的最大數(shù)據(jù)庫連接數(shù)
max-active: 100
# 連接池最大阻塞等待時間(使用負值表示沒有限制)
max-wait: -1這里只需要配置 Sentinel 節(jié)點.?客戶端會自動從 sentinel 獲取數(shù)據(jù)節(jié)點信息.
配置 Redis 連接池、RedisTemplate
package club.javafamily.sbredissentinel.conf;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.lettuce.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.ArrayList;
import java.util.List;
/**
* @author Jack Li
* @date 2022/5/8 下午6:42
* @description Redis 哨兵配置
*/
@Configuration
public class RedisConfig {
private final RedisProperties redisProperties;
public RedisConfig(RedisProperties redisProperties) {
this.redisProperties = redisProperties;
}
/**
* 創(chuàng)建 Redis 連接池配置
*/
@Bean
public GenericObjectPoolConfig poolConfig() {
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle());
config.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle());
config.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive());
config.setMaxWaitMillis(redisProperties.getLettuce().getPool().getMaxWait().toMillis());
return config;
}
/**
* 聲明哨兵配置, 將哨兵信息放到配置中
*/
@Bean
public RedisSentinelConfiguration sentinelConfig() {
RedisSentinelConfiguration redisConfig = new RedisSentinelConfiguration();
redisConfig.setMaster(redisProperties.getSentinel().getMaster());
redisConfig.setPassword(redisProperties.getPassword());
if(redisProperties.getSentinel().getNodes()!=null) {
List sentinelNode = new ArrayList();
for(String sen : redisProperties.getSentinel().getNodes()) {
String[] arr = sen.split(":");
sentinelNode.add(new RedisNode(arr[0],Integer.parseInt(arr[1])));
}
redisConfig.setSentinels(sentinelNode);
}
return redisConfig;
}
/**
* 創(chuàng)建連接工廠 LettuceConnectionFactory
*/
@Bean("lettuceConnectionFactory")
public LettuceConnectionFactory lettuceConnectionFactory(
@Qualifier("poolConfig") GenericObjectPoolConfig config,
@Qualifier("sentinelConfig") RedisSentinelConfiguration sentinelConfig)
{
LettuceClientConfiguration clientConfiguration
= LettucePoolingClientConfiguration.builder()
.poolConfig(config)
.build();
return new LettuceConnectionFactory(sentinelConfig, clientConfiguration);
}
/**
* 創(chuàng)建 RedisTemplate
*/
@Bean("stringObjectRedisTemplate")
public RedisTemplate stringObjectRedisTemplate(
@Qualifier("lettuceConnectionFactory") LettuceConnectionFactory connectionFactory)
{
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer
= new GenericJackson2JsonRedisSerializer();
//設(shè)置序列化器
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
} 4. 正常測試
讀寫測試
package club.javafamily.sbredissentinel;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
@SpringBootTest
class SbRedisSentinelApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
ValueOperations opsForValue;
@BeforeEach
public void init() {
opsForValue = redisTemplate.opsForValue();
}
@Test
void contextLoads() {
Assertions.assertNotNull(redisTemplate);
}
@Test
void writeTest() {
opsForValue.set("tsb1", "springboot");
}
@Test
void readTest() {
final String value = (String) opsForValue.get("tsb1");
System.out.println(value);
}
} 
在 slave 節(jié)點讀取

可以看到主從復(fù)制及讀寫都沒問題.
5. 主節(jié)點 Master 宕機測試
哨兵的一大作用即就是自動故障轉(zhuǎn)移, 因此, 我們測試一下主節(jié)點宕機后在?SpringBoot 應(yīng)用不重啟的情況下, SpringBoot 應(yīng)用是否還可以繼續(xù)正常工作.
寫一個 Controller 接收讀寫請求, 啟動并讓 SpringBoot 處于運行狀態(tài)
package club.javafamily.sbredissentinel.controller;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
/**
* @author Jack Li
* @date 2022/5/8 下午11:43
* @description
*/
@RestController
public class RedisTestController {
private final RedisTemplate redisTemplate;
public RedisTestController(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@GetMapping("/write")
public String write(@RequestParam("key") String key,
@RequestParam("value") String val)
{
// 寫入
redisTemplate.opsForValue().set(key, val);
// 讀取并返回
return (String) redisTemplate.opsForValue().get(key);
}
} 
下線當(dāng)前的 master
通過
info指令可以看到當(dāng)前集群的 master 是 slave1 節(jié)點(192.168.3.27)

執(zhí)行下線



觀察 SpringBoot 后臺也會重新連接

再次執(zhí)行寫入

查看從節(jié)點數(shù)據(jù)
目前 master 為 slave 節(jié)點, slave1 已經(jīng)下線, slave2 為 master

至此, SpringBoot 連接 Sentinel 集群及自動故障轉(zhuǎn)移測試就講完了, 這里再補充一個知識點, 也是一道
字節(jié)面試題哦:
哨兵集群完成了主從切換,客戶端如何感知?
我們知道Redis有pub/sub機制,為了便于外部知道當(dāng)前的切換進度,哨兵提供了多個訂閱頻道。其中就有一個新主庫切換頻道,(switch-master)
SUBSCRIBE +switch-master訂閱對應(yīng)頻道,就可以獲得切換后的新主庫ip、port,并與之建立連接,繼續(xù)使用 Redis 服務(wù)。
? ? ? ? 如果有任何相關(guān)的問題都可以加入 QQ/微信群一起討論, 學(xué)習(xí), 進步. 此外如果有任何對于本公眾號的意見和建議也歡迎大家留言積極批評指正, 最后, 愿你我都能成為更好的自己.?
? ? ? ? 我是帥帥, 一個集帥氣, 幽默與內(nèi)涵, 并且熱愛編程, 擁抱開源, 喜歡烹飪與旅游的暖男, 我們下期再見.?拜了個拜!
? ? ? ??老規(guī)矩別忘了哦,?點擊原文鏈接跳轉(zhuǎn)到我們官方的博客平臺哦.
悄悄話
————
每文一句
————
Don't aim for success if you really want it. Just stick to what you love and believe in, and it will come naturally.
少一些功利主義的追求, 多一些不為什么的堅持.
日常求贊
————
? ? ? 你們白漂的力量就是我拖更的史詩級動力, 點贊, 評論, 再看, 贊賞, 看都看到這了, 隨便點一個咯.
關(guān)注加好友
拉你進大佬交流群
————————————————
