Redis学习之1招击穿自己的系统附送N个击穿解决大礼包

1.缓存击穿解释

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和前面缓存雪崩的区别在于这里针对某一key缓存,前者则是一堆key在同一时刻失效。

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

结合上一章Redis学习之1招雪崩自己的系统附送N招解决雪崩大礼包 对雪崩的理解,我通过画图对比。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KMPMdiNo-1584792797148)(C:\Users\lps\Documents\myblog\缓存击穿.assets\image-20200321164106161.png)]

可以通过图中差异得出,击穿在于同一个key集中查询,当key失效,所有查询压力就直接下去DB,最后应该最差情况导致数据库挂掉,但是一般我们会用连接池限制得情况下,会导致数据库连接过大,业务系统直接gg,无法从数据库获取查询,同时影响其他db操作。

2.缓存击穿模拟

使用前面Redis+SpringBoot+SpringCache基础项目搭建 作为基础项目进行模拟。

具体思路:

  1. 设置数据库连接池最大数量为5,增大数据库表数据量(增加连接占用时间)。
  2. 创建key1-8888数据持久化到数据库。
  3. 设置缓存过期时间为1s。
  4. 创建线程池,多线程针对查询。
    @Test
    public void testQueryIdWithBreakDown(){
        String key = "8888";
        //存进缓存
        redisTemplate.opsForValue().set(key,"value", Duration.ofSeconds(1));
        ExecutorService es = Executors.newFixedThreadPool(10);
        int loop= 1;

        //睡个觉等过期
        try {
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }

       //开始了疯狂查询,其实并不是太疯狂,结合DB最大连接数才5 我们放10条线程攻击就很容易gg了
        for (int i = 0; i < 10; i++) {
            es.execute(() -> {
                for (int k = 0; k < loop; k++) {
                    if(!redisTemplate.hasKey(key)){
                        //数据查询
                        Assert.assertNotNull(userService.queryByIdWithTest(Integer.parseInt(key)));
                        //放进缓存
                        redisTemplate.opsForValue().set(key,"value", Duration.ofSeconds(1));
                    }

                }
            });
        }

    }

控制台结果:

`java.sql.SQLNonTransientConnectionException: Data source rejected establishment of connection,  message from server: "Too many connections"
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:110) ~[mysql-connector-java-8.0.19.jar:8.0.19]
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.19.jar:8.0.19]
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.19.jar:8.0.19]
	at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:836) ~[mysql-connector-java-8.0.19.jar:8.0.19]
	at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:456) ~[mysql-connector-java-8.0.19.jar:8.0.19]
	at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:246) ~[mysql-connector-java-8.0.19.jar:8.0.19]
	at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:197) ~[mysql-connector-java-8.0.19.jar:8.0.19]
	at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:149) ~[druid-1.1.9.jar:1.1.9]
	at com.alibaba.druid.filter.stat.StatFilter.connection_connect(StatFilter.java:218) ~[druid-1.1.9.jar:1.1.9]
	at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:143) ~[druid-1.1.9.jar:1.1.9]
	at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1515) ~[druid-1.1.9.jar:1.1.9]
	at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1578) ~[druid-1.1.9.jar:1.1.9]
	at com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(DruidDataSource.java:2466) ~[druid-1.1.9.jar:1.1.9]`

很明显数据库连接数超了,导致gg,以上就是简单对击穿进行模拟。而在实际情况中,缓存击穿的现象很常见,比如微博某个热点话题导致宕机,就很有可能在缓存层已经撑不住了,对这样的“话题”我们可以认为是热点key,从缓存击穿的解决方案上,我们可以沿用雪崩那套解决Redis学习之1招雪崩自己的系统附送N招解决雪崩大礼包,因为击穿跟雪崩的差别就是热点key跟一大堆或者全部key的差别而已。

3.缓存击穿解决方案

3.1 通过逐级降低访问的流量的方式保持业务系统的稳定性

与雪崩同样的,再面对大范围的访问,先想到的还是“倒三角”模型,通过逐级降低访问的流量的方式保持业务系统的稳定性

在这里插å¥å›¾ç‰‡æè¿°

3.2 使用互斥锁

使用互斥锁的原理就是就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

具体可以参考:Redis学习之1招雪崩自己的系统附送N招解决雪崩大礼包

3.3 异步构建缓存

异步构建缓存就是将过期时间存放在value里面或者其他地方,然后发现过期后异步更新缓存,但是很有可能导致一致性问题。

下面我们来简单实现一下

这里我采用的方案是在value属性里面存放过期时间,然后使用互斥锁单写缓存

String get(final String key) {
        V v = redis.get(key);  
        String value = v.getValue();  
        long timeout = v.getTimeout();  
        if (v.timeout <= System.currentTimeMillis()) {
            // 异步更新后台异常执行  

            threadPool.execute(new Runnable() {  
                public void run() {  
                    String keyMutex = "mutex:" + key;  
                    if (redis.setnx(keyMutex, "1")) {  
                        // 3 min timeout to avoid mutex holder crash  

                        redis.expire(keyMutex, 3 * 60);  
                        String dbValue = db.get(key);  
                        redis.set(key, dbValue);  
                        redis.delete(keyMutex);  
                    }  
                }  
            });  
        }  
        return value;  
    }

3.4 热点标注

其实按照现在大多数业务系统都会进行热点统计监控的话,很容易得出热点集合,对热点集合进行“永不过期”处理也是一种方案。

4. 总结

我觉得缓存击穿是缓存雪崩的一种特殊形式,但是其实又是最常见的一种问题,在经过模拟场景后,通过代码形式,结合程序执行返回,更好地理解缓存击穿是怎么产生的,在理解产生之后,针对现象进行分析获取缓存击穿的解决方案,虽然上面列举几个解决方法,但是并都不是一劳永逸的方案,需要结合具体的场景采取具体的解决方案,以上只是给与思路参考。

发布了35 篇原创文章 · 获赞 44 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_28540443/article/details/104974989