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

缓存操作的一些问题

作者: 无聊之园 | 来源:发表于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