二、高并发治理:5、Redis穿透到DB流量治理

本文分析了DB被打崩的原因,重点在于Redis穿透问题,包括未限流的查询DB和DB查不到时无法设置Redis,导致持续穿透。提出了两种优化策略:一是对穿透Redis的id进行去重加锁限流;二是即使DB不存在的值,也要设置Redis以防止持续穿透。详细阐述了策略的实现思路和代码示例。

一、为什么DB被打崩了

1.1 穿透到DB被打崩场景还原

代码逻辑:
Redis缓冲查询不到(被穿透),查询DB重建缓冲
但代码不健壮,发生大量并发线程,并发访问热点DB数据
此类情况,轻造成重复浪费,重则DB被打崩
  • 真实案例:突增了每秒一万次查询后,导致DB CPU100%,造成某个模块故障

    • 如图,某个时刻Redis被击穿,大量查询DB, 可以看到DB在一段时间除以不可用状态

1.2 原始代码有什么问题

public void loadData(){
    //1、待查询redis的id list
    List<String> ids;
    //2、从redis查询
    List<String> redisValues = redisUtils.queryByCacheKeys("业务场景"+ids);
    //3、如果请求的ID,在redis没有全部查询到,下沉到DB查询
    if(CollectionUtils.isEmpty(redisValues) || ids.size()>redisValues.size())    {
        //Redis 未查询到的id list
        List<String> redisMissIds;
        //查询db
        List<Object> objectList = dao.queryObjectFromMasterDB(redisMissIds);
        //4、回源从db设置redis缓冲
        redisUtils.batchSetCaches(buildCache(dos), redis超时时间);
    }
}

1.2.1 问题1:redis穿透的id,未限流全部下沉查询DB

redis穿透的id ,理论上热点数据是存在较多重复的,所以对重复数据应该进行加锁达到限流目的。
避免瞬时大并发重复下沉到DB查询,导致DB变慢,特别是一些耗时比较大的查询

1.2.2 问题2:DB查不到时,无法设置Redis,造成持续Redis穿透

如代码,只有db查询到的结果,才能回源设置了redis
当db查询结果是空时,无法在Redis设置缓冲,下次查询时候依然穿透到DB进行查询

二、优化解决策略

  • 策略1:穿透Redis的id,去重加锁限流后查询DB,确保同一时间一个id只访问一次DB
  • 策略2:DB不存在的值,也要设置Redis,防止持续穿透
  • 策略1解决思路如下
    • 如果6次sleep共计90ms后 Redis缓冲依然未建立,则查询DB,并回源重建redis缓冲
    • 后续redis穿透的的id,如果发现存在查询DB处理中标识,线程sleep后,再访问重建后Redis缓冲
      • 由于线程宝贵,sleep时间采用衰减策略,第一次sleep 20ms,后续每次次衰减减少2毫秒
      • 大部分情况20ms,可以查询db并重建redis,衰减机制可以根据中间件性能,自行优化调整
      • 之所以用衰减方式,而非直接sleep 200ms,是因为线程是JVM中非常宝贵的限量资源,新开需要花费额外成本
    • 对每个需访问DB的id,在Redis设置处理中标识,并继续访问DB
    • 代码样例如下:
      //一、id分类,处理中的id(用于限流),待处理的id,锁list
      //1.1 被其他线程锁持有ID(其他线程在查询db处理中)
      List<String> processingIds = Lists.newArrayList();
      //1.2 本线程成功加锁持有的ID,需要查询db
      List<String> lockedIds = Lists.newArrayList();
      //1.3 本线程设置的redis锁标识,用于处理完成后,统一释放
      List<ILock> alLocks = Lists.newArrayList();
      
      //二、redisMissIds 表示从redis中没有查到的ID list
      for (String redisMissId : redisMissIds) {
          //2.1设置redis锁标识
          ILock lock = iLockFactory.getLock(“业务类型-”+redisMissId,锁有效期毫秒);
          alLocks.add(lock);
      
          //2.2该ID加锁失败,说明被其他线程加锁并查询db中
          //放弃竞争锁,等待其他线程处理完成后从redis中取值
          if (!lock.lock(lockKeysEnum.getTimeout())) {
              processingIds.add(redisMissId);
          } else {
              //该ID加锁成功,需要到db查询
              lockedIds.add(redisMissId);
          }
      }
      
      //三、成功获取锁的ID,查DB并放入Redis
      try {
          
          if (!CollectionUtils.isEmpty(lockedIds)) {
              //3.1 db查询有结果,设置redis
              List<Object> dos = dao.getByIdsFromMaster(lockedIds);
              redisUtils.batchSetCaches(buildCache(dos), redis超时时间);
      
              //3.2 db查询无结果,也设置redis
              //结构参考文档之前提到的内容
          }
      } finally {
          // redis处理中标识释放
          for (ILock lock : locks) {
              lock.releaseLock();
          }
      }
      
      //四、未获取锁的ID,获取其他线程重建后的缓存
      if (!CollectionUtils.isEmpty(processingIds)) {
          //衰减6次,共计90ms ,从redis 获取值
          for (int i = 1; i <= 6; i++) {
              try {
                  //4.1 建议用pipeline + mget提升性能
                  redisUtils.queryByCacheKeys(“业务场景”+processingIds);
                  
                  //4.2 判断全部请求key ,都取到redis值,查询终止直接返回
                  
                  //4.3 部分ID未取到redis值,线程休眠等待
                  //线程很宝贵,第一次休眠后,大概率可以取到值,后续休眠时间衰减
                  Thread.sleep(20 - i * 2);
              } catch (InterruptedException e) {
                  Logger.warn("Thread.sleep异常", e);
              } 
          }
      }
      
      //五、如果缓存中获取6次(90ms)依然未拿到,放弃等待查询db
      
      
    • 策略2解决思路如下
      • redis结构为(String结构为例)
      key:业务场景+id,
      value:json:{
               key:业务场景+id,
               dataEmpty:true|false,false表示该值为空,无需查db重建
               dataValue:xxx
             }
      
  • TIPS 对于Redis没有,DB也没有的数据,如果会高频查询,必须在Redis设置标识,减少查询DB

    • 如果设置了空缓冲,当DB设置有数据时,一定要移除或修改Redis缓冲标识
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

米灵君的架构思维

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值