SpringBoot +Redis集群(填坑Lettuce)

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

新建一个springboot项目,pom中引入相关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>

```

编写 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<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;

    }

}

```

新建一个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<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类,启动服务

```

@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<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连接工厂实现

    * LettuceConnectionFactory实现了RedisConnectionFactory接口

    * UVPV用Redis

    *

    * @return 返回LettuceConnectionFactory

    */

    @Bean(destroyMethod = "destroy")

    //这里要注意的是,在构建LettuceConnectionFactory 时,如果不使用内置的destroyMethod,可能会导致Redis连接早于其它Bean被销毁

    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 包括线程池配置和安全项配置

    *

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

```

<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的,既可以完成底层对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连接还是会报错

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,198评论 6 514
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,334评论 3 398
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,643评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,495评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,502评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,156评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,743评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,659评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,200评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,282评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,424评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,107评论 5 349
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,789评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,264评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,390评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,798评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,435评论 2 359

推荐阅读更多精彩内容