8、缓存预热,缓存雪崩,缓存击穿,缓存穿透

1、缓存雪崩

① redis 主机挂了,Redis全盘崩溃

② 比如缓存中有大量数据同时过期

解决:

1、Redis缓存集群实现高可用

(1)主从+哨兵

(2)Redis Cluster

2、ehcache本地缓存+Hystrix或者阿里sentinel限流&降级

3、开启Redis持久化机制aof/rdb,尽快恢复缓存集群

2、缓存穿透

2.1、是什么

请求去查询一条记录,先redis后mysq发现都查询不到该条记录, 但是请求每次都会打到数据库上面去,导致后台数据库压力暴增, 这种现象我们称为缓存穿透,这个 redis变成了一个摆设。。。。。

简单说就是本来无一物,既不在 Redis缓存中,也不在数据库中

2.2、危害

第一次来查询后,一般我们有回写 redis机制

第二次来查的时候redis就有了,偶尔出现穿透现象一般情况无关紧要

2.3、解决

方案一:空对象缓存或者缺省值

一般ok:一旦发生缓存穿透,我们就可以针对查询的数据,在 Redis中缓存一个空值或是和业务层协商确定的缺省值 (例如,库存的缺省值可以设为0)。紧接着,应用发送的后续请求再进行查询时,就可以直接从 redis中读 取空值或缺省值,返回给业务应用了,避免了把大量请求发送给数据库处理,保持了数据库的正常运行。

恶意攻击无效:黑客会对你的系统进行攻击,拿一个不存在的id 去查询数据,会产生大量的请求到数据库去查询。 可能会导致你的数据库由于压力过大而宕掉

id相同打你系统

第一次打到 mysql,空对象缓存后第二次就返回null了, 避免 mysql被攻击,不用再到数据库中去走一圈了

id不同打你系统

由于存在空对象缓存和缓存回写(看自己业务不限死) ,redis中的无关紧要的key也会越写越多(记得设置redis过期时间)

方案二:Google布隆过滤器Guava解决缓存穿透

Guava中布隆过滤器的实现算是比较权威的, 所以实际项目中我们不需要手动实现一个布隆过滤器

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>

 

package com.shuidi.redis.bloomfilter;

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import io.swagger.models.auth.In;

import java.util.ArrayList;
import java.util.List;

/**
 * @author shizan
 * @Classname GuavaBloomfilterDemo
 * @Description TODO
 * @Date 2022/4/1 11:42 下午
 */
public class GuavaBloomfilterDemo {

    public static final int _1w = 10000;
    /**
     * 布隆过滤器里预计要插入多少数据
     */
    public static int size = 100 * _1w;
    /**
     * 误判率,它越小误判的个数也就越小(思考,是不是可以设置的无限小,没有误判岂不更好)
     */
    public static double fpp = 0.03;

    /**
     * helloworld入门
     */
    public void bloomFilter() {
        //创建布隆过滤器
        BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 100);
        //判断指定元素是否存在
        System.out.println(filter.mightContain(1));
        System.out.println(filter.mightContain(2));
        //将元素添加进布隆过滤器
        filter.put(1);
        filter.put(2);
        System.out.println(filter.mightContain(1));
        System.out.println(filter.mightContain(2));
    }

    /**
     * 误判率演示+源码分析
     */
    public void bloomFilter2() {
        //创建布隆过滤器
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size);
        //1.先往布隆过滤器里面插入100万样本数据
        for (int i = 0; i < size; i++) {
            bloomFilter.put(i);
        }
        List<Integer> listSample = new ArrayList<>(size);

        //2.这100万的样本数据,是否都在布隆过滤器里面存在?
        for (int i = 0; i < size; i++) {
            if (bloomFilter.mightContain(i)) {
                listSample.add(i);
                continue;
            }
        }
        System.out.println("存在的数量:" + listSample.size());
        //3.故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里,误判率演示
        List<Integer> list = new ArrayList<>(10 * _1w);
        for (int i = size + 1; i < size + 100000; i++) {
            if (bloomFilter.mightContain(i)) {
                System.out.println(i + "\t" + "被误判了");
                list.add(i);
            }
        }
        System.out.println("误判的数量:" + list.size());

    }

    public static void main(String[] args) {
        new GuavaBloomfilterDemo().bloomFilter2();
    }
}

输出:
存在的数量:1000000
1000029	被误判了
1000049	被误判了
1000092	被误判了
。。。。。
。。。。。
1099882	被误判了
1099947	被误判了
误判的数量:3033

3033/100000≈0.03

Guava‘ BloomFilter源码剖析

 

 

精度提高hash函数使用的个数会增多,效率越低

布隆过滤器说明

 

方案三:Redis布隆过滤器解决缓存穿透

Guava缺点说明:

Guava提供的布隆过滤器的实现还是很不错的(想要详细了解的可以看一下它的源码实现),但是它有一个重大的缺陷就是只能单机使用 ,而现在互联网一般都是分布式的场景。

为了解决这个问题,我们就需要用到 Redis中的布隆过滤器了

案例:白名单过滤器

白名单架构说明:

误判率问题,但是概率小可以接受,不能从布隆过滤器删除

全部合法的key都需要放入过滤器+redis里面,不然数据就是返回null

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>

 

import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.redisson.config.Config;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * @author shizan
 * @Classname RedissonBloomFilterDemo
 * @Description redis布隆过滤器demo
 * @Date 2022/4/3 1:03 上午
 */
public class RedissonBloomFilterDemo {

    public static final int _1w = 10000;
    /**
     * 布隆过滤器里预计要插入多少数据
     */
    public static int size = 100 * _1w;
    /**
     * 误判率,它越小误判的个数也就越小(思考,是不是可以设置的无限小,没有误判岂不更好)
     */
    public static double fpp = 0.03;

    static RedissonClient redissonClient = null;

    static RBloomFilter rBloomFilter = null;
    @Resource
    RedisTemplate redisTemplate;

    static {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://172.16.119.100:6379").setDatabase(0);
        //构造redisson
        redissonClient = Redisson.create(config);
        //通过redisson构造BloomFilter
        rBloomFilter = redissonClient.getBloomFilter("phoneListBloomFilter", new StringCodec());
        rBloomFilter.tryInit(size, fpp);

        //1.测试 布隆过滤器有+redis有
        rBloomFilter.add("10086");
        redissonClient.getBucket("10086", new StringCodec()).set("chinamobile10086");

        //2.测试 布隆过滤器有+redis无
        rBloomFilter.add("10087");

        //3.测试 布隆过滤器无+redis有
    }

    private static String getPhoneListById(String IDNumber) {
        String result=null;
        if(IDNumber==null){
            return null;
        }
        //1.先去布隆过滤器里面查询
        if(rBloomFilter.contains(IDNumber)){
            //2.布隆过滤器里面有,再去redis里面查询
            RBucket<String> rBucket = redissonClient.getBucket(IDNumber, new StringCodec());
            result = rBucket.get();
            if(result!=null){
                return "i come from redis: "+result;
            }else {
                result = getPhoneListByMySQL(IDNumber);
                if(result==null){
                    return null;
                }
                //重新将数据更新回redis
                redissonClient.getBucket(IDNumber,new StringCodec()).set(result);

            }
            return "i come from mysql: "+result;
        }
        return result;
    }

    private static String getPhoneListByMySQL(String IDNumber) {
        return "chinamobile" + IDNumber;
    }


    public static void main(String[] args) {
        String phoneListById1 = getPhoneListById("10086");
        System.out.println("---查询出来的结果:" + phoneListById1);
        String phoneListById2 = getPhoneListById("10087");
        System.out.println("---查询出来的结果:" + phoneListById2);
        String phoneListById3 = getPhoneListById("10088");
        System.out.println("---查询出来的结果:" + phoneListById3);

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        redissonClient.shutdown();
    }

}
输出结果:
---查询出来的结果:i come from redis: chinamobile10086
---查询出来的结果:i come from mysql: chinamobile10087
---查询出来的结果:null

 

重要总结

 

黑名单使用:

 

2.4、在centos7下布隆过滤器2种安装方式

① 采用docker安装RedisBloom,推荐

Redis在4.0之后有了插件功能( Module),可以使用外部的扩展功能, 可以使用 RedisBloom作为 Redis布隆过滤器插件

 docker run -p 6379:6379 --name=redis6379bloom -d redislabs/rebloom
 docker exec -it redis6379bloom /bin/bash
 redis-cli

布隆过滤器常用操作命令

127.0.0.1:6379> BF.ADD filter 10086 #添加10086到布隆过滤器filter
(integer) 1
127.0.0.1:6379> BF.EXISTS filter 10086 #判断10086是否在布隆过滤器
(integer) 1
127.0.0.1:6379> BF.EXISTS filter 10087
(integer) 0
127.0.0.1:6379> BF.MADD filter 10088 10089 #一次添加多个值
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> BF.MEXISTS filter 10088 10010 #一次判断多个值是否存在
1) (integer) 1
2) (integer) 0

#重置误判率
bf.reserve key error_rate 的值 initial_size的值 默认的error_rate是0.01,默认的initial_size是100

 

② 编译安装

3、缓存击穿

3.1、是什么

大量的请求同时查询一个key时, 此时这个key正好失效了,就会导致大量的请求都打到数据库上面去

简单说就是热点key突然失效了,暴打mysql

3.2、危害

会造成某一时刻数据库请求量过大,压力剧增。

3.3、解决

方案一:

缓存击穿

热点key失效

互斥更新、随机退避、差异失效时间

方案二:

对于访问频繁的热点key,干脆就不设置过期时间

方案三:

互斥独占锁防止击穿

多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。

其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直 接走缓存。

public User findUserById2(Integer id)
{
    User user = null;
    String key = CACHE_KEY_USER+id;

    //1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql
    user = (User) redisTemplate.opsForValue().get(key);

    if(user == null)
    {
        //2 大厂用,对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysql
        synchronized (UserService.class){
            user = (User) redisTemplate.opsForValue().get(key);
            //3 二次查redis还是null,可以去查mysql了(mysql默认有数据)
            if (user == null) {
                //4 查询mysql拿数据
                user = userMapper.selectByPrimaryKey(id);//mysql有数据默认
                if (user == null) {
                    return null;
                }else{
                    //5 mysql里面有数据的,需要回写redis,完成数据一致性的同步工作
                    redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);
                }
            }
        }
    }
    return user;
}

 

3.4、案例

淘宝聚划算功能实现+防止缓存击穿

分析过程:

步骤

说明

1

100%高并发,绝对不可以用 mysql实现

2

先把 mysql里面参加活动的数据抽取进redis,一般采用定时器扫描来决定上线活动还是下线取消。

3

支持分页功能,一页20条记录

请大家思考, redis里面什么样子的数据类型支持上述功能?

 

redis数据类型选型

list

springboot + redis实现高并发的淘宝聚划算业务

代码一:

@Service
@Slf4j
public class JHSTaskService {
    @Autowired
    private RedisTemplate redisTemplate;

    @PostConstruct
    public void initJHS() {
        log.info("启动定时器淘宝聚划算功能模拟......" + DateUtil.now());
        new Thread(()->{
            while (true){
                List<Product> list = this.products();
                this.redisTemplate.delete(Constants.JHS_KEY);
                this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY,list);
                try {
                    TimeUnit.MINUTES.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("runJhs定时更新.......");
            }
        },"t1").start();
    }

    /**
     * 模拟从数据库读取20件特价商品,用于加载到聚划算的页面中
     *
     * @return
     */
    public List<Product> products() {
        List<Product> list = new ArrayList<>();

        for (int i = 1; i <= 20; i++) {
            Random rand = new Random();
            int id = rand.nextInt(10000);
            Product product = new Product((long) id, "product:" + i, i, "detail:" + i);
            list.add(product);
        }
        return list;
    }
}

@RestController
@Slf4j
public class JHSProductController {
    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping(value = "product/find", method = RequestMethod.GET)
    public List<Product> find(int page, int size) {

        List<Product> list = null;
        long start = (page - 1) * size;
        long end = start + size - 1;
        list = this.redisTemplate.opsForList().range(Constants.JHS_KEY, start, end);
        if (CollectionUtil.isEmpty(list)) {
            //TODO:走DB查询
        }
        log.info("查询结果:{}", list);


        return list;
    }
}

 代码二:

@Service
@Slf4j
public class JHSTaskABService {
    @Autowired
    private RedisTemplate redisTemplate;

    @PostConstruct
    public void initJHS() {
        log.info("启动定时器淘宝聚划算功能模拟......" + DateUtil.now());
        new Thread(() -> {
            while (true) {
                List<Product> list = this.products();
                //先更新缓存B
                this.redisTemplate.delete(Constants.JHS_KEY_B);
                this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY_B, list);
                this.redisTemplate.expire(Constants.JHS_KEY_B, 20L, TimeUnit.DAYS);
                //再更新缓存A
                this.redisTemplate.delete(Constants.JHS_KEY_A);
                this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY_A, list);
                this.redisTemplate.expire(Constants.JHS_KEY_A, 15L, TimeUnit.DAYS);
                try {
                    TimeUnit.MINUTES.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("runJhs定时更新.......");
            }
        }, "t1").start();
    }

    /**
     * 模拟从数据库读取20件特价商品,用于加载到聚划算的页面中
     *
     * @return
     */
    public List<Product> products() {
        List<Product> list = new ArrayList<>();

        for (int i = 1; i <= 20; i++) {
            Random rand = new Random();
            int id = rand.nextInt(10000);
            Product product = new Product((long) id, "product:" + i, i, "detail:" + i);
            list.add(product);
        }
        return list;
    }
}

 

@RestController
@Slf4j
public class JHSABProductController {
    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping(value = "product/findab", method = RequestMethod.GET)
    public List<Product> find(int page, int size) {

        List<Product> list = null;
        long start = (page - 1) * size;
        long end = start + size - 1;
        try {
            list = this.redisTemplate.opsForList().range(Constants.JHS_KEY_A, start, end);
            if (CollectionUtil.isEmpty(list)) {
                log.info("=========A缓存已经失效了,记得人工修补,B缓存自动延续5天");
                //用户先查询缓存A(上面代码),如果缓存A查询不到(例如,更新缓存的时候删除了),再查询缓存B
                list=this.redisTemplate.opsForList().range(Constants.JHS_KEY_B, start, end);
            }
            log.info("查询结果:{}", list);
        } catch (Exception e) {
            //这里的异常,一般是redis瘫痪,或者redis网络timeout
            log.error("exception:", e);
            //TODO:走DB查询
        }

        return list;
    }
}

4、总结

缓存问题

产生原因

解决方案

缓存更新方式

数据变更、缓存时效性

同步更新、失效更新、异步更新、定时更新

缓存不一致

同步更新失败、异步更新

增加重试、补偿任务、最终一致

缓存穿透

恶意攻击

空对象缓存、 bloomfilter过滤器

缓存击穿

热点key失效

互斥更新、随机退避、差异失效时间

缓存雪崩

缓存挂掉

快速失败熔断、主从模式、集群模式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值