当前位置: 首页 > 后端技术 > Java

同事乱用Redis,我真的醉了...

时间:2023-04-01 17:22:06 Java

来源:https://my.oschina.net/xiaomu...首先说一下问题现象:内网沙箱环境的API已经卡了1周了,一开始所有api都没有反应。当测试抱怨环境响应慢时,我们重启应用,应用恢复正常,所以没有处理。但是后来,出问题的频率越来越频繁,越来越多的同事开始抱怨,于是觉得代码可能有问题,开始排查。首先我发现开发的本地ide没发现什么问题。应用卡住的时候,数据库和redis都是正常的,也没有特别的错误日志。开始怀疑是沙盒环境机器的问题(测试环境本身就很脆弱!_!),于是在服务器上sshd,执行如下命令top,发现机器还是正常的,于是我打算先查看jvm堆栈信息,看看问题所在。应用程序消耗更多资源。线程执行top-H-p12798找到前3个比较耗资源的线程jstack查看堆内存jstack12798|grep1279916进制31ff没看出什么问题,也看看上下10行,于是执行到请参阅某些线程处于锁定状态。但是没有业务相关的代码,忽略。这时,我毫无头绪。想想看。为了保护事故现场,我决定放弃被卡住的机器。我首先转储了问题进程的所有堆内存,然后以调试模式重新启动了测试环境应用程序。当问题再次出现时,我打算直接远程调试问题机器。第二天问题又出现了,于是通知运维nginx转发并移除这个有问题的应用,自己远程调试tomcat。随便找了一个接口,断点就在接口的入口处。悲剧开始了,什么都没有发生!API等待服务响应,不进入断点。这个时候有点迷茫,先冷静一下,在入口前的aop处下断点,重新调试。这次我进入了断点。f8N次后,发现主卡在执行redis命令时卡住了。另外,Redis系列面试题和答案都整理好了。微信搜索Java技术栈,后台发送:面试,可以在线阅读。继续跟随,终于在jedis的一个地方发现了一个问题:/***返回一个Jedis实例作为Redis的连接。该实例可以是新创建的,也可以是从*池中检索的。**@returnJedis实例准备好包装到{@linkRedisConnection}中。*/protectedJedisfetchJedisConnector(){尝试{if(usePool&&pool!=null){returnpool.getResource();}Jedisjedis=newJedis(getShardInfo());//强制初始化(参见Jedis问题#82)jedis.connect();返回绝地武士;}catch(Exceptionex){thrownewRedisConnectionFailureException("无法获取Jedis连接",ex);}}线程在pool.getResource()之后开始waitpublicTgetResource(){try{returninternalPool.borrowObject();}catch(Exceptione){thrownewJedisConnectionException("无法从池中获取资源",e);}}returninternalPool.borrowObject();this代码应该是一个租赁代码,后面是publicTborrowObject(longborrowMaxWaitMillis)throwsException{this.assertOpen();AbandonedConfigac=this.abandonedConfig;如果(ac!=null&&ac.getRemoveAbandonedOnBorrow()&&this.getNumIdle()<2&&this.getNumActive()>this.getMaxTotal()-3){this.removeAbandoned(ac);}PooledObjectp=null;booleanblockWhenExhausted=this.getBlockWhenExhausted();长等待时间=0L;while(p==null){布尔创建=false;if(blockWhenExhausted){p=(PooledObject)this.idleObjects.pollFirst();如果(p==null){创建=true;p=this.create();}if(p==null){if(borrowMaxWaitMillis<0L){p=(PooledObject)this.idleObjects.takeFirst();}else{waitTime=System.currentTimeMillis();p=(PooledObject)this.idleObjects.pollFirst(borrowMaxWaitMillis,TimeUnit.MILLISECONDS);waitTime=System.currentTimeMillis()-等待时间;}}if(p==null){thrownewNoSuchElementException("等待空闲对象超时");}有一段代码if(p==null){if(borrowMaxWaitMillis<0L){p=(PooledObject)this.idleObjects.takeFirst();}else{waitTime=System.currentTimeMillis();p=(PooledObject)this.idleObjects.pollFirst(borrowMaxWaitMillis,TimeUnit.MILLISECONDS);waitTime=System.currentTimeMillis()-等待时间;}}borrowMaxWaitMillis<0会一直执行,然后一直循环。我开始怀疑这个值没有配置。查找redispool配置,发现确实没有配置MaxWaitMillis。配置后else代码也是Exception,无法解决问题。继续F8publicEtakeFirst()throwsInterruptedException{this.lock.lock();对象var2;尝试{对象x;while((x=this.unlinkFirst())==null){this.notEmpty.await();}var2=x;}最后{this.lock.unlock();}returnvar2;}在这里找到lockword,开始怀疑是不是所有的请求API都被屏蔽了,于是又在ssh服务器上安装了arthas,(Arthas是阿里巴巴开源的Java诊断工具)执行thread命令,发现大量的http-nio线程等待状态,http-nio-8083-exec-这个线程其实就是http请求出来的tomcat线程。随机找一个线程检查堆内存线程-428。这个确认是api的连续循环的问题。是redis获取连接的代码导致的。解释这个内存代码。等待@53e5504e这个对象释放锁,于是jstack全局搜索了53e5504e,没有找到这个对象所在的线程。自那以后。问题原因可以确定是redis连接获取的问题。但不确定是什么原因导致连接失败。再次执行arthas的thread-b(thread-b,找出当前阻塞其他线程的线程)没有结果。这和我想的不一样。应该可以找到阻塞的线程,于是看了下这个命令的文档,发现了下面这句话。好吧,我们恰好是后者。...再次整理思路。这次修改redis池配置,设置连接超时为2s,然后观察问题再次出现时应用做了什么。添加一些配置JedisConnectionFactoryjedisConnectionFactory=newJedisConnectionFactory();......JedisPoolConfigconfig=newJedisPoolConfig();config.setMaxWaitMillis(2000);......jedisConnectionFactory.afterPropertiesSet();重新启动服务并等待。...又过一天,再次恢复ssh服务器,检查tomcataccesslog,发现大量api请求显示500,org.springframework.data.redis.RedisConnectionFailureException:CannotgetJedisconnection;嵌套异常是redis.clients.jedis.exceptions.JedisConnectionException:无法从org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:140)atorg.springframework.data的池中获取资源.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:229)在org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:57)在org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:128)在org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:91)在org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:78)在org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:177)在org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:152)在org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:85)atorg.springframework.data.redis.core.DefaultHashOperations.get(DefaultHashOperations.java:48)找到第一次出现500的出处,找到以下代码.......Cursorc=stringRedisTemplate.getConnectionFactory().getConnection().scan(options);while(c.hasNext()){...,,}分析这段代码,stringRedisTemplate.getConnectionFactory()。getConnection()获取到pool中的redisConnection后,就没有后续操作了。也就是说此时redis连接池中的链接在被租用后并没有释放或者归还连接池。虽然业务已经处理完毕,redisConnection处于空闲状态,但是在发现这个问题后,池中的redisConnection并没有恢复到正常应该的空闲状态总结:springstringRedisTemplate对redis的常规操作做了一些封装,但是不支持像ScanSetNx这样的命令。这时候需要获取一些特殊命令的jedisConnection。不推荐使用stringRedisTemplate.getConnectionFactory().getConnection()我们可以使用stringRedisTemplate.execute(newRedisCallback(){@OverridepublicCursordoInRedis(RedisConnectionconnection)throwsDataAccessException{returnconnection.scan(options);}});执行或使用RedisConnectionUtils.releaseConnection(conn,factory);释放连接。同时,不推荐在redis中使用keys命令。redispool的配置要搭配得当。否则,出现问题时,没有错误日志,也没有错误报告,定位难度很大。近期热点文章推荐:1.1,000+Java面试题及答案(2021最新版)2.别在满屏的if/else中,试试策略模式,真的很好吃!!3.操!Java中xx≠null的新语法是什么?4、SpringBoot2.5发布,深色模式太炸了!5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!