開心一刻
昨晚和一個朋友聊天
我:處對象嗎,咱倆試試?
朋友:我有對象
我:我不信,有對象不公開?
朋友:不好公開,我當?shù)男∪?/p>
問題背景
程序在生產(chǎn)環(huán)境穩(wěn)定的跑著
直到有一天,公司執(zhí)行組件漏洞掃描,有漏洞的?jar?要進行升級修復(fù)
然后我就按著掃描報告將有漏洞的?jar?修復(fù)到指定的版本
自己在開發(fā)環(huán)境也做了主流業(yè)務(wù)的測試,沒有任何異常,穩(wěn)如老狗
提測之后,測試小姐姐也沒測出問題,一切都是這么美好
結(jié)果升級到生產(chǎn)后,生產(chǎn)日志瘋狂報錯:?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,執(zhí)行如下?test?方法即可進行測試
項目很簡單,通過?redisson-spring-boot-starter?引入?redisson?
扯點題外的東西,關(guān)于?redisson-spring-boot-starter?的配置方式
配置方式有很多種,官網(wǎng)文檔做了說明,有 4 種配置方式:README.md
方式 1:
方式 2:
方式 3:
方式 4:
如果 4 種方式都配置,最終生效的是哪一種?
樓主我此刻只想給你個大嘴巴子,怎么這么多問題?
既然你們都提出來了,那我就不能不管,誰讓我太愛你們了,盤它!
從哪盤,怎么盤?
源碼之下無密碼,我們就從源碼去盤,找到自動配置類
?。P(guān)于?spring-boot?的自動配置,參考:springboot2.0.3源碼篇 - 自動配置的實現(xiàn),發(fā)現(xiàn)也不是那么復(fù)雜)
?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?,此版本在開發(fā)、測試、生產(chǎn)環(huán)境都是能正常跑的
把?redisson-spring-boot-starter?升級到?3.15.0?之后,在開發(fā)、測試環(huán)境運行正常,上生產(chǎn)后則報錯:?org.redisson.client.RedisException: ERR unknown command 'WAIT'?
因為沒做任何的業(yè)務(wù)代碼修改,所以問題肯定出在升級后的?redisson-spring-boot-starter?,你說是不是?
那這個問題肯定有前輩碰到過,我們?nèi)?redisson?的issues看看
直接搜索關(guān)鍵字:?WAIT?
點進去你就會發(fā)現(xiàn)
這不就是我們的生產(chǎn)異常?
我立馬找運維確認,生產(chǎn)確實用的是阿里云?redis?,并且是代理模式!
出于嚴謹,我們還需要對:?3.14.0?是正常的,?3.14.1?有異常 這個結(jié)論進行驗證
因為公司未提供測試環(huán)境的阿里云?redis?,所以樓主只能自掏腰包購買一套最低配的阿里云?redis?
就沖樓主這認真負責的態(tài)度,你們不得一鍵三連?
我們來看下驗證結(jié)果
結(jié)論確實是對的
樓主又去阿里云翻了一下手冊
我們是不是可以把問題范圍縮小了
?redisson??3.14.0?未引入?wait?命令,而?3.14.1?引入了,所以問題產(chǎn)生了!
但這只是我們的猜想,我們需要強有力的支撐,找誰了?肯定還得是源碼!
WAIT 源碼分析
我們先跟?3.14.0?
我們可以看到,真正發(fā)送給?redis-server?執(zhí)行的命令不只是加鎖的腳本,還有?WAIT?命令!
只是因為異步執(zhí)行命令,只關(guān)注了加鎖腳本的執(zhí)行結(jié)果,而并沒有關(guān)注?WAIT?命令的執(zhí)行結(jié)果
也就是說?3.14.0?也有?WAIT?命令,并且在阿里云?redis?的代理模式下執(zhí)行是失敗的,只是?redisson?并沒有去管?WAIT?命令的執(zhí)行結(jié)果
所以只要加鎖命令執(zhí)行是成功的,那么?Redisson?就認為執(zhí)行結(jié)果是成功的
這也就是?3.14.0?執(zhí)行成功,沒有報異常的原因
我們再來看看?3.14.1?
真正發(fā)送給?redis-server?執(zhí)行的命令有加鎖腳本,也有?WAIT?命令
兩個命令的執(zhí)行結(jié)果都有關(guān)注
加鎖腳本執(zhí)行是成功的,?redis?已經(jīng)有對應(yīng)的記錄
而阿里云?redis?的代理模式是不支持?WAIT?命令,所以?WAIT?命令是執(zhí)行失敗的
而最終的執(zhí)行結(jié)果是所有命令的執(zhí)行結(jié)果,所以最終執(zhí)行結(jié)果是失敗的!
問題處理
那么如何正確的升級到生產(chǎn)環(huán)境了?
1、將?redisson?版本降到?3.14.0?
不去關(guān)注?WAIT?命令的執(zhí)行結(jié)果,相當于沒有?WAIT?命令
這個可能產(chǎn)生什么問題(?redisson?引入?WAIT?命令的意圖),轉(zhuǎn)動你們智慧的頭腦,評論區(qū)告訴我答案
2、阿里云?redis?改成直連模式
總結(jié)
1、環(huán)境一致的重要性
測試環(huán)境一定要保證和生產(chǎn)環(huán)境一致
否則就會出現(xiàn)和樓主一樣的問題,其他環(huán)境都沒問題,就生產(chǎn)有問題
環(huán)境不一致,排查問題也很棘手
2、?Redisson?很早就會附加?WAIT?命令,只是從?3.14.1?開始才關(guān)注?WAIT?命令的執(zhí)行結(jié)果
3、對于維護中的老項目,代碼能不動就不動,配置能不動就不動文章來源:http://www.zghlxwxcb.cn/news/detail-705823.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-705823.html
到了這里,關(guān)于記一次 Redisson 線上問題 → ERR unknown command 'WAIT' 的排查與分析的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!