前面我們已經(jīng)[搭建好了redis集群](http://note.youdao.com/noteshare?id=62f1eef263c2c2d798be04808346b823&sub=22A944A5F5264A65A8380D385E30E9F5),使用springboot來(lái)集成這個(gè)集群
新建一個(gè)springboot項(xiàng)目,pom中引入相關(guān)jar
```
<parent>
? ? ? ? <groupId>org.springframework.boot</groupId>
? ? ? ? <artifactId>spring-boot-starter-parent</artifactId>
? ? ? ? <version>2.1.5.RELEASE</version>
? ? ? ? <relativePath/>
? ? </parent>
? ? <dependencies>
? ? ? ? <dependency>
? ? ? ? ? ? <groupId>org.springframework.boot</groupId>
? ? ? ? ? ? <artifactId>spring-boot-starter-data-redis</artifactId>
? ? ? ? </dependency>
? ? ? ? <dependency>
? ? ? ? ? ? <groupId>org.springframework.boot</groupId>
? ? ? ? ? ? <artifactId>spring-boot-starter-web</artifactId>
? ? ? ? </dependency>
? ? ? ? <dependency>
? ? ? ? ? ? <groupId>org.springframework.boot</groupId>
? ? ? ? ? ? <artifactId>spring-boot-starter-test</artifactId>
? ? ? ? ? ? <scope>test</scope>
? ? ? ? </dependency>
? ? ? ? <dependency>
? ? ? ? ? ? <groupId>org.apache.commons</groupId>
? ? ? ? ? ? <artifactId>commons-pool2</artifactId>
? ? ? ? </dependency>
? ? </dependencies>
```
編寫(xiě) application.yml文件
```
spring:
? redis:
? ? cluster:
? ? ? nodes:
? ? ? ? - 10.10.1.114:6391
? ? ? ? - 10.10.1.114:6392
? ? ? ? - 10.10.1.114:6393
? ? ? ? - 10.10.1.49:6394
? ? ? ? - 10.10.1.49:6395
? ? ? ? - 10.10.1.49:6396
? ? ? max-redirects: 3
? ? lettuce:#使用spring默認(rèn)的lettuce連接池
? ? ? pool:
? ? ? ? max-active: 10
? ? ? ? max-wait: -1ms
? ? ? ? max-idle: 10
? ? ? ? min-idle: 0
```
RedisTemplate默認(rèn)使用的是JdkSerializationRedisSerializer,可視化和效率都不太好,我們這里改成使用Jackson2JsonRedisSerializer 和 StringRedisSerializer序列化數(shù)據(jù)
```
@Configuration
public class RedisConfiguration {
? ? @Autowired
? ? private RedisProperties redisProperties;
? ? @Bean("redisTemplate")
? ? public RedisTemplate<String, Serializable> redisTemplate(RedisConnectionFactory factory) {
? ? ? ? RedisTemplate<String, Serializable> template = new RedisTemplate<String, Serializable>();
? ? ? ? template.setConnectionFactory(factory);
? ? ? ? Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
? ? ? ? ObjectMapper om = new ObjectMapper();
? ? ? ? om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
? ? ? ? om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
? ? ? ? jackson2JsonRedisSerializer.setObjectMapper(om);
? ? ? ? StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
? ? ? ? // key采用String的序列化方式
? ? ? ? template.setKeySerializer(stringRedisSerializer);
? ? ? ? // hash的key也采用String的序列化方式-[
? ? ? ? template.setHashKeySerializer(stringRedisSerializer);
? ? ? ? // value序列化方式采用jackson
? ? ? ? template.setValueSerializer(jackson2JsonRedisSerializer);
? ? ? ? // hash的value序列化方式采用jackson
? ? ? ? template.setHashValueSerializer(jackson2JsonRedisSerializer);
? ? ? ? template.afterPropertiesSet();
? ? ? ? return template;
? ? }
}
```
新建一個(gè)User對(duì)象進(jìn)行測(cè)試
```
public class User implements Serializable {
? ? private static final long serialVersionUID = 4220515347228129741L;
? ? private Integer id;
? ? private String username;
? ? private Integer age;
? ? private User parents;
? ? public User(Integer id, String username, Integer age,User parents) {
? ? ? ? this.id = id;
? ? ? ? this.username = username;
? ? ? ? this.age = age;
? ? ? ? this.parents = parents;
? ? }
? ? public User() {
? ? }
? //省set和get方法
}
```
新建一個(gè)Controller類(lèi)提供接口測(cè)試(提供接口是為了模擬redis服務(wù)部分宕機(jī)后服務(wù)的真實(shí)情況)
```
@RestController
@RequestMapping("/user")
public class UserController {
? ? @Autowired
? ? private RedisTemplate<String, Serializable> redisTemplate;
? ? @PostMapping
? ? public void addUser(@RequestBody? User user){
? ? ? ? String key="user:"+user.getId();
? ? ? ? redisTemplate.opsForValue().set(key,user);
? ? }
? ? @GetMapping
? ? public User getUser(@RequestParam? Integer userId){
? ? ? ? String key="user:"+userId;
? ? ? return (User)redisTemplate.opsForValue().get(key);
? ? }
}
```
新建Application類(lèi),啟動(dòng)服務(wù)
```
@SpringBootApplication
public class ApplicationRunner {
? ? public static void main(String[] args) {
? ? ? ? SpringApplication.run(ApplicationRunner.class);
? ? }
}
```
使用Postman 調(diào)用接口插入6條數(shù)據(jù) id 為 1-6
```
post http://localhost:8080/user
{
"id":1,
"username":"aaa",
"age":"23"
}
```
查看redis中的keys,發(fā)現(xiàn)6條數(shù)據(jù)分布在3臺(tái)機(jī)器中
```
root@cloud-nlp:/config# redis-cli -c -p 6391
127.0.0.1:6391> keys *
1) "user:3"
127.0.0.1:6391> exit
root@cloud-nlp:/config# redis-cli -c -p 6392
127.0.0.1:6392> keys *
1) "user:4"
127.0.0.1:6392> exit
root@cloud-nlp:/config# redis-cli -c -p 6393
127.0.0.1:6393> keys *
1) "user:5"
2) "user:1"
3) "user:6"
4) "user:2"
127.0.0.1:6393>
```
調(diào)用獲取數(shù)據(jù)接口,能夠獲取數(shù)據(jù)
```
get http://localhost:8081/user?userId=6
{
? ? "id": 6,
? ? "username": "aaa",
? ? "age": 23
}
```
模擬一臺(tái)服務(wù)器宕機(jī)(停掉一臺(tái)服務(wù)器上的三個(gè)容器)
```
docker stop redis-master1 redis-master2 redis-master3
```
發(fā)現(xiàn)此時(shí)springboot的redis集群無(wú)法使用
查看redis集群狀態(tài)
```
127.0.0.1:6393> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:4
cluster_stats_messages_ping_sent:5059
cluster_stats_messages_pong_sent:5130
cluster_stats_messages_meet_sent:4
cluster_stats_messages_sent:10193
cluster_stats_messages_ping_received:5128
cluster_stats_messages_pong_received:5063
cluster_stats_messages_meet_received:2
cluster_stats_messages_received:10193
```
集群狀態(tài)還是正常,前面停掉的三個(gè)節(jié)點(diǎn)已經(jīng)fail(不影響集群)
```
127.0.0.1:6393> cluster nodes
8ea8565ad01b17f9274110b34bf145e5a9b23cd7 10.10.1.114:6391@16391 master - 0 1587035427998 1 connected 0-5460
71a3a55b7f62b69cbb9de13462e0dc33a14918ae 10.10.1.49:6394@16394 master,fail - 1587035388110 1587035387000 4 disconnected
89979f22bb855c3fb11026539d8ce20664107c18 10.10.1.49:6395@16395 slave,fail bdcd632b46ae639ffe7cd1aa533539c88c5e0e3d 1587035388110 1587035384000 5 disconnected
99d5283110e71f2c1462a647662074459c2aa33d 10.10.1.49:6396@16396 slave,fail 8ea8565ad01b17f9274110b34bf145e5a9b23cd7 1587035388110 1587035386000 6 disconnected
c6a72d4cc1cf65daef4a6ee720c32ed23bccadbe 10.10.1.114:6393@16393 myself,master - 0 1587035426000 7 connected 5461-10922
bdcd632b46ae639ffe7cd1aa533539c88c5e0e3d 10.10.1.114:6392@16392 master - 0 1587035426996 2 connected 10923-16383
```
由此可知是springboot集群連接出現(xiàn)問(wèn)題,查資料發(fā)現(xiàn)spring-redis默認(rèn)連接池(Lettuce pool)框架在redis的其中一臺(tái)master機(jī)器崩了之后,并沒(méi)有刷新連接池的連接,仍然連接的是掛掉的那臺(tái)redis服務(wù)器
通過(guò)尋找資料,發(fā)現(xiàn)springboot在1.x使用的是jedis框架,在2.x改為默認(rèn)使用Lettuce框架與redis連接。
在Lettuce官方文檔中找到了關(guān)于Redis Cluster的相關(guān)信息 [《Refreshing the cluster topology view》](https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#refreshing-the-cluster-topology-view)
這里面的大概意思是 自適應(yīng)拓?fù)渌⑿拢ˋdaptive updates)與定時(shí)拓?fù)渌⑿拢≒eriodic updates) 是默認(rèn)關(guān)閉的,可以通過(guò)代碼打開(kāi)
(參考? https://juejin.im/post/5e12e39cf265da5d381d0f00)
```
@Configuration
public class RedisConfiguration {
? ? @Autowired
? ? private RedisProperties redisProperties;
? ? @Bean("redisTemplate")
? ? public RedisTemplate<String, Serializable> redisTemplate(@Qualifier("lettuceConnectionFactoryUvPv") RedisConnectionFactory factory) {
? ? ? ? RedisTemplate<String, Serializable> template = new RedisTemplate<String, Serializable>();
? ? ? ? template.setConnectionFactory(factory);
? ? ? ? Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
? ? ? ? ObjectMapper om = new ObjectMapper();
? ? ? ? om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
? ? ? ? om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
? ? ? ? jackson2JsonRedisSerializer.setObjectMapper(om);
? ? ? ? StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
? ? ? ? // key采用String的序列化方式
? ? ? ? template.setKeySerializer(stringRedisSerializer);
? ? ? ? // hash的key也采用String的序列化方式-[
? ? ? ? template.setHashKeySerializer(stringRedisSerializer);
? ? ? ? // value序列化方式采用jackson
? ? ? ? template.setValueSerializer(jackson2JsonRedisSerializer);
? ? ? ? // hash的value序列化方式采用jackson
? ? ? ? template.setHashValueSerializer(jackson2JsonRedisSerializer);
? ? ? ? template.afterPropertiesSet();
? ? ? ? return template;
? ? }
? ? /**
? ? * 為RedisTemplate配置Redis連接工廠實(shí)現(xiàn)
? ? * LettuceConnectionFactory實(shí)現(xiàn)了RedisConnectionFactory接口
? ? * UVPV用Redis
? ? *
? ? * @return 返回LettuceConnectionFactory
? ? */
? ? @Bean(destroyMethod = "destroy")
? ? //這里要注意的是,在構(gòu)建LettuceConnectionFactory 時(shí),如果不使用內(nèi)置的destroyMethod,可能會(huì)導(dǎo)致Redis連接早于其它Bean被銷(xiāo)毀
? ? public LettuceConnectionFactory lettuceConnectionFactoryUvPv() throws Exception {
? ? ? ? List<String> clusterNodes = redisProperties.getCluster().getNodes();
? ? ? ? Set<RedisNode> nodes = new HashSet<RedisNode>();
? ? ? ? clusterNodes.forEach(address -> nodes.add(new RedisNode(address.split(":")[0].trim(), Integer.valueOf(address.split(":")[1]))));
? ? ? ? RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration();
? ? ? ? clusterConfiguration.setClusterNodes(nodes);
? ? ? ? clusterConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
? ? ? ? clusterConfiguration.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
? ? ? ? GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
? ? ? ? poolConfig.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle());
? ? ? ? poolConfig.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle());
? ? ? ? poolConfig.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive());
? ? ? ? return new LettuceConnectionFactory(clusterConfiguration, getLettuceClientConfiguration(poolConfig));
? ? }
? ? /**
? ? * 配置LettuceClientConfiguration 包括線程池配置和安全項(xiàng)配置
? ? *
? ? * @param genericObjectPoolConfig common-pool2線程池
? ? * @return lettuceClientConfiguration
? ? */
? ? private LettuceClientConfiguration getLettuceClientConfiguration(GenericObjectPoolConfig genericObjectPoolConfig) {
? ? ? /*
? ? ? ? ClusterTopologyRefreshOptions配置用于開(kāi)啟自適應(yīng)刷新和定時(shí)刷新。如自適應(yīng)刷新不開(kāi)啟,Redis集群變更時(shí)將會(huì)導(dǎo)致連接異常!
? ? ? ? */
? ? ? ? ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
? ? ? ? ? ? ? ? //開(kāi)啟自適應(yīng)刷新
? ? ? ? ? ? ? ? //.enableAdaptiveRefreshTrigger(ClusterTopologyRefreshOptions.RefreshTrigger.MOVED_REDIRECT, ClusterTopologyRefreshOptions.RefreshTrigger.PERSISTENT_RECONNECTS)
? ? ? ? ? ? ? ? //開(kāi)啟所有自適應(yīng)刷新,MOVED,ASK,PERSISTENT都會(huì)觸發(fā)
? ? ? ? ? ? ? ? .enableAllAdaptiveRefreshTriggers()
? ? ? ? ? ? ? ? // 自適應(yīng)刷新超時(shí)時(shí)間(默認(rèn)30秒)
? ? ? ? ? ? ? ? .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(25)) //默認(rèn)關(guān)閉開(kāi)啟后時(shí)間為30秒
? ? ? ? ? ? ? ? // 開(kāi)周期刷新
? ? ? ? ? ? ? ? .enablePeriodicRefresh(Duration.ofSeconds(20))? // 默認(rèn)關(guān)閉開(kāi)啟后時(shí)間為60秒 ClusterTopologyRefreshOptions.DEFAULT_REFRESH_PERIOD 60
? ? ? ? ? ? ? ? .build();
? ? ? ? return LettucePoolingClientConfiguration.builder()
? ? ? ? ? ? ? ? .poolConfig(genericObjectPoolConfig)
? ? ? ? ? ? ? ? .clientOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build())
? ? ? ? ? ? ? ? //將appID傳入連接,方便Redis監(jiān)控中查看
? ? ? ? ? ? ? ? //.clientName(appName + "_lettuce")
? ? ? ? ? ? ? ? .build();
? ? }
}
```
或者不使用Lettuce,使用Jedis
當(dāng)然,如果你想就此放棄Lettuce轉(zhuǎn)用jedis也是可以的。在Spring Boot2.X版本,只要在pom.xml里,調(diào)整一下依賴(lài)包的引用即可:
```
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
```
配置上lettuce換成jedis的,既可以完成底層對(duì)jedis的替換
```
spring:
? redis:
? ? cluster:
? ? ? nodes:
? ? ? - 10.10.1.114:6391
? ? ? - 10.10.1.114:6392
? ? ? - 10.10.1.114:6393
? ? ? - 10.10.1.49:6394
? ? ? - 10.10.1.49:6395
? ? ? - 10.10.1.49:6396
? ? ? max-redirects: 3
#? ? lettuce:
#? ? ? pool:
#? ? ? ? max-active: 10
#? ? ? ? max-wait: -1ms
#? ? ? ? max-idle: 10
#? ? ? ? min-idle: 0
? ? jedis:
? ? ? pool:
? ? ? ? max-active: 10
? ? ? ? max-wait: -1ms
? ? ? ? max-idle: 10
? ? ? ? min-idle: 1
```
ps:不管是Lettuce 還是Jedis 拓普刷新都是有時(shí)間間隔的,Lettuce我們?cè)O(shè)置的時(shí)間是25秒adaptiveRefreshTriggersTimeout(Duration.ofSeconds(25)),25秒內(nèi),redis連接還是會(huì)報(bào)錯(cuò)