前面我们已经[搭建好了redis集群](http://note.youdao.com/noteshare?id=62f1eef263c2c2d798be04808346b823&sub=22A944A5F5264A65A8380D385E30E9F5),使用springboot来集成这个集群
新建一个springboot项目,pom中引入相关jar
```
```
编写 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默认的lettuce连接池
pool:
max-active: 10
max-wait: -1ms
max-idle: 10
min-idle: 0
```
RedisTemplate默认使用的是JdkSerializationRedisSerializer,可视化和效率都不太好,我们这里改成使用Jackson2JsonRedisSerializer 和 StringRedisSerializer序列化数据
```
@Configuration
public class RedisConfiguration {
@Autowired
private RedisProperties redisProperties;
@Bean("redisTemplate")
public RedisTemplate
RedisTemplate
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;
}
}
```
新建一个User对象进行测试
```
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方法
}
```
新建一个Controller类提供接口测试(提供接口是为了模拟redis服务部分宕机后服务的真实情况)
```
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private 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类,启动服务
```
@SpringBootApplication
public class ApplicationRunner {
public static void main(String[] args) {
SpringApplication.run(ApplicationRunner.class);
}
}
```
使用Postman 调用接口插入6条数据 id 为 1-6
```
post http://localhost:8080/user
{
"id":1,
"username":"aaa",
"age":"23"
}
```
查看redis中的keys,发现6条数据分布在3台机器中
```
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>
```
调用获取数据接口,能够获取数据
```
get http://localhost:8081/user?userId=6
{
"id": 6,
"username": "aaa",
"age": 23
}
```
模拟一台服务器宕机(停掉一台服务器上的三个容器)
```
docker stop redis-master1 redis-master2 redis-master3
```
发现此时springboot的redis集群无法使用
查看redis集群状态
```
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
```
集群状态还是正常,前面停掉的三个节点已经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集群连接出现问题,查资料发现spring-redis默认连接池(Lettuce pool)框架在redis的其中一台master机器崩了之后,并没有刷新连接池的连接,仍然连接的是挂掉的那台redis服务器
通过寻找资料,发现springboot在1.x使用的是jedis框架,在2.x改为默认使用Lettuce框架与redis连接。
在Lettuce官方文档中找到了关于Redis Cluster的相关信息 [《Refreshing the cluster topology view》](https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#refreshing-the-cluster-topology-view)
这里面的大概意思是 自适应拓扑刷新(Adaptive updates)与定时拓扑刷新(Periodic updates) 是默认关闭的,可以通过代码打开
(参考 https://juejin.im/post/5e12e39cf265da5d381d0f00)
```
@Configuration
public class RedisConfiguration {
@Autowired
private RedisProperties redisProperties;
@Bean("redisTemplate")
public RedisTemplate
RedisTemplate
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连接工厂实现
* LettuceConnectionFactory实现了RedisConnectionFactory接口
* UVPV用Redis
*
* @return 返回LettuceConnectionFactory
*/
@Bean(destroyMethod = "destroy")
//这里要注意的是,在构建LettuceConnectionFactory 时,如果不使用内置的destroyMethod,可能会导致Redis连接早于其它Bean被销毁
public LettuceConnectionFactory lettuceConnectionFactoryUvPv() throws Exception {
List
Set
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 包括线程池配置和安全项配置
*
* @param genericObjectPoolConfig common-pool2线程池
* @return lettuceClientConfiguration
*/
private LettuceClientConfiguration getLettuceClientConfiguration(GenericObjectPoolConfig genericObjectPoolConfig) {
/*
ClusterTopologyRefreshOptions配置用于开启自适应刷新和定时刷新。如自适应刷新不开启,Redis集群变更时将会导致连接异常!
*/
ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
//开启自适应刷新
//.enableAdaptiveRefreshTrigger(ClusterTopologyRefreshOptions.RefreshTrigger.MOVED_REDIRECT, ClusterTopologyRefreshOptions.RefreshTrigger.PERSISTENT_RECONNECTS)
//开启所有自适应刷新,MOVED,ASK,PERSISTENT都会触发
.enableAllAdaptiveRefreshTriggers()
// 自适应刷新超时时间(默认30秒)
.adaptiveRefreshTriggersTimeout(Duration.ofSeconds(25)) //默认关闭开启后时间为30秒
// 开周期刷新
.enablePeriodicRefresh(Duration.ofSeconds(20)) // 默认关闭开启后时间为60秒 ClusterTopologyRefreshOptions.DEFAULT_REFRESH_PERIOD 60
.build();
return LettucePoolingClientConfiguration.builder()
.poolConfig(genericObjectPoolConfig)
.clientOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build())
//将appID传入连接,方便Redis监控中查看
//.clientName(appName + "_lettuce")
.build();
}
}
```
或者不使用Lettuce,使用Jedis
当然,如果你想就此放弃Lettuce转用jedis也是可以的。在Spring Boot2.X版本,只要在pom.xml里,调整一下依赖包的引用即可:
```
```
配置上lettuce换成jedis的,既可以完成底层对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 拓普刷新都是有时间间隔的,Lettuce我们设置的时间是25秒adaptiveRefreshTriggersTimeout(Duration.ofSeconds(25)),25秒内,redis连接还是会报错