Redis之缓存预热、缓存雪崩、缓存击穿和缓存穿透以及布隆过滤器

文章目录

  • 一、缓存预热
  • 二、缓存雪崩
  • 三、缓存击穿
  • 四、缓存穿透
  • 五、布隆过滤器
    • 1. 原理实现
    • 2. Google工具包Guava实现布隆过滤器
    • 3. Redis布隆过滤器解决缓存穿透
    • 4. 安装rebloom

一、缓存预热

缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

二、缓存雪崩

指的是在短时间内,大量的键过期导致请求直接打到数据库,或者是Redis宕机,导致请求直接打到数据库,而数据库无法处理如此大量的请求,导致数据库宕机,进而造成整个系统或者服务的不可用。牵一发而动全身。

解决方案:

  1. Redis缓存集群实现高可用(合理设置过期时间或者延时,采用哨兵集群等)
  2. 采用多级缓存策略,例如 Nginx缓存+Redis缓存+ehcache缓存
  3. 采用Hystrix或者阿里sentinel进行服务限流限流或者降级
  4. 监控Redis各项指标,即使灾难预警
  5. 采用AOF/ RDB持久化,尽快恢复Redis集群

三、缓存击穿

缓存击穿就是单个热点key突然失效或者过期,导致大量的请求未命中Redis之后打在数据库上,导致数据库压力剧增

解决方案

  • 对于热点key加长过期时间,或者干脆不设置过期时间
  • 二级缓存设置不同的失效时间,保证不会同时失效
  • 如果缓存中没有该key则加锁,保证只会有一个线程打到数据库进行查询(效率较低)

四、缓存穿透

缓存穿透指的是⼤量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本
没有经过缓存这⼀层,并且在数据库中也没有该跳数据,导致也没有继续回写到缓存中,导致大量请求直接打到数据库层面

解决方案:

  • 缓存空对象或者缺省值
    • 如果在数据库也无法查询,同样回写到缓存中null值,第二次请求就不会落到数据库中
    • 如果黑客恶意攻击每次采取不同的ID那么缓存将越写越多,所以要设置过期时间,并且大量请求还是直接打中了数据库所以该方案有缺陷
  • 使用布隆过滤器,下一章详细讲解

五、布隆过滤器

想要尽量避免缓存穿透,一个办法就是对数据进行预校验,在对Redis和数据库进行操作前,先检查数据是否存在,如果不存在就直接返回。如果我们想要查询一个元素是否存在,要保证查询效率,可以选择HashSet,但是如果有10亿个数据,都用HashSet进行存储,内存肯定是无法容纳的。这时就需要布隆过滤器了

布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(bit数组)一系列随机映射函数(hash)。布隆过滤器可以用于检索一个元素是否在一个集合中

特点:

  • 高效地插入和查询,占用空间少,返回的结果是不确定性的。
  • 一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在。
  • 布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。
  • 误判只会发生在过滤器没有添加过的元素,对于添加过的元素不会发生误判。

1. 原理实现

① 初始化

布隆过滤器 本质上 是由长度为 m 的位向量或位列表(仅包含 0 或 1 位值的列表)组成,最初所有的值均设置为 0

Redis之缓存预热、缓存雪崩、缓存击穿和缓存穿透以及布隆过滤器_第1张图片
② 添加

当我们向布隆过滤器中添加数据时,为了尽量地址不冲突,会使用多个 hash 函数对 key 进行运算,算得一个下标索引值,然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。

例如,我们添加一个字符串wmyskxz

Redis之缓存预热、缓存雪崩、缓存击穿和缓存穿透以及布隆过滤器_第2张图片
③ 查询

向布隆过滤器查询某个key是否存在时,先把这个 key 通过相同的多个 hash 函数进行运算,查看对应的位置是否都为 1

只要有一个位为 0,那么说明布隆过滤器中这个 key 不存在

如果这几个位置全都是 1,那么说明极有可能存在;

为什么说极有可能呢?

因为这些位置的 1 可能是因为其他的 key 存在导致的,也就是hash冲突

就比如我们在 add 了字符串wmyskxz数据之后,很明显1/3/5 这几个位置的 1 ,如上图,是因为第一次添加的 wmyskxz 而导致的;此时我们查询一个没添加过的不存在的字符串inexistent-key,它有可能计算后坑位也是1/3/5 ,这时候经过hash函数计算以及查询发现1 /3 / 5 的位置都是1,所以布隆过滤器认为该数据是存在的,就发生了误判

Redis之缓存预热、缓存雪崩、缓存击穿和缓存穿透以及布隆过滤器_第3张图片
为什么key不能删除呢?

因为该key经过运算放入bit数组中的值,并不一定是你产生的,有可能其他数值也共用着这个数位,如果你删除了该位置,别的key进行查询时可能导致查询失败

2. Google工具包Guava实现布隆过滤器

  • 首先将maven中导入相关依赖(其他一些SpringBoot整合redis依赖就不赘述了)
        
        <dependency>
            <groupId>com.google.guavagroupId>
            <artifactId>guavaartifactId>
            <version>23.0version>
        dependency>
  • 模拟秒杀场景获取商品详情页面的单机版Guava实现布隆过滤器

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import lombok.Data;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * @Description:模拟秒杀,演示布隆过滤器业务逻辑比较简单
 * @Date 2021/6/20 17:26
 * @@author: A.iguodala
 */
@RestController
public class SeckillGuavaBloomFilterDemoController implements InitializingBean {

    /**
     * 模拟秒杀服务,真实中用自动注入
     * @Autowired
     */
    private SeckillService seckillService = new SeckillService();

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 布隆过滤器
     * 三个参数 (Funnel funnel, long expectedInsertions, double fpp)
     * Funnel funnel : 数据类型 (long 保存商品ID)
     * long expectedInsertions : 希望插入值的个数
     * double fpp : 错误率,不设置默认为0.03 ,过小的话误判太多,造成数据库接受到大量请求,过大数组长度过长效率降低
     */
    BloomFilter<Long> filter = BloomFilter.create(Funnels.longFunnel(), 100,0.03);


    /**
     * 模拟获取商品详情请求
     * @param goodsId
     * @return
     */
    @RequestMapping(value = "/toDetail/{goodsId}")
    public Map<String, Object> toDetail(@PathVariable(value = "goodsId") Long goodsId) {
        Map<String, Object> map = new HashMap<>();

		/**
         * 先判断该商品是存在,存在则查询缓存或者数据库,不存在直接返回
         */
        if (filter.mightContain(goodsId)) {
            SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.opsForValue().get("goods:" + goodsId);
            if (seckillGoods == null) {
                seckillGoods = seckillService.get();
                redisTemplate.opsForValue().set("goods:" + goodsId, seckillGoods);
            }
            map.put("data", seckillGoods);
            map.put("code", 200);
            return map;
        }else {
            map.put("code", 400);
            return map;
        }
    }

    /**
     * 缓存预热,将秒杀商品加入到redis以及boolmFilter中
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {

        List<SeckillGoods> goodsList = seckillService.list();
        if (CollectionUtils.isEmpty(goodsList)) {
            return;
        }
        // 循环将数据加入过滤器
        goodsList.forEach(goods -> {
            filter.put(goods.getId());
        });
    }
}

/**
 * 模拟秒杀服务
 */
class SeckillService {

    /**
     * 模拟查询所有商品
     * @return
     */
    List<SeckillGoods> list() {
        return null;
    }

    /**
     * 模拟查询商品
     * @return
     */
    SeckillGoods get() {
        return null;
    }
}

/**
 * 模拟秒杀商品
 */
@Data
class SeckillGoods {

    /**
     * 商品ID
     */
    private Long id;
}

3. Redis布隆过滤器解决缓存穿透

  • 导入 redisson 依赖,里面包括布隆过滤器,分布式锁等
        
        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
            <version>3.13.4version>
        dependency>
  • 和上一个同样的场景,使用redis自己的布隆过滤器,区别在于该过滤器可以使用集群的方式
  • config.useClusterServers().addNodeAddress(“redis://127.0.0.1:6379”, “redis://127.0.0.2:6379”);

import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.redisson.config.Config;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Description:模拟秒杀获取商品详情功能
 * @Date 2021/6/20 22:27
 * @@author: A.iguodala
 */
@RestController
public class SeckillRedissonBloomFilterDemoController implements InitializingBean {

    /**
     * 操作redisson客户端(jedis)
     */
    static RedissonClient redissonClient = null;

    /**
     * redis版内置的布隆过滤器
     */
    static RBloomFilter rBloomFilter = null;

    /**
     * 模拟秒杀服务,真实中用自动注入
     * @Autowired
     */
    private SeckillService seckillService = new SeckillService();
    
    @Autowired
    private RedisTemplate redisTemplate;


    static
    {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
        
        // 集群版
        //config.useClusterServers().addNodeAddress("redis://127.0.0.1:6379", "redis://127.0.0.1:6379");
        
        //构造redisson
        redissonClient = Redisson.create(config);
        //通过redisson构造rBloomFilter
        rBloomFilter = redissonClient.getBloomFilter("seckillGoodsBloomFilter",new StringCodec());
        rBloomFilter.tryInit(10000,0.03);
    }

    /**
     * 模拟获取商品详情请求
     * @param goodsId
     * @return
     */
    @RequestMapping(value = "/toDetail/{goodsId}")
    public Map<String, Object> toDetail(@PathVariable(value = "goodsId") Long goodsId) {
        Map<String, Object> map = new HashMap<>();
        /**
         * 先判断该商品是存在,存在则查询缓存或者数据库,不存在直接返回
         */
        if (rBloomFilter.contains(goodsId)) {
            SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.opsForValue().get("goods:" + goodsId);
            if (seckillGoods == null) {
                seckillGoods = seckillService.get();
                redisTemplate.opsForValue().set("goods:" + goodsId, seckillGoods);
            }
            map.put("data", seckillGoods);
            map.put("code", 200);
            return map;
        }else {
            map.put("code", 400);
            return map;
        }
    }
    /**
     * 缓存预热,将秒杀商品加入boolmFilter中
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {

        List<SeckillGoods> goodsList = seckillService.list();
        if (CollectionUtils.isEmpty(goodsList)) {
            return;
        }
        // 循环将数据加入过滤器
        goodsList.forEach(goods -> {
            rBloomFilter.add(goods.getId());
        });
    }
}

/**
 * 模拟秒杀服务
 */
class SeckillService {

    /**
     * 模拟查询所有商品
     * @return
     */
    List<SeckillGoods> list() {
        return null;
    }

    /**
     * 模拟查询商品
     * @return
     */
    SeckillGoods get() {
        return null;
    }
}

/**
 * 模拟秒杀商品
 */
@Data
class SeckillGoods {

    /**
     * 商品ID
     */
    private Long id;
}

4. 安装rebloom

安装一个redis的插件rebloom,可以通过命令行来对redis操作布隆过滤器,比较简单就不演示了

你可能感兴趣的:(Redis,数据库,redis,java,缓存,分布式)