SpringBoot +Redis集群(填坑Lettuce)

前面我們已經(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ò)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容