低版本SpringBoot Redis缓存旁路设计改造方案实践

简介: 低版本SpringBoot Redis缓存旁路设计改造方案实践

1. 引言

1.1 编写目的

在越来越多的系统建设中,旁路设计受到重视,但是在低版本SpringBoot,以及其默认引入的Lettuce Redis客户端中,并没有很好的处理旁路问题。本文则讲述通过引入Redisson Redis客户端替换Lettuce的方式,进行更好的旁路解决。

1.2 读者对象

本文档适合以下人员阅读:

项目组设计与开发人员;

2. 业务背景

SpringBoot 2.0.x-2.2.x版本中,默认引入的配置,不支持配置连接Redis 客户端Socket超时时间。以SpringBoot 2.0.8.RELEASE为例,其引入的letture-core 5.0.5.RELEASE中,默认的连接超时为10秒,且无开放修改接口,部分代码如下:

// io.lettuce.core.SocketOptions
public class SocketOptions {
    public static final long DEFAULT_CONNECT_TIMEOUT = 10;
    public static final TimeUnit DEFAULT_CONNECT_TIMEOUT_UNIT = TimeUnit.SECONDS;
    public static final Duration DEFAULT_CONNECT_TIMEOUT_DURATION = Duration.ofSeconds(DEFAULT_CONNECT_TIMEOUT);
}
// io.lettuce.core.ClientOptions
public class ClientOptions implements Serializable {
    public static final SocketOptions DEFAULT_SOCKET_OPTIONS = SocketOptions.create();
    public static final SslOptions DEFAULT_SSL_OPTIONS = SslOptions.create();
    private final SocketOptions socketOptions;
    private final SslOptions sslOptions;
}

对于一个添加@Cacheable注解意图进行实现Redis旁路的方法来说,当Redis集群不能提供服务时,它将进行如下操作:

  1. 连接Redis进行get操作,进行读取缓存;
  2. 连接Redis失败,执行方法体,获得执行结果;
  3. 连接Redis,将执行结果写入缓存;

那么在步骤1、3中,由于连接Redis会失败,但是由于无法配置的连接超时为10秒,这个为了使用缓存进行加速访问的接口,在不能正常加速的情况下,还会有进行连接Socket至少额外20秒的开销。那么在一个大接口调用两个需要缓存加速的小接口时,额外的耗时时间就提升至40秒,直接超过了网关的超时时间30秒,接口将被熔断,旁路设计实际上并不生效。

所以项目中需要有一个对Redis客户端连接Socket超时时间可配置的选型。

3. 适用场景

当Redis缓存作用为加速查询耗时,减少接口耗时的场景,则适用本方案进行实践。若Redis缓存作为内存数据库进行持久化场景使用时,则不适用此方案,例如Session会话管理。

4. 解决方案

在整体脚手架基于SpringBoot 2.0.8.RELEASE版本,且无法升级版本的大前提下,通过更改Redis客户端进行解决成了可靠的途径。

4.1 引入Redisson替换Lettuce

  1. 排除Lettuce
    在spring-boot-starter-data-redis中,将其默认依赖的Redis客户端Lettuce进行排除。
<!-- 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>
  1. 引入Redisson
    在引入Redisson时,需要注意版本问题,参考官方redisson-spring-data、以及redisson-spring-boot-starter工程中的介绍,对于SpringBoot 2.0.x版本,引入的Redisson版本须为3.9.1。
<!-- pom.xml -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.9.1</version>
        </dependency>

4.2 修改配置文件

Redisson配置文件既读取SpringBoot默认的RedisProperties,也读取其自定义的RedissonProperties。其配置如下:

  • SpringBoot 公共配置
spring:
  redis:
    database:
    host:
    port:
    password:
    ssl:
    timeout:
    cluster:
      nodes:
    sentinel:
      master:
      nodes:
  • Redisson 配置
spring:
  redis:
   redisson:
      file: classpath:redisson.yaml
      config: |
        clusterServersConfig:
          idleConnectionTimeout: 10000
          # 连接超时时间配置
          connectTimeout: 10000
          timeout: 3000
          retryAttempts: 3
          retryInterval: 1500
          failedSlaveReconnectionInterval: 3000
          failedSlaveCheckInterval: 60000
          password: null
          subscriptionsPerConnection: 5
          clientName: null
          loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
          subscriptionConnectionMinimumIdleSize: 1
          subscriptionConnectionPoolSize: 50
          slaveConnectionMinimumIdleSize: 24
          slaveConnectionPoolSize: 64
          masterConnectionMinimumIdleSize: 24
          masterConnectionPoolSize: 64
          readMode: "SLAVE"
          subscriptionMode: "SLAVE"
          nodeAddresses:
          - "redis://127.0.0.1:7004"
          - "redis://127.0.0.1:7001"
          - "redis://127.0.0.1:7000"
          scanInterval: 1000
          pingConnectionInterval: 0
          keepAlive: false
          tcpNoDelay: false
        threads: 16
        nettyThreads: 32
        codec: !<org.redisson.codec.MarshallingCodec> {}
        transportMode: "NIO"

通过将Redisson配置中connectTimeout配置项改小(例如2秒),同时将retryAttempts配置的次数改为0,则可以将Redis Socket 连接的问题做到快速失败,在Redis集群宕机时,单个@Cacheable注解的接口增加的耗时为4秒,并不会导致超过网关的超时时间,最终导致接口被熔断。

4.3 更优雅的配置方式

当前Redisson的配置中,spring.redis.redisson.config是作为String类型变量引入的,其后通过尝试JSON转型、YAML转型成配置类的方式,代码如下:

//  org.redisson.spring.starter.RedissonAutoConfiguration#redisson
  @Bean(destroyMethod = "shutdown")
    @ConditionalOnMissingBean(RedissonClient.class)
    public RedissonClient redisson() throws IOException {
        Config config = null;
        if (redissonProperties.getConfig() != null) {
            try {
                InputStream is = getConfigStream();
                config = Config.fromJSON(is);
            } catch (IOException e) {
                try {
                    InputStream is = getConfigStream();
                    config = Config.fromYAML(is);
                } catch (IOException e1) {
                    throw new IllegalArgumentException("Can't parse config", e1);
                }
            }
        }
        return Redisson.create(config);
    }

这样进行引入配置的方式存在一个十分不优雅的方式,Redis集群信息,密码等单个配置项无法使用${xxx}方式在SpEL中解析,则无法放入配置中心,当然将spring.redis.redisson.config整个key放入配置中心可以解决问题,但是也同样很不优雅。

为了能够将spring.redis.redisson.config配置项中的单个配置项放入配置中心,可以通过增加工具类解析该配置项,并通过Spring提供的Environment进行参数的获取。

  • 解析配置工具类
@Component
public class SpringExpressionUtil {
    @Resource
    private Environment environment;
    /**
     * 匹配${xxx}表达式
     */
    private static final Pattern SPRING_ELEMENT_LANGUAGE_PATTERN = Pattern.compile("\\$\\{(.*?)}");
    public String replaceSpELPatternValue(final String config) {
        String[] result = new String[1];
        result[0] = config;
        Map<String, String> kv = new HashMap<>();
        Matcher matcher = SPRING_ELEMENT_LANGUAGE_PATTERN.matcher(config);
        String key;
        String realKey;
        while (matcher.find()) {
            key =  matcher.group();
            // 去除${}, 提取key
            realKey = key.substring(2, key.length() - 1);
            kv.put(key, environment.getProperty(realKey, ""));
        }
        kv.forEach((pattern, value) -> {
            result[0] = result[0].replace(pattern, value);
        });
        return result[0];
    }
}
  • 注入RedissonClient.class Bean
// 在@Configuration配置类中进行注入
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson(@Value("classpath:/redisson.yaml") Resource configFile) throws IOException {
        return createRedissonClient(configFile);
    }
    private RedissonClient createRedissonClient(Resource configFile) throws IOException {
        String config = null;
        InputStream in = null;
        try {
            in = configFile.getInputStream();
            config = IoUtil.read(in, Charset.defaultCharset());
        } finally {
            IoUtil.close(in);
        }
        return Redisson.create(Config.fromYAML(config));
    }
  • 配置文件redisson.yaml
    在配置文件中,根据需要,将需要参数化的配置以${xxx}形式配置,并在spring配置文件、配置中心中配置对应key的属性即可。其具体可配置参数在类org.redisson.config.Config中可以进行查看。
singleServerConfig:  idleConnectionTimeout: ${spring.redis.redisson.idleConnectionTimeout}  pingTimeout: ${spring.redis.redisson.pingTimeout}  connectTimeout: ${spring.redis.redisson.connectTimeout}  timeout: ${spring.redis.redisson.timeout}  retryAttempts: ${spring.redis.redisson.retryAttempts}  retryInterval: ${spring.redis.redisson.retryInterval}  reconnectionTimeout: ${spring.redis.redisson.reconnectionTimeout}  failedAttempts: ${spring.redis.redisson.failedAttempts}  password: ${spring.redis.redisson.password}  subscriptionsPerConnection: ${spring.redis.redisson.subscriptionsPerConnection}  clientName: ${spring.redis.redisson.clientName}  address: ${spring.redis.redisson.address}  subscriptionConnectionMinimumIdleSize: ${spring.redis.redisson.subscriptionConnectionMinimumIdleSize}  subscriptionConnectionPoolSize: ${spring.redis.redisson.subscriptionConnectionPoolSize}  connectionMinimumIdleSize: ${spring.redis.redisson.connectionMinimumIdleSize}  connectionPoolSize: ${spring.redis.redisson.connectionPoolSize}  database: ${spring.redis.redisson.database}  dnsMonitoringInterval: ${spring.redis.redisson.dnsMonitoringInterval}threads: ${spring.redis.redisson.threads}nettyThreads: ${spring.redis.redisson.nettyThreads}codec: !<org.redisson.codec.JsonJacksonCodec> {}transportMode: "NIO"


目录
相关文章
|
6月前
|
消息中间件 缓存 NoSQL
Redis各类数据结构详细介绍及其在Go语言Gin框架下实践应用
这只是利用Go语言和Gin框架与Redis交互最基础部分展示;根据具体业务需求可能需要更复杂查询、事务处理或订阅发布功能实现更多高级特性应用场景。
393 86
|
5月前
|
NoSQL Java 网络安全
SpringBoot启动时连接Redis报错:ERR This instance has cluster support disabled - 如何解决?
通过以上步骤一般可以解决由于配置不匹配造成的连接错误。在调试问题时,一定要确保服务端和客户端的Redis配置保持同步一致。这能够确保SpringBoot应用顺利连接到正确配置的Redis服务,无论是单机模式还是集群模式。
484 5
|
5月前
|
缓存 负载均衡 监控
135_负载均衡:Redis缓存 - 提高缓存命中率的配置与最佳实践
在现代大型语言模型(LLM)部署架构中,缓存系统扮演着至关重要的角色。随着LLM应用规模的不断扩大和用户需求的持续增长,如何构建高效、可靠的缓存架构成为系统性能优化的核心挑战。Redis作为业界领先的内存数据库,因其高性能、丰富的数据结构和灵活的配置选项,已成为LLM部署中首选的缓存解决方案。
|
6月前
|
存储 缓存 NoSQL
Redis专题-实战篇二-商户查询缓存
本文介绍了缓存的基本概念、应用场景及实现方式,涵盖Redis缓存设计、缓存更新策略、缓存穿透问题及其解决方案。重点讲解了缓存空对象与布隆过滤器的使用,并通过代码示例演示了商铺查询的缓存优化实践。
286 1
Redis专题-实战篇二-商户查询缓存
|
5月前
|
缓存 运维 监控
Redis 7.0 高性能缓存架构设计与优化
🌟蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕Redis 7.0高性能缓存架构,探索函数化编程、多层缓存、集群优化与分片消息系统,用代码在二进制星河中谱写极客诗篇。
|
6月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
531 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
6月前
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。
|
6月前
|
存储 缓存 监控
Redis分区的核心原理与应用实践
Redis分区通过将数据分散存储于多个节点,提升系统处理高并发与大规模数据的能力。本文详解分区原理、策略及应用实践,涵盖哈希、范围、一致性哈希等分片方式,分析其适用场景与性能优势,并探讨电商秒杀、物联网等典型用例,为构建高性能、可扩展的Redis集群提供参考。
321 0
|
10月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
10月前
|
缓存 NoSQL Java
Redis+Caffeine构建高性能二级缓存
大家好,我是摘星。今天为大家带来的是Redis+Caffeine构建高性能二级缓存,废话不多说直接开始~
1345 0