开心一刻

  昨晚和一个朋友聊天

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

  朋友:我有对象

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

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

问题背景

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

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

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

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

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

  结果升级到生产后,生产日志疯狂报错: 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)

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

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

问题排查与处理

  项目搭建

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

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

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

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

  方式 1:

  方式 2:

  方式 3:

  方式 4:

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

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

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

  从哪盘,怎么盘?

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

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

   RedissonAutoConfiguration 中有如下代码

@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);
}

  谁先生效,一目了然!

  问题分析

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

   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 的issues看看

  直接搜索关键字: WAIT

  点进去你就会发现

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

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

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

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

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

  我们来看下验证结果

  结论确实是对的

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

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

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

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

  WAIT 源码分析

  我们先跟 3.14.0

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

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

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

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

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

  我们再来看看 3.14.1

  真正发送给 redis-server 执行的命令有加锁脚本,也有 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' 的排查与分析的更多相关文章

  1. 线上服务Java进程假死快速排查、分析

    引用 https://zhuanlan.zhihu.com/p/529350757 最近我们有一台服务器上的Java进程总是在运行个两三天后就无法响应请求了,具体现象如下: 请求业务返回状态码502, ...

  2. LogStash启动报错:<Redis::CommandError: ERR unknown command 'script'>与batch_count 的 配置

    环境条件: 系统版本:centos 6.8 logstash版本:6.3.2 redis版本:2.4 logstash  input配置: input { redis { host => &qu ...

  3. Spring session redis ERR unknown command 'CONFIG'

    部署线上服务启动报错 redis.clients.jedis.exceptions.JedisDataException: ERR unknown command 'CONFIG' Redis CON ...

  4. "ERR unknown command 'cluster'"

    golang 连接redis 集群提示  "ERR unknown command 'cluster'" redisdb = redis.NewClusterClient(& ...

  5. 【异常】redis.clients.jedis.exceptions.JedisDataException: ERR unknown command 'PSETEX'

    在spring中 针对 RedisTemplate类: private RedisTemplate<String, String> template; 当调用下面方法 template.o ...

  6. io.lettuce.core.RedisCommandExecutionException: ERR unknown command 'GEOADD'

    io.lettuce.core.RedisCommandExecutionException: ERR unknown command 'GEOADD' at io.lettuce.core.Exce ...

  7. 记一次 android 线上 oom 问题

    背景 公司的主打产品是一款跨平台的 App,我的部门负责为它提供底层的 sdk 用于数据传输,我负责的是 Adnroid 端的 sdk 开发. sdk 并不直接加载在 App 主进程,而是隔离在一个单 ...

  8. 线上mysql内存持续增长直至内存溢出被killed分析(已解决)

    来新公司前,领导就说了,线上生产环境Mysql库经常会发生日间内存爆掉被killed的情况,结果来到这第一天,第一件事就是要根据线上服务器配置优化配置,同时必须找出现在mysql内存持续增加爆掉的原因 ...

  9. 五月天的线上演唱会你看了吗?用Python分析网友对这场线上演唱会的看法

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:CDA数据分析师 豆瓣9.4分!这场线上演唱会到底多好看? 首先让我 ...

  10. Java线上应用故障之CPU占用高排查与定位

    最近线上频繁报警CPU空闲不足,故紧急排查后分享给大家 1.使用top命令,获取占用CPU最高的进程号 2.查看线程号对应的进程信息 命令:ps -ef|grep 22630 3.查看进程对应的线程信 ...

随机推荐

  1. 代码随想录算法训练营Day5 数组、链表复习

    数组部分 数组最重要的思维方式是双指针的使用. 快慢指针 在进行元素移除和元素操作时会使用两个for循环嵌套,此时时间复杂度为O(n²).在for循环中通过双指针(快慢指针)的使用可以使时间复杂度将为 ...

  2. mybatis拦截器实现数据权限

    前端的菜单和按钮权限都可以通过配置来实现,但很多时候,后台查询数据库数据的权限需要通过手动添加SQL来实现. 比如员工打卡记录表,有id,name,dpt_id,company_id等字段,后两个表示 ...

  3. go语言字符与字符串相关

    ASCII ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁 字母的一套单字节编码系统 字符 本质上来 ...

  4. 从输入URI到浏览器渲染中间都经历了什么

    这篇文章总共分为两个部分,第一部分我会把从输入url到浏览器渲染的整个流程给大致说一下.第二部分我就会一一介绍各个部分的详细作用. 一.从输入url到浏览器渲染的整个流程   1.DNS域名解析 2. ...

  5. 逍遥自在学C语言 | 指针函数与函数指针

    前言 在C语言中,指针函数和函数指针是强大且常用的工具.它们允许我们以更灵活的方式处理函数和数据,进而扩展程序的功能. 本文将介绍指针函数和函数指针的概念,并讲解一些常见的应用示例. 一.人物简介 第 ...

  6. Linux系统运维之MYSQL数据库集群部署(主从复制)

    一.介绍 Mysql主从复制,前段时间生产环境部署了一套主从复制的架构,当时现找了很多资料,现在记录下 二.拓扑图 三.环境以及软件版本 主机名 IP 操作系统 角色 软件版本 MysqlDB_Mas ...

  7. Python Django 零基础从零到一部署服务,Hello Django!全文件夹目录和核心代码!

    在这篇文章中,我将手把手地教你如何从零开始部署一个使用Django框架的Python服务.无论你是一个刚开始接触开发的新手,还是一个有经验的开发者想要快速了解Django,这篇教程都会为你提供一条清晰 ...

  8. H5 WebGL实现水波特效

    前言 零几年刚开始玩电脑的时候,经常在安装程序上看到一种水波特效,鼠标划过去的时候,就像用手在水面划过一样,感觉特别有意思.但是后来,就慢慢很少见过这种特效了.最近突然又想起了这种特效,于是开始折磨怎 ...

  9. LeetCode 周赛上分之旅 #33 摩尔投票派上用场

    ️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问. 学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思 ...

  10. 2023ccpc大学生程序设计竞赛-zx

    这次ccpc整体来说做题做的比较卡,第一个签到都wa了,后面几道中档题全都是至少wa一次才能过,这导致我们不仅罚时增加也导致需要大量时间修改代码,还有一个G题很可惜,当时只注意到B过题多所以有点被带歪 ...