Java架构直通车——Redis缓存穿透/击穿/雪崩

缓存穿透

在高并发下,查询一个不存在的值时,缓存不会被命中,导致大量请求直接落到数据库上。
比如下面的代码:
首先查询缓存,如果没有该缓存,查询数据库,然后再放入缓存。

		String categoryVOSStr=redisOperator.get("subCat:"+rootCatId);
        if (StringUtils.isBlank(categoryVOSStr)){
            categoryVOS=categoryService.getSubCatList(rootCatId);
            if (categoryVOS!=null&&categoryVOS.size()!=0)
                redisOperator.set("subCat:"+rootCatId,JsonUtils.objectToJson(categoryVOS));
        }

如果现在查询的rootCatId在数据库中有,比如说rootCatId为1,那么之后再次查询的时候,就会在缓存中查找key为:subCat:1的缓存。

不过如果查询的rootCatId在数据库中不存在,比如非法用户进行攻击,大量请求会请求rootCatId为999,此时数据库中又没有999这个Id,那么缓存也不会产生,大量的请求会直接打在db上,造成宕机,从而影响整个系统。 这种现象就叫做缓存穿透。

缓存穿透也比较好解决,最简单的方法如下:
把空的数据也缓存起来,比如空字符串,空对象,空数组或者list。(可以给这些空数据设定一个过期时间。)

		String categoryVOSStr=redisOperator.get("subCat:"+rootCatId);
        if (StringUtils.isBlank(categoryVOSStr)){
            categoryVOS=categoryService.getSubCatList(rootCatId);
            redisOperator.set("subCat:"+rootCatId,JsonUtils.objectToJson(categoryVOS));
        }

缓存击穿

在高并发下,对一个特定的值进行查询,但是这个时候缓存正好过期了,缓存没有命中,导致大量请求直接落到数据库上。


  1. 缓存永不过期

最简单的方法当然是缓存永远不过期,这样会产生一个问题,当缓存占用内存过大后,也会触发缓存的淘汰机制。


  1. 限流或者加锁

常用方法之一是限流,常见的限流算法有滑动窗口,令牌桶算法和漏桶算法,或者直接使用队列、加锁等,在layering-cache里面我主要使用分布式锁来做限流。
layering-cache:
采用 L1 (一级缓存)和 L2(二级缓存) 缓存方式,L1 缓存失效时间短,L2 缓存失效时间长。 请求优先从 L1 缓存获取数据,如果 L1缓存未命中则加锁,只有 1 个线程获取到锁,这个线程再从数据库中读取数据并将数据再更新到到 L1 缓存和 L2 缓存中,而其他线程依旧从 L2 缓存获取数据并返回。
这种方式,主要是通过避免缓存同时失效并结合锁机制实现。所以,当数据更
新时,只能淘汰 L1 缓存,不能同时将 L1 和 L2 中的缓存同时淘汰。L2 缓存中
可能会存在脏数据,需要业务能够容忍这种短时间的不一致。而且,这种方案
可能会造成额外的缓存空间浪费。

或者使用redis或者zookeep提供的互斥锁可以解决击穿的问题。

缓存雪崩

在高并发下,大量的缓存key在同一时间失效,导致大量的请求落到数据库上。
缓存雪崩一般可以采用:

  • 缓存永不过期
  • 过期时间错开:可以采用一个随机时间来设置过期时间。
  • 多缓存结合:不一定只使用redis缓存,可以再使用其他的比如memcache等等。
  • 缓存预加载。

这里说下layering-cache的缓存预加载:
在 layering-cache里面二级缓存会配置两个时间,expireTime是缓存的过期时间,preloadTime 是缓存的刷新时间(预加载时间)。每次二级缓存被命中都会去检查缓存的过期时间是否小于刷新时间,如果小于就会开启一个异步线程预先去更新缓存,并将新的值放到缓存中,有效的保证了热点数据"永不过期"。这里预先更新缓存也是需要加锁的,并不是所有的线程都会落到库上刷新缓存,如果没有获取到锁就直接结束当前线程。

在缓存总量和并发量都很大的时候,这个时候缓存如果同时失效,缓存预热将是一个非常慢长的过程,就比如说服务重启或新上线一个新的缓存。这个时候我们可以采用切流的方式,让缓存慢慢预热,如开始切10%流量,观察没有异常后,再切30%流量,观察没有异常后,再切60%流量,然后全量。这种方式虽然有点繁琐,但是一旦遇到异常我们可以快速的切回流量,让风险可控。

发布了385 篇原创文章 · 获赞 326 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/No_Game_No_Life_/article/details/104308881