首页 > 数据库 >记一次 Redisson 线上问题 → ERR unknown command 'WAIT' 的排查与分析

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

时间:2023-10-27 11:34:11浏览次数:43  
标签:netty Redisson java ERR unknown redisson io redisProperties config

开心一刻

  昨晚和一个朋友聊天

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

  朋友:我有对象

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

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

记一次 Redisson 线上问题 → ERR unknown command

问题背景

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

jar

jar

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

记一次 Redisson 线上问题 → ERR unknown command

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

记一次 Redisson 线上问题 → ERR unknown command

org.redisson.client.RedisException: 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

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

问题排查与处理

  项目搭建

  示例代码:redisson-spring-boot-demo,执行如下 test

记一次 Redisson 线上问题 → ERR unknown command

redisson-spring-boot-starter 引入 redisson

redisson-spring-boot-starter

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

  方式 1:

记一次 Redisson 线上问题 → ERR unknown command

  方式 2:

记一次 Redisson 线上问题 → ERR unknown command

  方式 3:

记一次 Redisson 线上问题 → ERR unknown command

  方式 4:

记一次 Redisson 线上问题 → ERR unknown command

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

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

记一次 Redisson 线上问题 → ERR unknown command

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

记一次 Redisson 线上问题 → ERR unknown command

  从哪盘,怎么盘?

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

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

记一次 Redisson 线上问题 → ERR unknown command

RedissonAutoConfiguration

记一次 Redisson 线上问题 → ERR unknown command

记一次 Redisson 线上问题 → ERR unknown command

@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

redisson 的issues看看

WAIT

记一次 Redisson 线上问题 → ERR unknown command

  点进去你就会发现

记一次 Redisson 线上问题 → ERR unknown command

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

redis

3.14.0 是正常的, 3.14.1

redis ,所以楼主只能自掏腰包购买一套最低配的阿里云 redis

记一次 Redisson 线上问题 → ERR unknown command

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

  我们来看下验证结果

记一次 Redisson 线上问题 → ERR unknown command

  结论确实是对的

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

记一次 Redisson 线上问题 → ERR unknown command

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

redisson  3.14.0 未引入 wait 命令,而 3.14.1

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

  WAIT 源码分析

3.14.0

记一次 Redisson 线上问题 → ERR unknown command

redis-server 执行的命令不只是加锁的脚本,还有 WAIT

WAIT

记一次 Redisson 线上问题 → ERR unknown command

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

Redisson

3.14.0

3.14.1

记一次 Redisson 线上问题 → ERR unknown command

redis-server 执行的命令有加锁脚本,也有 WAIT

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

记一次 Redisson 线上问题 → ERR unknown command

redis

redis 的代理模式是不支持 WAIT 命令,所以 WAIT

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

  问题处理

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

redisson 版本降到 3.14.0

WAIT 命令的执行结果,相当于没有 WAIT

redisson 引入 WAIT

redis

总结

  1、环境一致的重要性

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

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

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

Redisson 很早就会附加 WAIT 命令,只是从 3.14.1 开始才关注 WAIT

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

记一次 Redisson 线上问题 → ERR unknown command



标签:netty,Redisson,java,ERR,unknown,redisson,io,redisProperties,config
From: https://blog.51cto.com/u_13423706/8052007

相关文章

  • NLTK debug记录——"[nltk_data] Error loading xxx"下载数据集失败
    问题:运行nltk.download("xxx")时遇到连接下载失败Error解决:在gitee上下载对应的.zip词库包(如,nltk_data/pakages/copora/目录下的下载链接);NLTK下载数据集时会自动搜索某些以./nltk_data/为结尾的目录(见附注),找到一个这样的目录并确保自己有写这个目录的权限,如果上一层目录下没有n......
  • 动态库加载失败:error while loading shared libraries: xxx.so: cannot open shared o
    lddmain|grepnot由0.1动态库的工作原理可知,只要把动态库libcalc.so的绝对路径添加到动态载入器ld-linux.so的搜索路径中,那么动态载入器就可以获取到动态库libcalc.so的绝对路径,接着就可以找到动态库文件libcalc.so,将动态库文件载入内存,然后就可以使用动态库里面的代码,最终可......
  • gerrit 将他人改动直接 打patch到自己代码上
    原文:https://blog.csdn.net/qq_21438461/article/details/131362485 在Linux中,patch命令用于将补丁文件应用到源代码文件中,从而实现对源代码的修改。patch命令的详细描述如下:patch命令用于将补丁文件应用到源代码文件中,以实现对源代码的修改。补丁文件通常是由开发者或者社区......
  • Redisson分布式锁主从一致性问题解决
    Redis联锁联锁(RedissonMultiLock)对象可以将多个RLock对象关联为一个联锁,实现加锁和解锁功能。每个RLock对象实例可以来自于不同的Redisson实例。如果负责储存分布式锁的某些Redis节点宕机以后,而且这些锁正好处于锁住状态,就会出现死锁问题。为了避免这种情况的发生,Redisson内部提供......
  • [ERROR FileContent--proc-sys-net-bridge-bridge-nf-call-iptables]: /proc/sys/net/
     永久解决方法:在/etc/sysctl.conf中添加:net.bridge.bridge-nf-call-ip6tables=1net.bridge.bridge-nf-call-iptables=1执行sysctl-p时刷新sysctl-p如果出现缺少文件的现象sysctl:cannotstat/proc/sys/net/bridge/bridge-nf-call-iptables:没有那个文......
  • [ERROR KubeletVersion]: the kubelet version is higher than the control plane ver
     kubeadm、kubelet、kubectl一起安装时,由于疏忽写成kubelet-1.27.3.0,结果版本变成kubelet-1.28了,导致报标题中的错误安装指定版本yum-yinstallkubeadm-1.27.3-0kubelet-1.27.3-0kubectl-1.27.3-0 原因:Kubelet和Kubeadm版本不一致导致查看kubelet和kube......
  • ERROR: Failed to Setup IP tables: Unable to enable SKIP DNAT rule
    1.报错信息#docker-composeup-dCreatingnetwork"2023-10-25-xcheck-net"withthedefaultdriverERROR:FailedtoSetupIPtables:UnabletoenableSKIPDNATrule:(iptablesfailed:iptables--wait-tnat-IDOCKER-ibr-7b14cc2d1da4-jRETURN:......
  • RuntimeError: default_program(24): error: extra text after expected end of numbe
    详细报错Traceback(mostrecentcalllast):File"eval_roberta_qa.py",line24,in<module>output=model(input_ids,attention_mask,token_type_ids)File"/home/rzhang/miniconda3/envs/vamc/lib/python3.7/site-packages/torch/nn/mo......
  • Linux/UNIX系统编程手册 Michael Kerrisk/孙剑 pdf电子版
    Linux/UNIX系统编程手册MichaelKerrisk/孙剑pdf电子版下载作者: MichaelKerrisk / 孙剑原作名: TheLinuxProgrammingInterface:ALinuxandUNIXSystemProgrammingHandbook出版年: 2014-1ISBN: 9787115328670连接提取码:hfkr内容确实组织得比APUE更适合系统学习......
  • pytest报错UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc3 in position 1
    报错UnicodeDecodeError:'utf-8'codeccan'tdecodebyte0xc3inposition11:invalidcontinuationbyte代码运行时,报错 可以看出是编码的问题,根据提示,有可能是__init__.py文件的问题,通过查看源代码:尝试改变"utf-8"为“gbk"路径:C:\python3.8\Lib\site-packages\inic......