美文网首页
缓存操作的一些问题

缓存操作的一些问题

作者: 无聊之园 | 来源:发表于2019-01-28 17:46 被阅读0次

    缓存雪崩:一旦缓存没有数据了,所有请求打到数据库,数据库承受不住,然后这个应用就无法使用。
    解决方案:
    1、redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
    2、服务限流,降级,避免MySQL被打死
    3、redis持久化,快速恢复缓存数据

    缓存穿透:再查询一个不存在的数据的时候,先查询缓存,缓存为null,再查询数据库,数据也为null,之后,每一次都如此,每一个都把压力落到了数据库层。
    解决办法
    1、ip或id限流加黑名单。防止恶意攻击。
    2、设置默认值,在查询到数据库为null的时候,在缓存中保存一个默认值。
    缺点:1、对于insert操作,需要去过期这些缓存,增加了缓存的复杂性。2、浪费内存。3、如果别人操控很多台机器在限流范围内循环请求不存在数据,比如:id从-1到-9999。没有办法,但是代价太大了,也很难做到。

    改善方法:设置缓存过期时间,设置短一点,redis的缓存策略改成lru(volatile或allkey lru),内存满了之后,去掉最近最少使用的缓存。或者,缓存穿透的key单独存放,不影响正常数据的缓存。

    贴代码:我们系统使用的是ace-cache缓存,但是这个对缓存相关的问题都没有进行处理,所以可以做以下修改。

        public static final String nullObject = "null-data-uuid";
    @Pointcut("@annotation(com.ace.cache.annotation.Cache)")
        public void aspect() {
        }
    
        @Around("aspect()&&@annotation(anno)")
        public Object interceptor(ProceedingJoinPoint invocation, Cache anno)
                throws Throwable {
            MethodSignature signature = (MethodSignature) invocation.getSignature();
            Method method = signature.getMethod();
            Object result = null;
            Class<?>[] parameterTypes = method.getParameterTypes();
            Object[] arguments = invocation.getArgs();
            String key = "";
            String value = "";
            try {
                key = getKey(anno, parameterTypes, arguments);
                value = cacheAPI.get(key);
                if(nullObject.equals(value)){
                    // result不为null,则finally块就不会去查数据库
                    result = value;
                    return null;
                }
                Type returnType = method.getGenericReturnType();
                result = getResult(anno, result, value, returnType);
            } catch (Exception e) {
                log.error("获取缓存失败:" + key, e);
            } finally {
                if (result == null) {
                    result = invocation.proceed();
                    // 防止缓存穿透
                    if(result == null){
                        if (StringUtils.isNotBlank(key)) {
                            cacheAPI.set(key, nullObject, 3600);
                        }
                    }
                    if (StringUtils.isNotBlank(key)) {
                        cacheAPI.set(key, result, anno.expire());
                    }
                }
            }
            return result;
        }
    
    



    对于写for循环的缓存穿透攻击,限制ip,每秒超过200次,则限制ip访问,对于多个ip的同事缓存穿透攻击,模仿正常请求太像了,无法限制,只能加机器,加大数据库的读性能。
    代码:redis限流

    缓存更新顺序的问题:先更新数据库,再移除缓存,一旦缓存移除失败,则回滚数据库。先后顺序不能反过来,一旦先移除缓存,再更新数据库,那么在移除缓存后、还没来得及更新数据库之前,又来了一个线程,重新 从数据库中读取数据,写入缓存中,则会导致缓存的是脏数据。
    这里,有一个问题:如果存在数据库读写分离,再加上缓存机制,那么单纯这样的操作还是会导致缓存不一致的问题:DB主从同步延迟导致的缓存不一致

    @Around("aspect()&&@annotation(anno)")
       public Object interceptor(ProceedingJoinPoint invocation, CacheClear anno)
               throws Throwable {
           // 先更新数据库,再更新缓存。更新缓存失败,则回滚数据库。
           Object proceed = invocation.proceed();
           try {
               MethodSignature signature = (MethodSignature) invocation.getSignature();
               Method method = signature.getMethod();
               Class<?>[] parameterTypes = method.getParameterTypes();
               Object[] arguments = invocation.getArgs();
               String key = "";
               if (StringUtils.isNotBlank(anno.key())) {
                   key = getKey(anno, anno.key(), CacheScope.application,
                           parameterTypes, arguments);
                   cacheAPI.remove(key);
               } else if (StringUtils.isNotBlank(anno.pre())) {
                   key = getKey(anno, anno.pre(), CacheScope.application,
                           parameterTypes, arguments);
                   cacheAPI.removeByPre(key);
               } else if (anno.keys().length > 1) {
                   for (String tmp : anno.keys()) {
                       tmp = getKey(anno, tmp, CacheScope.application, parameterTypes,
                               arguments);
                       cacheAPI.removeByPre(tmp);
                   }
               }
           } catch (Exception e){
               log.error(e);
               throw  new RuntimeException("清楚缓存失败");
           }
           return proceed;
       }
    

    缓存击穿:在更新数据并且缓存过期了之后,在一瞬间同事发生多个读数据的请求,这一瞬间缓存中没有数据,压力都落到了数据库。
    解决办法:使用分布式锁锁住。分布式锁

     String value = redis.get(key);
          if (value == null) { 
             // redission是redis工具类,比较完善的处理好了redis分布式锁的各种问题
            redisson.getLock(lockKey).lock(30, TimeUnit.SECONDS)
              if (value == null) {
                      value = db.get(key);
                      redis.set(key, value, expire_secs);
                      redisson.getLock(lockKey).unlock()
              }
          }
    

    或者粗糙一点

    public String get(key) {
          String value = redis.get(key);
          if (value == null) { //代表缓存值过期
              //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
              if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
                   value = db.get(key);
                          redis.set(key, value, expire_secs);
                          redis.del(key_mutex);
                  } else {  //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
                          sleep(50);
                          get(key);  //重试
                  }
              } else {
                  return value;      
              }
     }
    

    相关文章

      网友评论

          本文标题:缓存操作的一些问题

          本文链接:https://www.haomeiwen.com/subject/fsxzjqtx.html