记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

这篇具有很好参考价值的文章主要介绍了记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

开心一刻

  昨晚和一个朋友聊天

  我:处对象吗,咱俩试试?

  朋友:我有对象

  我:我不信,有对象不公开?

  朋友:不好公开,我当的小三

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

问题背景

  程序在生产环境稳定的跑着

  直到有一天,公司执行组件漏洞扫描,有漏洞的 jar 要进行升级修复

  然后我就按着扫描报告将有漏洞的 jar 修复到指定的版本

  自己在开发环境也做了主流业务的测试,没有任何异常,稳如老狗

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  提测之后,测试小姐姐也没测出问题,一切都是这么美好

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  结果升级到生产后,生产日志疯狂报错: org.redisson.client.RedisException: ERR unknown command 'WAIT' 

  完整的异常堆栈信息类似如下

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析
org.redisson.client.RedisException: ERR unknown command 'WAIT'. channel: [id: 0x84149c6e, L:/192.168.2.40:3592 - R:/47.98.21.100:6379] command: (WAIT), params: [1, 1000]

    at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:346)
    at org.redisson.client.handler.CommandDecoder.decodeCommandBatch(CommandDecoder.java:247)
    at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:189)
    at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:117)
    at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:102)
    at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:508)
    at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366)
    at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.lang.Thread.run(Thread.java:748)
View Code

  突然来个这个鬼玩意,脑阔有点疼

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  先让运维同事回滚,然后就开始了我的问题排查之旅

问题排查与处理

  项目搭建

  示例代码:redisson-spring-boot-demo,执行如下 test 方法即可进行测试

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  项目很简单,通过 redisson-spring-boot-starter 引入 redisson 

  扯点题外的东西,关于 redisson-spring-boot-starter 的配置方式

  配置方式有很多种,官网文档做了说明,有 4 种配置方式:README.md

  方式 1:

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  方式 2:

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  方式 3:

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  方式 4:

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  如果 4 种方式都配置,最终生效的是哪一种?

  楼主我此刻只想给你个大嘴巴子,怎么这么多问题?

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  既然你们都提出来了,那我就不能不管,谁让我太爱你们了,盘它!

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  从哪盘,怎么盘?

  源码之下无密码,我们就从源码去盘,找到自动配置类

  (关于 spring-boot 的自动配置,参考:springboot2.0.3源码篇 - 自动配置的实现,发现也不是那么复杂)

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

   RedissonAutoConfiguration 中有如下代码

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(RedissonClient.class)
public RedissonClient redisson() throws IOException {
    Config config = null;
    Method clusterMethod = ReflectionUtils.findMethod(RedisProperties.class, "getCluster");
    Method timeoutMethod = ReflectionUtils.findMethod(RedisProperties.class, "getTimeout");
    Object timeoutValue = ReflectionUtils.invokeMethod(timeoutMethod, redisProperties);
    int timeout;
    if(null == timeoutValue){
        timeout = 10000;
    }else if (!(timeoutValue instanceof Integer)) {
        Method millisMethod = ReflectionUtils.findMethod(timeoutValue.getClass(), "toMillis");
        timeout = ((Long) ReflectionUtils.invokeMethod(millisMethod, timeoutValue)).intValue();
    } else {
        timeout = (Integer)timeoutValue;
    }

    if (redissonProperties.getConfig() != null) {
        try {
            config = Config.fromYAML(redissonProperties.getConfig());
        } catch (IOException e) {
            try {
                config = Config.fromJSON(redissonProperties.getConfig());
            } catch (IOException e1) {
                throw new IllegalArgumentException("Can't parse config", e1);
            }
        }
    } else if (redissonProperties.getFile() != null) {
        try {
            InputStream is = getConfigStream();
            config = Config.fromYAML(is);
        } catch (IOException e) {
            // trying next format
            try {
                InputStream is = getConfigStream();
                config = Config.fromJSON(is);
            } catch (IOException e1) {
                throw new IllegalArgumentException("Can't parse config", e1);
            }
        }
    } else if (redisProperties.getSentinel() != null) {
        Method nodesMethod = ReflectionUtils.findMethod(Sentinel.class, "getNodes");
        Object nodesValue = ReflectionUtils.invokeMethod(nodesMethod, redisProperties.getSentinel());

        String[] nodes;
        if (nodesValue instanceof String) {
            nodes = convert(Arrays.asList(((String)nodesValue).split(",")));
        } else {
            nodes = convert((List<String>)nodesValue);
        }

        config = new Config();
        config.useSentinelServers()
            .setMasterName(redisProperties.getSentinel().getMaster())
            .addSentinelAddress(nodes)
            .setDatabase(redisProperties.getDatabase())
            .setConnectTimeout(timeout)
            .setPassword(redisProperties.getPassword());
    } else if (clusterMethod != null && ReflectionUtils.invokeMethod(clusterMethod, redisProperties) != null) {
        Object clusterObject = ReflectionUtils.invokeMethod(clusterMethod, redisProperties);
        Method nodesMethod = ReflectionUtils.findMethod(clusterObject.getClass(), "getNodes");
        List<String> nodesObject = (List) ReflectionUtils.invokeMethod(nodesMethod, clusterObject);

        String[] nodes = convert(nodesObject);

        config = new Config();
        config.useClusterServers()
            .addNodeAddress(nodes)
            .setConnectTimeout(timeout)
            .setPassword(redisProperties.getPassword());
    } else {
        config = new Config();
        String prefix = REDIS_PROTOCOL_PREFIX;
        Method method = ReflectionUtils.findMethod(RedisProperties.class, "isSsl");
        if (method != null && (Boolean)ReflectionUtils.invokeMethod(method, redisProperties)) {
            prefix = REDISS_PROTOCOL_PREFIX;
        }

        config.useSingleServer()
            .setAddress(prefix + redisProperties.getHost() + ":" + redisProperties.getPort())
            .setConnectTimeout(timeout)
            .setDatabase(redisProperties.getDatabase())
            .setPassword(redisProperties.getPassword());
    }
    if (redissonAutoConfigurationCustomizers != null) {
        for (RedissonAutoConfigurationCustomizer customizer : redissonAutoConfigurationCustomizers) {
            customizer.customize(config);
        }
    }
    return Redisson.create(config);
}
View Code

  谁先生效,一目了然!

  问题分析

  有点扯远了,我们再回到主题

   jar 未升级之前, redisson-spring-boot-starter 的版本是 3.13.6 ,此版本在开发、测试、生产环境都是能正常跑的

  把 redisson-spring-boot-starter 升级到 3.15.0 之后,在开发、测试环境运行正常,上生产后则报错: org.redisson.client.RedisException: ERR unknown command 'WAIT' 

  因为没做任何的业务代码修改,所以问题肯定出在升级后的 redisson-spring-boot-starter ,你说是不是?

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  那这个问题肯定有前辈碰到过,我们去 redisson 的issues看看

  直接搜索关键字: WAIT 

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  点进去你就会发现

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  这不就是我们的生产异常?

  我立马找运维确认,生产确实用的是阿里云 redis ,并且是代理模式!

  出于严谨,我们还需要对: 3.14.0 是正常的, 3.14.1 有异常 这个结论进行验证

  因为公司未提供测试环境的阿里云 redis ,所以楼主只能自掏腰包购买一套最低配的阿里云 redis 

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  就冲楼主这认真负责的态度,你们不得一键三连?

  我们来看下验证结果

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  结论确实是对的

  楼主又去阿里云翻了一下手册

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  我们是不是可以把问题范围缩小了

   redisson  3.14.0 未引入 wait 命令,而 3.14.1 引入了,所以问题产生了!

  但这只是我们的猜想,我们需要强有力的支撑,找谁了?肯定还得是源码!

  WAIT 源码分析

  我们先跟 3.14.0 

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  我们可以看到,真正发送给 redis-server 执行的命令不只是加锁的脚本,还有 WAIT 命令!

  只是因为异步执行命令,只关注了加锁脚本的执行结果,而并没有关注 WAIT 命令的执行结果

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  也就是说 3.14.0 也有 WAIT 命令,并且在阿里云 redis 的代理模式下执行是失败的,只是 redisson 并没有去管 WAIT 命令的执行结果

  所以只要加锁命令执行是成功的,那么 Redisson 就认为执行结果是成功的

  这也就是 3.14.0 执行成功,没有报异常的原因

  我们再来看看 3.14.1 

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  真正发送给 redis-server 执行的命令有加锁脚本,也有 WAIT 命令

  两个命令的执行结果都有关注

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

  加锁脚本执行是成功的, redis 已经有对应的记录

  而阿里云 redis 的代理模式是不支持 WAIT 命令,所以 WAIT 命令是执行失败的

  而最终的执行结果是所有命令的执行结果,所以最终执行结果是失败的!

  问题处理

  那么如何正确的升级到生产环境了?

  1、将 redisson 版本降到 3.14.0 

    不去关注 WAIT 命令的执行结果,相当于没有 WAIT 命令

    这个可能产生什么问题( redisson 引入 WAIT 命令的意图),转动你们智慧的头脑,评论区告诉我答案

  2、阿里云 redis 改成直连模式

总结

  1、环境一致的重要性

    测试环境一定要保证和生产环境一致

    否则就会出现和楼主一样的问题,其他环境都没问题,就生产有问题

    环境不一致,排查问题也很棘手

  2、 Redisson 很早就会附加 WAIT 命令,只是从 3.14.1 开始才关注 WAIT 命令的执行结果

  3、对于维护中的老项目,代码能不动就不动,配置能不动就不动

记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析文章来源地址https://www.toymoban.com/news/detail-705823.html

到了这里,关于记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 记一次线上问题引发的对 Mysql 锁机制分析

    最近双十一开门红期间组内出现了一次因 Mysql 死锁导致的线上问题,当时从监控可以看到数据库活跃连接数飙升,导致应用层数据库连接池被打满,后续所有请求都因获取不到连接而失败 整体业务代码精简逻辑如下: 数据库实例监控: 当时通过分析上游问题流量限流解决后

    2024年02月05日
    浏览(53)
  • 记一次Redisson连接k8s环境Redis报UnknownHostException-域名解析错误问题解决

    一直听说Redisson分布式锁好用,终于在项目上使用了!在本地测试完毕,一上测试环境,运维就反馈服务没起来,拉日志一看,是以下报错 主要报错信息是 于是乎,上github看了一下,发现这还是一个比较热门dns解析失败的问题,好多人都遇到了同样的问题。想着开源项目,这

    2024年02月04日
    浏览(45)
  • gitlab 16.x - ERR unknown command ‘HELLO‘

    gitlab部分操作报错500。通过Rails日志发现以下报错: 报错: RedisClient::CommandError ERR unknown command \\\'HELLO\\\' 日志报错是Redis执行HELLO命令报错,这个命令适用于指定RESP版本。 Gitlab官方文档要求16.0以上版本,需要使用Redis 6.x or 7.x Redis 6.x 开始支持RESP3 连接到Redis,执行Hello 2成功;执

    2024年04月26日
    浏览(20)
  • 记一次线上BUG排查过程

    1. 线上遇到一个非常奇怪的bug,为一个用户分配业务线类型后,该用户登录时,提示502,但其它的用户登录完全是正常的 2. 问题现象 3. 排查思路 先去看线上日志,看是否有error,但日志里边这个接口200正常返回 本地debug,也复现一样问题,在分配角色类型超过22个总数时就报

    2024年02月09日
    浏览(50)
  • 记一次线上kafka造成的事故

    背景:所有的原始数据均存储在mysql,mysql会通过binlog将数据同步至kafka消息队列,但是有人将mysql中的数据进行删除(大概有2、3年的数据),被删除的数据也通过binlog被同步至消息队列里导致大量消息积压,且该消息队列只有3个分区,最多3个线程消费,消费方即使过滤也远

    2024年02月13日
    浏览(43)
  • Caused by: io.lettuce.core.RedisCommandExecutionException: ERR unknown command `CONFIG`

    项目本机启动运行正常,打成jar包发布测试环境–公司测试服务器正常启动运行,项目上线前需要发布在客户购买的亚马逊的K8S服务器,同时客户在亚马逊购买了Redis服务器,PostgreSQL服务器,所以新建了配置文件,将代码构建为镜像推送至客户服务器,然后进行启动编译,连接数据库一

    2024年02月15日
    浏览(52)
  • 得物-Golang-记一次线上服务的内存泄露排查

    在风和日丽的一天,本人正看着需求、敲着代码,展望美好的未来。突然收到一条内存使用率过高的告警。 告警的这个项目,老代码是python的,最近一直在go化。随着go化率不断上升,发现内存的RSS使用率越飙越高。最终达到容器内存限制后,进程会自动重启。RSS如下图所示

    2024年02月04日
    浏览(58)
  • npm ERR! code 128npm ERR! An unknown git error occurrednpm ERR! command git --no-replace-objects l

    在安装vue-element-admin 项目所需依赖时,出现了如下报错信息 1、使用下面的命令,达到,把地址里的 ssh://git@ 换成 https:// 的目的 2、然后重新通过 npm install 安装项目依赖 效果图:      

    2024年02月12日
    浏览(46)
  • npm ERR code 128 npm ERR An unknown git error occurred npm ERR command git --no-replace-objects l

    在git上下载以来的vue-element-admin 安装依赖的时候报错。 以上这个报错是我最头疼的,弄了很久,网上查阅了各种文章,还是没有找到解决的办法,最后我通过自己解决了问题。 这个问题是ssh的报错,我们需要想办法把ssh改成https,这样就可以解决啦,那么下面我来介绍一下如

    2024年02月16日
    浏览(50)
  • 记一次pdjs时安装glob出现,npm ERR! code ETARGET和npm ERR! code ELIFECYCLE

    如往常一样,我使用pdjs来编译proto文件,但出现了以下报错: 大致就是pdjs的util在尝试执行` npm install glob@^7.2.1 escodegen@^1.13.0 `时出错了 尝试手动执行安装,` escodegen `被正确安装,但` glob@^7.2.1 `出错 也就是找不到符合条件的版本为7.2.1的包 执行` npm view glob versions `,出来了很

    2024年02月05日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包