14、缓存预热+缓存雪崩+缓存击穿+缓存穿透

缓存预热+缓存雪崩+缓存击穿+缓存穿透

● 缓存预热、雪崩、穿透、击穿分别是什么?你遇到过那几个情况?

● 缓存预热你是怎么做到的?

● 如何避免或者减少缓存雪崩?

● 穿透和击穿有什么区别?它两一个意思还是截然不同?

● 穿透和击穿你有什么解决方案?如何避免?

● 加入出现了缓存不一致,你有哪些修补方案?

1、缓存预热

2、缓存雪崩

发生原因

● Redis主机挂了,Redis全盘崩溃,偏硬件运维。

● Redis中有大量key同时过期大面积失效,偏软件开发。

预防+解决

● Redis中key设置为永不过期or过期时间错开

● Redis缓存集群实现高可用

○ 主从+哨兵

○ Redis Cluster

○ 开启Redis持久化机制RDB/AOF,尽快恢复缓存集群

● 多缓存结合预防雪崩

○ ehcache本地缓存+redis缓存

● 服务降级

○ Hystrix或者案例sentinel限流&降级

3、缓存穿透

发生原因

请求去查一条记录,先查Redis无,后查MySQL无,都查不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库压力暴增,这种就是缓存穿透。
简单来说就是本来无一物,两库都没有,既不在Redis缓存库,也不再MySQL,数据库存在被多次暴击风险

解决方案

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

一般正常情况下使用回写增强:mysql也查不到的话就让redis存入刚刚查不到的key并保护mysql,第一次来查询没有查询到,redis和mysql都没有,返回null给调用者,但是增强回写后第二次查同样的key,此时redis就有值了,可以直接从redis中读取default缺省值返回给业务程序,避免了把大量请求发送给mysql处理,打爆mysql------------>此种方法架不住黑客的恶意攻击,有缺陷...只能解决key相同的情况。

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

key相同--->第一次达到mysql,空对象缓存后第二次就返回default缺省值,避免mysql再被攻击,不用再到数据库中走一圈了。

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

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

Guava中布隆过滤器的实现算是比较权威的,所以实际项目中可以直接采用Guava布隆过滤器

白名单过滤器实战

白名单那过滤器架构说明

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

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

改POM

java 复制代码
        <!--  Guava  Google开源的Guava中自带的布隆过滤器-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
        </dependency>

业务类

我们的目的是再白名单里面设置100w的数据,然后再额外加入10w的数据,看一下误判率是多少

java 复制代码
/**
 * @author Guanghao Wei
 * @create 2023-04-25 14:51
 */
@Service
@Slf4j
public class GuavaWithBloomFilterService {
    //定义常量
    public static final int _1W = 10000;
    //定义guava布隆过滤器初始容量
    public static final int SIZE = 100 * _1W;
    //误判率,它越小,误判个数越少
    public static double fpp = 0.03;
    //创建guava布隆过滤器
    private BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE, fpp);

    public void guavaBloomFilter() {
        //先让bloomFilter加入100w数据
        for (int i = 1; i <= SIZE; i++) {
            bloomFilter.put(i);
        }
        //故意取10w个不在合法范围内的数据
        ArrayList<Object> list = new ArrayList<>(10 * _1W);
        //验证
        for (int i = SIZE + 1; i <= SIZE + (10 * _1W); i++) {
            if (bloomFilter.mightContain(i)) {
                log.info("被误判了:{}", i);
                list.add(i);
            }
        }
        log.info("误判总数量:{}", list.size());
    }
}
java 复制代码
/**
 * @author Guanghao Wei
 * @create 2023-04-25 14:51
 */
@Api(tags = "google工具Guava处理布隆过滤器")
@RestController
@Slf4j
public class GuavaWithBloomFilterController {
    @Autowired
    private GuavaWithBloomFilterService guavaWithBloomFilterService;

    @ApiOperation("guava布隆过滤器插入100万样本数据并额外添加10w测试是否存在")
    @GetMapping("guavafilter")
    public void guavaBloomFilter() {
        guavaWithBloomFilterService.guavaBloomFilter();
    }
}

这里有一个误判率的知识点我们通过debug源码来学习:

布隆过滤器说明

缓存击穿

是什么

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

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

穿透和击穿,截然不同

热点key为什么失效?

时间到了自然清除但还未被访问到
delete掉的key,刚巧又被访问

危害

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

一般技术部门需要知道热点key是哪些,做到心里有数防止击穿

解决

方案一:差异失效时间

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

方案二:互斥更新

采用双检加锁策略

案例

天猫聚划算功能实现+防止缓存击穿

数据类型可以选用list和zset,但这类场景一般还是选择list

实体类

java 复制代码
/**
 * @author Guanghao Wei
 * @create 2023-04-25 15:40
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "聚划算活动product信息")
public class Product {
    private Long id;
    private String name;
    private Integer price;
    private String detail;
}

service

java 复制代码
/**
 * @author Guanghao Wei
 * @create 2023-04-25 15:42
 */
@Service
@Slf4j
public class JHSTaskService {
    public static final String JHS_KEY = "jhs";
    public static final String JHS_KEY_A = "jhs:a";
    public static final String JHS_KEY_B = "jhs:b";
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 模拟从数据库读取20件特价商品,用于加载到聚划算的页面中
     *
     * @return
     */
    private List<Product> getProductsFromMysql() {
        List<Product> list = new ArrayList<>();
        for (int i = 1; i <= 20; i++) {
            Random random = new Random();
            int id = random.nextInt(10000);
            Product obj = new Product((long) id, "product" + i, i, "detail");
            list.add(obj);
        }
        return list;
    }

    @PostConstruct
    public void init() {
        log.info("启动定时器天猫聚划算功能模拟开始.........O(∩_∩)O");
        //用线程模拟定时任务,后台任务定时将mysql里面的参加活动的商品刷进redis
        new Thread(() -> {
            //模拟从mysql查出数据用于加载进redis,在页面展示
            List<Product> productList = this.getProductsFromMysql();
            //采用redis list数据结构的lpush命令来存储
            redisTemplate.delete(JHS_KEY);
            //加入最新的数据
            redisTemplate.opsForList().leftPushAll(JHS_KEY, productList);
            //暂停1分钟,间隔一分钟执行一次,模拟聚划算一天执行的参加活动的品牌
            try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        }, "t1").start();
    }
}

controller

java 复制代码
/**
 * @author Guanghao Wei
 * @create 2023-04-25 15:42
 */
@Api(tags = "聚划算页面展示控制器")
@RestController
@Slf4j
public class JHSProductController {
    public static final String JHS_KEY = "jhs";
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 分页查询,在高并发的情况下,只能走Redis查询,走db的话必定会吧db打垮
     *
     * @param page
     * @param size
     * @return
     */
    @ApiOperation("聚划算案例,每次1页展示5条数据")
    @GetMapping("product/find")
    public List<Product> find(int page, int size) {
        List<Product> list = null;
        long start = (page - 1) * size;
        long end = start + size - 1;
        try {
            list = redisTemplate.opsForList().range(JHS_KEY, start, end);
            if (CollectionUtils.isEmpty(list)) {
                //走数据库查询 TODO
            }
            log.info("参加活动的商家:{}",list);
        } catch (Exception e) {
            //出异常了,一般redis宕机了,或者redis网络抖动导致timeout
            log.error("jhs exception:{}",e);
            e.printStackTrace();
            //再次查询
        }
        return list;
    }

}

至此步骤,上述聚划算的功能算是完成了,请思考在高并发情况下又会产生什么样的经典生产问题?

Bug和隐患说明

热点key突然失效导致可怕的缓存击穿:delete命令执行的一瞬间有空隙,其他请求线程找Redis为null,达到mysql,暴击mysql...

复习again

最终目的:2条命令原子性是其次的,主要是防止热点key突然失效暴击mysql打爆系统。

进一步升级加固案例
互斥更新--->双检加锁策略

差异失效时间,在本案例中给我们使用这个方式

java 复制代码
    @PostConstruct
    public void initJHSAB() {
        log.info("启动AB定时器天猫聚划算功能模拟开始.........O(∩_∩)O" + DateUtil.now());
        //用线程模拟定时任务,后台任务定时将mysql里面的参加活动的商品刷进redis
        new Thread(() -> {
            //模拟从mysql查出数据用于加载进redis,在页面展示
            List<Product> productList = this.getProductsFromMysql();
            //先更新B缓存,且让B过期时间超过A,B做兜底
            redisTemplate.delete(JHS_KEY_B);
            redisTemplate.opsForList().leftPushAll(JHS_KEY_B, productList);
            redisTemplate.expire(JHS_KEY_B, 86410L, TimeUnit.SECONDS);
            //在更新A缓存
            redisTemplate.delete(JHS_KEY_A);
            redisTemplate.opsForList().leftPushAll(JHS_KEY_A, productList);
            redisTemplate.expire(JHS_KEY_A, 86400L, TimeUnit.SECONDS);
            //暂停1分钟,间隔一分钟执行一次,模拟聚划算一天执行的参加活动的品牌
            try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        }, "t1").start();
    }
java 复制代码
    @ApiOperation("AB双缓存架构,防止热点key突然失效")
    @GetMapping("product/findAB")
    public List<Product> findAB(int page, int size) {
        List<Product> list = null;
        long start = (page - 1) * size;
        long end = start + size - 1;
        try {
            list = redisTemplate.opsForList().range(JHS_KEY_A, start, end);
            if (CollectionUtils.isEmpty(list)) {
                log.info("-------A缓存已经失效或者过期了,记得人工修改,B缓存继续顶着");
                list = redisTemplate.opsForList().range(JHS_KEY_B, start, end);

                if (CollectionUtils.isEmpty(list)) {
                    //TODO 走数据库查询
                }
            }
        } catch (Exception e) {
            //出异常了,一般redis宕机了,或者redis网络抖动导致timeout
            log.error("jhs exception:{}", e);
            e.printStackTrace();
            //再次查询
        }
        return list;
    }
相关推荐
Albert Edison2 小时前
【Redis】Centos7.9 安装 Redis 5 教程
数据库·redis·缓存
Steadfast_GG3 小时前
Redis中的通用命令
redis·缓存
小二·3 小时前
Redis 内存溢出(OOM)排查与恢复实战
数据库·redis·bootstrap
pqk6V6Vep3 小时前
Redis 分布式锁进阶第一篇讲解
数据库·redis·分布式
giaz14n9X3 小时前
Redis 分布式锁进阶第六十一篇
数据库·redis·分布式
JAVA面经实录9176 小时前
Redis 知识体系(完整版)
java·redis·nosql数据库·nosql
颜笑晏晏7 小时前
长输入短输出场景下的 SGLang 推理性能实测前缀缓存、PD 分离配比与参数调优
缓存·推理优化·sglang·ai infra·pd分离
ManageEngine卓豪7 小时前
数据库可观测性:MySQL与Redis监控核心监控指标与全栈运维解决方案
数据库·redis·mysql·数据库性能·数据库监控
真实的菜8 小时前
Redis 从入门到精通(十四):Redis 7.x 新特性全解 —— 系列收官之作
数据库·redis·缓存
小小工匠9 小时前
Redis - 缓冲区管理:避免溢出引发的“惨案“
redis·性能优化·集群·内存管理·持久化