SpringBoot +Redis集群(填坑Lettuce)

前面我们已经[搭建好了redis集群](http://note.youdao.com/noteshare?id=62f1eef263c2c2d798be04808346b823&sub=22A944A5F5264A65A8380D385E30E9F5),使用springboot来集成这个集群

新建一个springboot项目,pom中引入相关jar

```

        org.springframework.boot

        spring-boot-starter-parent

        2.1.5.RELEASE

       

   

   

       

            org.springframework.boot

            spring-boot-starter-data-redis

       

       

            org.springframework.boot

            spring-boot-starter-web

       

       

            org.springframework.boot

            spring-boot-starter-test

            test

       

       

            org.apache.commons

            commons-pool2

       

   

```

编写 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(RedisConnectionFactory factory) {

        RedisTemplate template = new 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 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(@Qualifier("lettuceConnectionFactoryUvPv") RedisConnectionFactory factory) {

        RedisTemplate template = new 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 clusterNodes = redisProperties.getCluster().getNodes();

        Set nodes = new HashSet();

        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里,调整一下依赖包的引用即可:

```

org.springframework.boot

spring-boot-starter-data-redis

io.lettuce

lettuce-core

redis.clients

jedis

```

配置上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连接还是会报错

你可能感兴趣的:(SpringBoot +Redis集群(填坑Lettuce))