前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[Redis]Spring Boot中Redis Template集群配置

[Redis]Spring Boot中Redis Template集群配置

原创
作者头像
宇宙无敌暴龙战士之心悦大王
修改2023-03-16 15:23:59
6.8K0
修改2023-03-16 15:23:59
举报
文章被收录于专栏:kwai

1、问题背景

在一个Spring boot项目中,需要使用redis作为缓存,于是将使用spring-boot-starter-data-redis,具体依赖如下:

代码语言:javascript
复制
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
			<version>2.0.4.RELEASE</version>
		</dependency>

在测试环境中,功能测试,压力测试,都没有发现问题,原因是测试环境中redis自行搭建,没有设置密码,但是上线后,Redis使用的是A***的Pass服务的集群,并设置密码,使用过程中发现如下问题:

  1. redis负载高;
  2. redis异常,错误信息: com.lambdaworks.redis.RedisException: java.lang.IllegalArgumentException: Connection to XXX.XX.XXX.XXX:15000 not allowed. This connection point is not known in the cluster viewjava.lang.IllegalArgumentException: Connection to XXX.XX.XXX.XXX:15000 not allowed. This connection point is not known in the cluster viewConnection to XXX.XX.XXX.XXX:15000 not allowed. This connection point is not known in the cluster view

2、问题分析+解决方法

2.1、redis负载过高问题

2.1.1、问题原因

原本打算看一下是否是代码逻辑问题导致redis负载过高,于是登录redis服务器使用monitor命令观察命令执行的频率,发现每执行一次命令都执行一次Auth password命令,说明连接池未正确使用导致执行一次命令创建一次连接,导致负载高 ,并且代码执行效率低 。

2.1.2、解决方法

然后对比了使用JedisCluster的项目没有此类问题,因此怀疑是spring-boot-starter-data-redis的RedisTemplate的问题,查看源码后发现spring-data-redis的驱动包在某个版本之后替换为 Lettuce,在启用集群后jedis的连接池无效。错误配置如下:

代码语言:javascript
复制
# 错误配置
# Redis配置
spring.redis.cluster.nodes=127.0.0.1:6379
### 连接超时时间(毫秒)
spring.redis.timeout=60000
spring.redis.password=xxxxxxx
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
##连接池最大阻塞等待时间,若使用负值表示没有限制
spring.redis.jedis.pool.max-wait=-1
##连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0

需要改成正确的配置,修改之后无此现象,具体配置如下:

单机版:

代码语言:javascript
复制
# 单机版
# Redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
### 连接超时时间(毫秒)
spring.redis.timeout=60000
spring.redis.password=xxxxxxx
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
##连接池最大阻塞等待时间,若使用负值表示没有限制
spring.redis.jedis.pool.max-wait=-1
##连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0

集群版:

代码语言:javascript
复制
#集群版 
# Redis配置
spring.redis.cluster.nodes=127.0.0.1:6379
### 连接超时时间(毫秒)
spring.redis.timeout=60000
spring.redis.password=xxxxxxx
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
##连接池最大阻塞等待时间,若使用负值表示没有限制
spring.redis.lettuce.pool.max-wait=-1
##连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0

注意:启用集群版,需要额外添加如下依赖

代码语言:javascript
复制
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
			<version>2.8.0</version>
		</dependency>

2.2、redis异常 Connection to XXX.XX.XXX.XXX:15000 not allowed 问题

2.2.1、问题原因

网上搜索了一下,发现项目github上已有此问题的反馈以及解决办法github.com/lettuce-io/…,原因是由于Lettuce其中有个配置项validateClusterNodeMembership 默认是true导致;

2.2.2、解决办法

由于spring boot未能直接通过配置文件直接修改此配置,因此需要自定义Redis配置,具体代码如下: MylettuceConnectionFactory.java

代码语言:javascript
复制
package com.quison.test.config;

import io.lettuce.core.AbstractRedisClient;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import io.lettuce.core.cluster.RedisClusterClient;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;

import java.util.concurrent.TimeUnit;

public class MyLettuceConnectionFactory extends LettuceConnectionFactory {

    public MyLettuceConnectionFactory() { }

    public MyLettuceConnectionFactory(RedisClusterConfiguration redisClusterConfiguration, LettuceClientConfiguration lettuceClientConfiguration) {
        super(redisClusterConfiguration, lettuceClientConfiguration);
    }

    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        DirectFieldAccessor accessor = new DirectFieldAccessor(this);
        AbstractRedisClient client = (AbstractRedisClient) accessor.getPropertyValue("client");
        if(client instanceof RedisClusterClient){
            RedisClusterClient clusterClient = (RedisClusterClient) client;
            ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                    .enablePeriodicRefresh(10, TimeUnit.MINUTES)
                    .enableAllAdaptiveRefreshTriggers()
                    .build();
            ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
                     // 注意此配置项设置为false
                    .validateClusterNodeMembership(false)
                    .topologyRefreshOptions(topologyRefreshOptions)
                    .build();
            clusterClient.setOptions(clusterClientOptions);
        }
    }

}

由于配置后,连接池也需要自行设置,因此Redis的配置文件修改为如下设置 RedisConfig.java

代码语言:javascript
复制
package com.quison.test.config;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.lettuce.DefaultLettucePool;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

@Configuration
public class RedisConfig {


    @Value("${spring.redis.cluster.nodes}")
    private String clusterNodes;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.lettuce.pool.max-idle}")
    private Integer maxIdle;

    @Value("${spring.redis.lettuce.pool.max-active}")
    private Integer maxActive;

    @Value("${spring.redis.cluster.max-redirects}")
    private Integer maxRedirects;


    @Bean
    public RedisConnectionFactory myRedisConnectionFactory() {

        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
        String[] serverArray = clusterNodes.split(",");
        Set<RedisNode> nodes = new HashSet<RedisNode>();
        for (String ipPort : serverArray) {
            String[] ipAndPort = ipPort.split(":");
            nodes.add(new RedisNode(ipAndPort[0].trim(), Integer.valueOf(ipAndPort[1])));
        }
        redisClusterConfiguration.setPassword(RedisPassword.of(password));
        redisClusterConfiguration.setClusterNodes(nodes);
        redisClusterConfiguration.setMaxRedirects(maxRedirects);


        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
        genericObjectPoolConfig.setMaxIdle(maxIdle);
        genericObjectPoolConfig.setMinIdle(8);
        genericObjectPoolConfig.setMaxTotal(maxActive);
        genericObjectPoolConfig.setMaxWaitMillis(10000);

        LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
                .commandTimeout(Duration.ofMillis(10000))
                .poolConfig(genericObjectPoolConfig)
                .build();


        return new MyLettuceConnectionFactory(redisClusterConfiguration, clientConfig);
    }


    /**
     * redis模板,存储关键字是字符串,值是Jdk序列化
     *
     * @param myRedisConnectionFactory
     * @return
     * @Description:
     */
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    @Primary
    public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory myRedisConnectionFactory) {
        RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(myRedisConnectionFactory);
        //key序列化方式;但是如果方法上有Long等非String类型的话,会报类型转换错误;
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);

        //默认使用JdkSerializationRedisSerializer序列化方式;会出现乱码,改成StringRedisSerializer
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setValueSerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        redisTemplate.setHashValueSerializer(stringSerializer);
        return redisTemplate;
    }
}

3、总结

吃一堑、长一智,总结如下:

  1. 开发+测试环境尽量与线上一致,可提前发现问题;
  2. 使用新技术需要多多测试再投入生产使用;

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、问题背景
  • 2、问题分析+解决方法
    • 2.1、redis负载过高问题
      • 2.2、redis异常 Connection to XXX.XX.XXX.XXX:15000 not allowed 问题
      • 3、总结
      相关产品与服务
      云数据库 Redis
      腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档