【redis-03】redis缓存穿透、缓存击穿、缓存雪崩

redis系列整体栏目


内容 链接地址
【一】redis基本数据类型和使用场景 https://zhenghuisheng.blog.csdn.net/article/details/142406325
【二】redis的持久化机制和原理 https://zhenghuisheng.blog.csdn.net/article/details/142441756
【三】redis缓存穿透、缓存击穿、缓存雪崩 https://zhenghuisheng.blog.csdn.net/article/details/142577507

如需转载,请输入:https://blog.csdn.net/zhenghuishengq/article/details/142577507

redis缓存穿透、缓存击穿、缓存雪崩

一,redis缓存穿透、缓存击穿、缓存雪崩

在使用redis作为缓存时,经常会遇到缓存一系列的问题,如在大型的互联网公司中,一般的组织架构都会比较的依赖redis缓存,因此经常会可能出现以下问题: 缓存穿透、缓存击穿和缓存雪崩

1,缓存击穿(失效)

1.1,造成缓存击穿的原因

缓存击穿,又被称为缓存失效。如京东网站中,通常会有大量的秒杀场景,通常会分布在不同的时间段做对应的秒杀设计,每个商品都会有对应的秒杀时间,如苹果手机设置18点开始售卖,2个小时后结束。

当然这些商品的商家都需要后台运营进行维护,如选择商品上架、设置过期时间、设置价格等。但是由于数量实在是太多,运营人员一般会选择批量设置操作,假如说新来的运营人员或者说不熟悉这块业务的运营人员来进行这块操作的话,如不小心直接点了一个 一键上架 ,导致所有的商品都启动了秒杀服务,并且设置了1个小时的秒杀时间

以上是一个简单的架构流程图,假设说在启动了一键上架后,本该在不同时段参与秒杀服务的商品,现在全部都集中的在一个时间段进行了秒杀服务。假设在这1小时内,redis确实可以抗住这段时间内的流量,因为redis做了集群,并且通过内部的多路复用,Reactor模式等,可以暂时支持这段时间的高可用。

由于是批量上架,并且设置的都是1个小时的秒杀,那么其上架时间都一样,那么所有商品的失效时间也一样。当所有的商品都失效之后,那么如果失效时用户没有刷新页面,所有用户都还在浏览商品或者下单商品,那么这些操作都会打到mysql中,mysql的性能肯定是不如redis的,一个mysql能抗住2000-3000的并发量都很不错了,因此这就造成了 缓存击穿,也叫缓存失效

1.2,如何解决缓存击穿

1.2.1,添加随机时间

既然是大方面的缓存在同一时刻失效的问题导致的缓存击穿,那么首先第一种解决方案就是添加随机的缓存时间,如根据不同的商品设置不同的随机过期时间,如下端代码,在初始化redis的配置类时,添加redis的默认过期时间,并给过期时间加一个0-5分钟之间的随机值

java 复制代码
@Configuration
@EnableCaching
public class RedisCacheConfig {

    private static final int BASE_EXPIRE_TIME_SECONDS = 3600; // 1小时
    private static final int RANDOM_EXPIRE_TIME_SECONDS = 300; // 随机抖动最大5分钟

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 配置 Redis 缓存的默认过期时间,结合随机化过期时间
        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(getRandomExpireTime()))  // 设置随机化过期时间
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));

        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(cacheConfig)
                .build();
    }

    // 生成随机化的缓存过期时间
    private int getRandomExpireTime() {
        Random random = new Random();
        int randomTime = random.nextInt(RANDOM_EXPIRE_TIME_SECONDS); // 生成0到5分钟的随机时间
        return BASE_EXPIRE_TIME_SECONDS + randomTime;
    }

    // 配置默认的Key生成器(可选)
    @Bean
    public SimpleKeyGenerator keyGenerator() {
        return new SimpleKeyGenerator();
    }
}
1.2.2,添加限流和降级操作

在每次对redis进行操作时,对redis进行一个get操作,如果此时发现redis不可用,或者redis获取的值为空,则对用户的请求进行一个限流操作,然后给用户一个友好的提示进行降级,如商品已经卖完,秒杀已经结束等。

除了上面两种方式,还可以在本地添加一个一级缓存操作,在短时间内保证数据的高可用,以至于请求不打到数据库中,给数据库带来压力。

2,缓存穿透

2.1,什么是缓存穿透

如在大型网站中,为了加快整个网站的响应,往往会对一些信息进行缓存操作。如在京东商城中,在查看商品的详细信息时,可能会通过携带商品的id再后台进行查询,或者查看用户的历史订单数据,需要携带用户的id

java 复制代码
xxx?id = 10001

但是往往利用这个特点,可以通过抓包的方式获取到一些主要的数据,如拿到用户token,或者直接绕过这些认证鉴权的步骤,直接模拟相关http请求对后台进行数据的拉取和访问。如访问某个类型的商品信息,设置一个随机值,商品信息在redis中找不到,那么此时就会访问数据库,那么最终发现数据库也找不到,这就是缓存穿透

java 复制代码
xxx?typeId = 123123232132

当然在这里重点主要是看设置随机id在redis中找不到,然后导致大量数据查mysql,导致加重mysql的负载,进而影响整个系统的吞吐量。

2.2,布隆过滤器

为了解决这种缓存穿透问题,比较靠谱的方案就是使用布隆过滤器来解决。布隆过滤器主要通过一个类似于一个大的bitmap二进制数组,其设计思想主要是通过一些hash函数实现,其核心思想如下:当某个值存在时,这个值不一定存存在;当某个值不存在时,那么这个值一定不存在。在这个二进制数组中,其内部主要有0和1组成,如果数据插入,则在hash的地方设置成1,数组长度可以达到十亿百亿级别。

如以下图,模拟一个10个长度的数组,此时没有数据,其比特位全部设置为0

比如此时需要设置一个值商品id为10的key,其value值为100

java 复制代码
set 10 100

那么在经过布隆过滤器时,就会对这个key值做三次hash计算,如我这边自定义三个hash规则,此时经过hash计算的值为0,1,3。在计算结束之后,需要在布隆过滤器中,对对应的下标设置成1

java 复制代码
h1 = 10 % 10 = 0
h2 = (10 * 3 +1) % 10 = 1
h2 = (10 * 5 +3) % 10 = 3    

在获取商品id为1时,需要再对这个id进行相同的hash,并且需要根据这三个计算出来的hash值与bitmap中的进行比较,判断对应的下下标为的值是否全时1,只有3个全为1的,才认为这个值是可能存在的,那么再去redis缓存中获取数据。只要存在一个值为0,那么就直接认为数据在布隆过滤器中是不存在的

如需要获取id为5的数据,经过相同的hash算法之后,发现计算出来的值分别是5,6,8,然后再去布隆过滤器中进行对比,发现下标为5,6,8的数组对应的值都是0,因此认为这个值是一定不存在的,那么直接返回空即可

java 复制代码
h1 = 5 % 10 = 5
h2 = (5* 3 +1) % 10 = 6
h2 = (5* 5 +3) % 10 = 8

假如此时数据来到了1000万,如果还是在redis中查找,那么数据相对会很慢,甚至影响整个系统的性能,但是如果直接使用这种布隆过滤器,是可以大大的加快查询效率的,通过这种布隆过滤器拦截掉大部分的伪造请求,从而降低缓存穿透概率的发生

部分相关的布隆过滤器的代码如下,定义一个布隆过滤器,然后设置数组大小为10000,hash次数为5,主要有两个方法,一个是将key加入到布隆过滤器的add方法,一个是判断当前key是否在布隆过滤器中的 contains 方法

java 复制代码
@Component
public class BloomFilter {
    private final RedisTemplate<String, Object> redisTemplate;
    private final int size = 10000; // 位数组大小
    private final int hashCount = 5; // 哈希函数数量

    @Autowired
    public BloomFilter(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // 使用哈希函数计算索引
    private int[] getHashIndices(String value) {
        int[] indices = new int[hashCount];
        for (int i = 0; i < hashCount; i++) {
            indices[i] = (hash(value, i) % size + size) % size; // 确保索引为正
        }
        return indices;
    }

    // 简单的哈希函数
    private int hash(String value, int seed) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update((value + seed).getBytes(StandardCharsets.UTF_8));
            byte[] bytes = md.digest();
            return Math.abs(bytes[0]); // 取哈希值的绝对值
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    // 添加元素到布隆过滤器
    public void add(String value) {
        int[] indices = getHashIndices(value);
        for (int index : indices) {
            redisTemplate.opsForValue().setBit("bloom_filter", index, true);
        }
    }

    // 查询元素是否在布隆过滤器中
    public boolean contains(String value) {
        int[] indices = getHashIndices(value);
        for (int index : indices) {
            if (!redisTemplate.opsForValue().getBit("bloom_filter", index)) {
                return false; // 有位为 0,元素一定不存在
            }
        }
        return true; // 所有位均为 1,可能存在
    }
}

3,缓存雪崩

3.1,缓存雪崩的原因

缓存雪崩,指的是因为一个redis出现的一点小问题,导致问题越来越大,从而影响多层架构,就像滚雪球一样,因为一点小错误导致雪球越来越大。

在实际开发中,redis一般都会做主从来保证高可用,做集群来保证高性能。redis集群一般都是可以抗住很高的流量,但是突然集群中因为某个原因导致某个结点挂了,进而让整个集群承受不住那么大流量,进而导致redis集群多个结点挂了,然后数据全部给了mysql,进而导致mysql承受不住那么大流量,进而影响当前服务和其他服务的sql阻塞,进而导致所有tomcat连接池占满,进而导致所有的web服务都不可用。但是导致雪崩的主要原因,还是因为redis的不可用,导致数据全部打到mysql导致的。

除了redis本身不可用之外,也可能因为大key的问题导致,大key值的指的是value特别的大,比如一个用户的数据本来是好好的通过String类型存储的,但是后面优化改成了hash的方式存储,导致这个用户的value值特别大。由于redis的多路复用的特性,以及使用的是单线程的Reactor反应堆模式,导致在加载到内存中以及处理请求比较耗时,导致redis出现了阻塞,让其他命令等待,请求越多导致tomcat连接池占满,进而导致服务被打挂,导致整个服务不可用。

导致redis不可用或者说不能用的原因有很多,比如外部流量太大把redis集群压垮,或者多个商品在同一时间失效导致redis不能用。缓存雪崩和缓存击穿可以说是非常像,都是因为redis的不可用或者不能用导致请求全部来到了mysql,进而引发整个架构出现不可用的问题。

3.2,如何解决缓存雪崩

  • 首先第一点,得保证redis的高可用,可以搭建多节点cluster集群,或者搭建简单的哨兵集群
  • 设置限流降级功能,请求是先打到web服务,然后再进入redis,如果redis压测是只能 1w/s 的请求,那么在web服务端中得提前进行限流降级功能,当每秒请求达到8000或者9000时,则对多的请求进行一个平滑的降级功能,比如给一个友好的提示页面。
  • 或者用户在进入页面时,将用户的请求加入到mq队列中,然后异步的告知用户前面还有多少人在排队,让用户等待
  • 在上线前对服务以及redis进行压测,对redis进行数据备份,合理的设置jvm服务的个数,mysql的架构等
  • redis内部进行优化,如解决bigkey问题,批量的操作数据,合理的选择数据类型等
相关推荐
星河梦瑾1 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富1 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想1 小时前
JMeter 使用详解
java·jmeter
言、雲1 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
TT哇1 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
Yvemil72 小时前
《开启微服务之旅:Spring Boot 从入门到实践》(三)
java
Anna。。2 小时前
Java入门2-idea 第五章:IO流(java.io包中)
java·开发语言·intellij-idea
G_whang2 小时前
centos7下docker 容器实现redis主从同步
redis·docker·容器
.生产的驴2 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
爱上语文2 小时前
宠物管理系统:Dao层
java·开发语言·宠物