浅谈读并发的场景及优化方案

2 篇文章 0 订阅
2 篇文章 0 订阅
  • 前言

    • 写此文的主要原因源于工作中一次接口的并发压测出现OOM,经过dump堆日志、线程日志以及各种代码分析,最后发现居然是因为接口响应包太大(原响应包为2556k,中间优化代码将响应包压缩至400k左右,也只是稍微延长了oom的时间),达到网络带宽(内网百兆路由器)传输瓶颈,最终导致GC无法正常回收.
    • 本文主要针对读并发进行分析讨论.
  • 常见问题分析

    • 平时工作中遇到不少高并发读的场景,主要问题的表现要么是tps上不去要么是Full GC频繁,再严重点就OOM,线上遇到OOM,一首<凉凉>应该比较应景☺.

      Tps为什么上不去?

       tps反应的是系统每秒处理的事务数,这个数字上不去,就是接口响应慢,主要原因有下面两种:
      
      CPU瓶颈
        读取redis、代码循环、反序列化等等这些看起来很普通的操作,在高并发情况下都会成为cpu性能瓶颈.    
      
      在这里插入图片描述
      上图是压测接口的抽样分析,反序列化、redis操作占用了大量的cpu资源,该接口仅仅是读redis取数据然后反序列化返回.
      IO瓶颈
        主要分为磁盘io和网络io.
        磁盘io主要体现在数据库和程序日志的打印,读场景比较好解决.
        网络io是个很容易被忽视的场景,下图就是内网针对带宽场景的压测,换了千兆网卡,每秒600m左右流量输出,如果是百兆网卡,一首<凉凉>再次送给你.
      

      Full GC场景

       * 需要占用连续内存的大对象直接进入老年代
       * Minor gc达到一定次数还无法回收的对象进入老年代
       * 虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
       
       以上这些场景触发之前或之后都可能会引起full gc,而full gc次数多通常意味着编码不规范或者程序设计不合理.
      

      上图也是压测了同一个接口,响应数据包2556kb,压测了大概2小时左右,minor gc 5219次,full gc了18次,如果是生产出现这种效果,晚上又可以开心加班写bug了
  • 优化与建议

    * 针对IO场景,最典型的读场景使用缓存中间件,如redis、mongodb,高并发情况下需考虑防穿透、雪崩、击穿.  
       
    * 针对CPU场景,主要从降低接口执行的时间复杂度这方面考虑,如最少次数的循环,最少次数的中间件调用,必要时可考虑用空间换时间
    
    * 针对网络带宽场景,写代码尽量做到物尽其用,接口不要返回无关的信息,如果确实有这种返回数据包大还有高并发的需求,可以考虑让调用方做缓存(配合异步通知更新缓存)并且服务端做限流,或者直接binlog做数据副本
    
  • 附上一段防穿透的伪代码

    • 代码中的锁使用互斥锁即可,针对加锁对象需考虑使用场景 ,比如下面这个示例的加锁如果在高并发下穿透,会造成大量的锁等待,这时候需要根据实际业务场景考虑加锁对象或者减少获取锁等待时间

      `public Student getById(Integer id) {
            //从redis查询数据
            Student temp = get(id);
            if (temp == null) {
               try {
                  //如果缓存为空则查询数据库,避免并发穿透db,此处加锁
                  if (idLock.tryLock() || idLock.tryLock(CacheConstant.LOCK_TIMEOUT, TimeUnit.SECONDS)) {
                  		//双检模式
                		temp = get(id);
                  		if (temp != null) {//查到直接返回
                      		return temp;
                  		}
                  		//查询防穿透key,查到直接返回
                  		String expireKey = getIdExpireKey(id);
                  		if (StringUtils.isNotEmpty(expireKey)) {
                      		return null;
                  		}
                  		//未查到则开始查询数据库
                  		temp  = studentService.getById(id);
                        if (temp  == null) {
                            //如果数据库查询为空,则设置一个空对象
                           setIdExpireKey(id, STUDENT_IDS_EXPIRE_TIME);
                           return temp;
                         }
                  		//查询到数据则更新缓存
                 		hset(temp);
             	   }
        		} catch (Exception e) {
            	  log.error("StudentRedisUtils--getById:{}", ExceptionUtils.getFullStackTrace(e));
      		    } finally {
             		 if (idLock.isHeldByCurrentThread()) {
                		  idLock.unlock();
           	  	     } 
      	    	}
            }
      return temp;
      }`
      

以上就是本次分享了,有任何疑问可以留言讨论.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值