拿个offer - 开源&项目实战 拿个offer - 开源&项目实战
首页
后端技术
🚀大话面试
  • 校&社招实战项目

    • 12306(铁路购票平台)
    • 短链接(Sass平台)
  • 社招进阶项目

    • 刚果商城(DDD领域驱动)
  • 基础架构项目

    • 幂等基础组件(支持接口及多种MQ)
    • 企业基础架构组件库
  • 校&社招实战项目

    • 12306(铁路购票平台) (opens new window)
    • 短链接(Sass平台) (opens new window)
  • 社招进阶项目

    • 刚果商城(DDD领域驱动) (opens new window)
加入群聊
🔋知识星球
代码仓库 (opens new window)
首页
后端技术
🚀大话面试
  • 校&社招实战项目

    • 12306(铁路购票平台)
    • 短链接(Sass平台)
  • 社招进阶项目

    • 刚果商城(DDD领域驱动)
  • 基础架构项目

    • 幂等基础组件(支持接口及多种MQ)
    • 企业基础架构组件库
  • 校&社招实战项目

    • 12306(铁路购票平台) (opens new window)
    • 短链接(Sass平台) (opens new window)
  • 社招进阶项目

    • 刚果商城(DDD领域驱动) (opens new window)
加入群聊
🔋知识星球
代码仓库 (opens new window)
  • 小册简介

    • 什么是大话面试
  • Redis

    • Redis为什么这么快?
    • Redis宕机数据会丢失么?
    • Redis的AOF是怎么实现的?
    • Redis的RDB是怎么实现的?
    • Redis如何实现到期删除的?
    • Redis常用内存淘汰策略?
    • Redis字符串底层数据结构?
    • Redis的压缩列表是什么?
    • Redis的跳表是什么?
    • Redis的ZSet底层是怎么实现的?
    • 什么是布隆过滤器?
    • 布隆过滤器容量如何评估?
    • 布隆过滤器容量不够用如何解决?
    • Redis节点CPU核数越高越好?
    • 为什么Redis不适合海量请求?
    • Redis如何应对海量请求?
    • 如何提升Redis批量访问性能?
  • 缓存

    • 如何解决缓存击穿?
      • 面试话术
        • 1. 分布式锁
        • 2. 热点数据预加载
        • 3. 热点数据永不过期
      • 问题详解
        • 1. 查询缓存不存在请求数据库
        • 2. 分布式互斥锁优化数据库压力
        • 3. 双重判定锁
    • 如何解决缓存穿透?
    • 如何解决缓存雪崩?
    • 如何解决大Key问题?
    • 缓存如何预热?
    • 如何发现缓存中热Key?
    • 如何解决热Key问题?
    • 缓存与数据库一致性?
    • 先写DB再删除缓存解决一致性?
    • Binlog配合MQ如何解决一致性?
目录

如何解决缓存击穿?

# 面试话术

缓存击穿指在高并发的系统中,一个热点数据缓存过期或者在缓存中不存在,导致大量并发请求直接访问数据库,从而给数据库造成巨大压力,甚至可能引起宕机。

具体来说,当某个热点数据在缓存中过期时,如果此时有大量并发请求同时访问这个数据,由于缓存中不存在,所有请求都会直接访问数据库,导致数据库负载急剧增加。

一般来说,解决缓存击穿的主要方法分为三种:加分布式锁、热点数据预加载以及热点数据永不过期。

# 1. 分布式锁

分布式锁的解决方案就是保证只有一个请求可以访问数据库,其它请求等待结果。这样可以避免大量的请求同时访问数据库。

但是这种的话有一个弊端,那就是获取分布式锁的请求,都会执行一遍查询数据库,并更新到缓存。理论上只有第一个加载数据库记录请求是有效的。所以可以通过双重判定锁的形式,在获取到分布式锁之后,再次查询一次缓存是否存在,如果缓存中存在数据,就直接返回;如果不存在,才继续执行查询数据库的操作。这样就可以避免大量请求访问数据库。双重判定锁有效提升了解锁性能以及数据库访问。

# 2. 热点数据预加载

热点数据预加载,指的是在活动或者大促开始前,针对已知的热点数据从数据库加载到缓存中,这样可以避免海量请求第一次访问热点数据需要从数据库读取的流程。可以极大减少请求响应时间,有效避免缓存击穿。

# 3. 热点数据永不过期

热点数据永不过期,指的就是可以预知的热点数据,在活动开始前,设置过期时间为-1,这样的话,就不会有缓存击穿的风险。这个可以搭配热点数据预加载一起完成。等对应热点缓存的活动结束后,这些数据访问量就比较低了,可以通过后台任务的方案对指定缓存设置过期时间,这样可以有效降低 Redis 存储压力。

# 问题详解

我们可以从分布式锁的角度上,分析如何通过互斥锁完成缓存击穿场景解决方案?

# 1. 查询缓存不存在请求数据库

第一版,查询数据缓存是否存在,不存在的话请求数据库,数据库存在则把当前数据回写到缓存。

问题比较明显,如果缓存过期或被删除,大量请求就会全部请求到数据库,导致数据库压力暴涨。

伪代码如下:

public String selectTrain(String id) {
    String cacheData = cache.get(id);
    if (StrUtil.isBlank(cacheData)) {
        String dbData = trainMapper.selectId(id);
        if (StrUtil.isNotBlank(dbData)) {
            cahce.set(id, dbData);
            cacheData = dbData;
        }
    }
    return cacheData;
}

# 2. 分布式互斥锁优化数据库压力

在获取数据时,使用分布式锁(如 Redis 的分布式锁)来控制同时只有一个请求可以去后端获取数据,其他请求需要等待锁释放。这样可以防止多个请求同时穿透到后端存储。

在原有基础上继续改进,伪代码如下:

public String selectTrain(String id) {
    String cacheData = cache.get(id);
    if (StrUtil.isBlank(cacheData)) {
        Lock lock = getLock(id);
        lock.lock();
        try {
            String dbData = trainMapper.selectId(id);
            if (StrUtil.isNotBlank(dbData)) {
                cahce.set(id, dbData);
                cacheData = dbData;
            }
        } finally {
            lock.unlock();
        }
    }
    return cacheData;
}

这种方案有效地避免了缓存穿透问题,因为只有一个线程能够在同一时间内查询数据库,其他线程需要等待,不会同时穿透到后端存储系统。

完美解决问题,但是很多老板比较疑惑,为什么我在之前很多文档里都说了双重判定锁这个东西?这玩意是个啥,跟着马哥一起往下看,技术揭秘。

# 3. 双重判定锁

上边还有一个问题就是,假如 100w 的请求读取一个缓存,100w 的请求全部卡在 lock.lock 获取分布式锁处,只有一个线程会执行逻辑请求数据库并放入缓存。问题来了,因为接下来的所有请求,99.99...w 还是会继续请求数据库,大家读一下上面的伪代码就明白了。

这会造成两个实际的问题:

  1. 全部用户获取锁后查询数据库,会对数据库造成无用的性能浪费,因为这 100w 的请求,只有第一次是有效的。
  2. 查询数据库会造成用户响应时间变长,接口吞吐量下降。

双重判断:获取锁后,在查询数据库之前,再次检查一下缓存中是否存在数据。这是一个双重判断,如果缓存中存在数据,就直接返回;如果不存在,才继续执行查询数据库的操作。

伪代码如下:

public String selectTrain(String id) {
    String cacheData = cache.get(id);
    if (StrUtil.isBlank(cacheData)) {
        Lock lock = getLock(id);
        lock.lock();
        try {
            cacheData = cache.get(id);
            if (StrUtil.isBlank(cacheData)) {
                String dbData = trainMapper.selectId(id);
                if (StrUtil.isNotBlank(dbData)) {
                    cahce.set(id, dbData);
                    cacheData = dbData;
                }
            }
        } finally {
            lock.unlock();
        }
    }
    return cacheData;
}

下面是这种解决方案的一般步骤:

  1. 获取锁:在查询数据库前,首先尝试获取一个分布式锁。只有一个线程能够成功获取锁,其他线程需要等待。
  2. 查询数据库:如果双重判断确认数据确实不存在于缓存中,那么就执行查询数据库的操作,获取数据。
  3. 写入缓存:获取到数据后,将数据写入缓存,并设置一个合适的过期时间,以防止缓存永远不会被更新。
  4. 释放锁:最后,释放获取的锁,以便其他线程可以继续使用这个锁。
上次更新: 2023/11/06, 23:14:13
如何提升Redis批量访问性能?
如何解决缓存穿透?

← 如何提升Redis批量访问性能? 如何解决缓存穿透?→

Theme by Vdoing | Copyright © 2022-2023 马丁玩编程 | Apache2 License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式