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

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

时间:2023-04-01 22:32:19 Java

首先说一下问题现象:内网沙箱环境API卡死1周,所有API无响应。一开始,测试抱怨环境响应慢,我们重启应用,应用恢复正常,就什么都不做。但是后来,出问题的频率越来越频繁,越来越多的同事开始抱怨,于是觉得代码可能有问题,开始排查。top命令首先发现开发的localide没有发现问题。应用卡住的时候,数据库和redis都是正常的,也没有特别的错误日志。开始怀疑是沙盒环境机器的问题(测试环境本身就很脆弱!_!),于是在服务器上sshd,执行如下命令top,发现机器还是正常的,于是我打算先查看jvm堆栈信息,看看问题所在。应用程序消耗更多资源。线程执行top-H-p12798查找前3个比较耗资源的线程jstack命令jstack查看堆内存jstack12798|grep12799hexadecimal31ff没看出什么问题,看上下10行,于是执行看到有部分线程处于锁状态。但是没有业务相关的代码,忽略。这时,我毫无头绪。想想看。为了保护事故现场,我决定放弃卡机,将问题进程的堆内存全部dump掉,然后以debug模式重启测试环境应用。本来打算等问题重现的时候直接调试问题机Tomcat。通过远程调试第二天问题又出现了,于是通知运维nginx转发并移除有问题的应用,自己远程调试tomcat。随便找了一个接口,断点就在接口的入口处。悲剧开始了,什么都没有发生!API等待服务响应,不进入断点。这个时候有点迷茫,先冷静一下,在入口前的aop处下断点,重新调试。这次我进入了断点。f8N次后,发现主卡在执行redis命令时卡住了。继续跟随,终于在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();这段代码应该是租赁代码,后面跟着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,时间单位.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。开锁();}returnvar2;}当我发现这里有lock这个词的时候,我开始怀疑是不是所有的请求API都被屏蔽了,于是又在ssh服务器上安装了arthas。nio的线程等待状态,http-nio-8083-exec-这个线程其实就是http请求出来的tomcat线程。arthas随机找一个线程检查堆内存线程-428。这样可以确认问题出在api一直在转。这是redis获取connected代码导致的,解释这段内存代码,所有线程都在等待@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命令,合理搭配redis池的配置。否则,出现问题时,没有错误日志,也没有错误报告,定位难度很大。来源:my.oschina.net/xiaomu0082/blog/2990388