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

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 低版本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"


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
3天前
|
存储 缓存 NoSQL
Redis多级缓存指南:从前端到后端全方位优化!
本文探讨了现代互联网应用中,多级缓存的重要性,特别是Redis在缓存中间件的角色。多级缓存能提升数据访问速度、系统稳定性和可扩展性,减少数据库压力,并允许灵活的缓存策略。浏览器本地内存缓存和磁盘缓存分别优化了短期数据和静态资源的存储,而服务端本地内存缓存和网络内存缓存(如Redis)则提供了高速访问和分布式系统的解决方案。服务器本地磁盘缓存因I/O性能瓶颈和复杂管理而不推荐用于缓存,强调了内存和网络缓存的优越性。
22 1
|
5天前
|
缓存 NoSQL Redis
深度解析Redis的缓存双写一致性
【4月更文挑战第20天】
30 1
|
5天前
|
缓存 NoSQL 算法
Redis入门到通过之解决Redis缓存击穿、缓存穿透、缓存雪崩
Redis入门到通过之解决Redis缓存击穿、缓存穿透、缓存雪崩
12 0
|
5天前
|
存储 缓存 NoSQL
Redis入门到通关之解决Redis缓存一致性问题
Redis入门到通关之解决Redis缓存一致性问题
19 0
|
5天前
|
存储 缓存 NoSQL
Redis入门到通关之Redis缓存数据实战
Redis入门到通关之Redis缓存数据实战
14 0
|
5天前
|
缓存 NoSQL Java
Springboot 多级缓存设计与实现
Springboot 多级缓存设计与实现
|
2月前
|
缓存 NoSQL 安全
【Redis】缓存穿透
【Redis】缓存穿透
30 0
|
2月前
|
缓存 监控 NoSQL
解析Redis缓存雪崩及应对策略
解析Redis缓存雪崩及应对策略
|
2月前
|
存储 缓存 Java
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践
|
7天前
|
存储 缓存 运维
软件体系结构 - 缓存技术(5)Redis Cluster
【4月更文挑战第20天】软件体系结构 - 缓存技术(5)Redis Cluster
137 10