前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Dubug】bitField 引发的栈溢出排错记

【Dubug】bitField 引发的栈溢出排错记

作者头像
玛卡bug卡
发布2022-12-18 12:04:27
6361
发布2022-12-18 12:04:27
举报
文章被收录于专栏:Java后端修炼Java后端修炼

1、背景

前期因为布隆过滤器的实现需求,导入了 redisson 的依赖,后面项目需求迭代,需要用到 redis 的 bitmap 来做签到信息的存储,并且需要提供读取每月签到记录的功能,这里需要用 bitField 方法将位信息读取成 Long 数值,之后进行移位操作得到当月每天的签到情况,问题代码如下:

对于 bit 的 set 操作是没问题的,但是用到这个 bitField 就出问题了。

项目的依赖版本信息如下:

依赖

版本

springboot

2.4.12

spring-data-redis

2.4.14

lettuce-core

6.0.8.RELEASE

redisson-spring-boot-starter

3.11.5

2、现象

做单元测试的时候发现在 bitField 方法会栈溢出,看堆栈信息是递归调用自己了:

3、定位

在图 1 的 136 行上打个断点 debug 看下,跟着 stringRedisTemplate.opsForValue() 方法可以来到 DefaultValueOperations 的 bitField 方法,398 行是将 key 转换为字节数组形式可以直接跳过,我们直接在 399 行进去看看。

进到 AbstractOperations 的 execute 方法,接着进到 RedisTemplate 的 execute 方法,然后进入重载 execute 方法,重点看下这个方法。

关键是这里的工厂设置,可以看到这里是 redisson 的工厂。

前面是一些判断以及工厂的设置,209、214 行是获取连接的操作,这里可以看到 conn 获取的是 RedissonConnection 对象,而 connToUse 获取的是 DefaultStringRedisConnection 连接对象。

继续单步下去可以看到这里调用了 doInRedis 方法,并且传进去的是 DefaultStringRedisConnection 。

跟进去可以看到 doInRedis 实际上是个回调方法,回调到一开始的 connection.bitField 方法。

继续跟进去来到了 DefaultStringRedisConnection 的 bitField 方法,这里我们可以看到 delegate 是一个 RedissonConnection 对象,我们再跟进看看。

来到的是接口的默认方法 bitField,再看看这里的 stringCommands 方法返回的是什么对象,

stringCommands 方法实际上返回的还是 RedissonConnection,意味着stringCommands().bitField(xx) 继续执行下去一直是在调用自己,别忘了我们刚刚是怎么进来的,就是在 delegate 为 RedissionConnection 的时候进到了这个接口的默认方法。

那么到这里我们可以发现,实际上是在接口的默认方法这里发生了递归调用,并且没有停止条件,导致最后的栈溢出,这一点也跟我们的堆栈信息是一致的。

4、验证

那么明明 delegate 是 RedissonConnection 对象,为什么会进到接口的默认方法呢?只有一种可能就是其中没有对接口进行实现。

为了验证我们看看 redissonConnection 中有没有实现这个方法:

搜一下可以发现只有三个 bit 开头的方法,并没有 bitField,这就导致只能循环调用到接口的默认方法。

这里对比之下我们在调用 set 方法时是没问题的,我们同样探究以下它的执行过程。首先是业务代码,就是一行简单的调用:

接着我们跟进去看 set 方法,可以看到跟 bitField 类似,进到 execute 看看。

接着经过一系列的 execute 调用、重载到真正执行的方法 RedisTemplate.execute(xx)。

可以看到这里的 connToExpose 也是 DefaultStringRedisConnection,接着跟进 doInRedis 方法。

依旧是执行回调方法,进去 connection.set 方法看看。

来到了 DefaultStringRedisConnection 的 set 方法,可以看到这里的 delegate 同样是 RedissonConnection 。

跟进去可以看到,这里跟 bitField 不同了,不是进到接口的默认方法实现,而是到了 RedissonConnection 的 set 实现,也就代表着这里可以调用里面的执行流程了,不会无休止地调用接口的默认方法。

其实到这里已经可以结束了,但为了过程的完整我们继续跟下去。跟到 write 方法中可以看到这里是用到了异步的写入,我们跟进这个方法。

可以看到这里只是注册了异步的请求信息,并没有等待获取结果。

回到 write 方法继续往下走,实际上获取结果是在 sync 方法这里,进行同步等待。

跟进可以看到这里返回的是 syncFuture 的结果,看名字可以猜到这个方法是同步的。

进到方法里面可以看到是调用刚刚注册好异步事件的服务的 get 方法进行结果获取。

可以看到这里会 await,直到有结果会被唤醒进入下面的获取操作,getNow 里面就是获取返回值了。

到这里 set 的操作就结束了,对比可以发现,正是因为 redisson 中没有实现 bitField 方法才导致的递归调用默认方法,进而导致栈溢出。

5、解决

1)第一种解决方案

翻看 redisson 的 issue 记录可以发现在 2021 年提出 RedissonConnection 中缺少部分方法这个问题,那么

可以看到在这一次的 commit 中对缺失的方法进行实现,其中就包括 bitField 方法。

对照版本看一下,在 3.15.6 已经应用上了,也就是说如果使用的是 3.15.6 之后的版本就不存在这个问题了。

2)第二种解决方案

实际上在未引入 redisson 之前这个方法是可用的,因为在 yml 文件中已经做了 lettuce 连接池的相关配置,那么会自动配置到 lettuce 工厂中。

从 debug 结果也可以看到,获取到的是 lettuceConnectionFactory,那么后续的 bitField 自然就没问题了,问题是引入了 redisson 之后工厂的实现就被替换为 redisson 了。

如果要在当前版本解决这个问题,就需要对那些 issue 中提到的缺失的方法进行引导,让他们找到对应的实现。

简而言之,如果连接工厂是 lettuce 或者 jedis 就可以正常使用,因为其中有对 bitField 的实现,那么我们只要手动配置一个 lettuce 连接工厂即可,并且这不会影响到使用 redisson 的初衷——使用其中的布隆过滤器实现。

代码语言:javascript
复制
package cn.gpnusz.userservice.config;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;

@Configuration
public class LettuceConfig {
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private Integer port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.database}")
    private Integer database;
    @Value("${spring.redis.lettuce.pool.max-active}")
    private Integer maxActive;
    @Value("${spring.redis.lettuce.pool.max-idle}")
    private Integer maxIdle;
    @Value("${spring.redis.lettuce.pool.max-wait}")
    private Long maxWait;
    @Value("${spring.redis.lettuce.pool.min-idle}")
    private Integer minIdle;

    @Bean
    public LettuceConnectionFactory lettuceConnectionFactory() {
        GenericObjectPoolConfig<Object> genericObjectPoolConfig = new GenericObjectPoolConfig<>();
        genericObjectPoolConfig.setMaxIdle(maxIdle);
        genericObjectPoolConfig.setMinIdle(minIdle);
        genericObjectPoolConfig.setMaxTotal(maxActive);
        genericObjectPoolConfig.setMaxWaitMillis(maxWait);
        genericObjectPoolConfig.setTimeBetweenEvictionRunsMillis(100);
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setDatabase(database);
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);
        redisStandaloneConfiguration.setPassword(RedisPassword.of(password));
        LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
                .poolConfig(genericObjectPoolConfig)
                .build();
        return new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfig);
    }
}

6、总结

① 全面理解新特性。实际上我们会发现,正是 JDK8 之后接口中多了默认方法的实现,一些老版本的工具包的类才得以兼容,但是今天遇到的这个异常还真就是因为默认方法的实现,导致了找不到实现类一直调接口自己的默认方法,只能说有利有弊吧;

② 尽量选择稳定版本。对于工具/框架的使用尽量使用修复已知 BUG 的稳定版本,否则因为一个小缺陷可能就要花上大半天去解决;

③ 持续锻炼排错能力。对于工具/框架内部某些方法的报错信息,还是要坚持打断点去调试,经过一轮探索之后的收获远强于自己去网上 Copy 相应的解决方案,在这个过程中对排错能力也有较大提升。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-10-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java后端修炼 微信公众号,前往查看

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

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、背景
  • 2、现象
  • 3、定位
  • 4、验证
  • 5、解决
    • 1)第一种解决方案
      • 2)第二种解决方案
      • 6、总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档