• 缓存穿透的解决办法有哪些?


    一、概述

    缓存穿透是指查询一个不存在的数据,由于缓存和数据库都没有命中,导致每次请求都需要从数据库中读取数据,增加了数据库的负担。解决缓存穿透的方法有以下几种:

    1. 布隆过滤器(Bloom Filter):使用位数组来表示一个集合,并通过哈希函数将元素映射到数组上。在查询数据时,先判断该数据是否存在于布隆过滤器中,如果存在则直接返回结果,否则再从数据库中查询数据。

    2. 缓存空对象:在缓存中存储空对象,当查询一个不存在的数据时,直接返回空对象,而不是默认值或者错误信息。

    3. 设置热点数据永不过期:对于一些热点数据,可以设置永不过期,这样即使缓存未命中,也不会影响数据的一致性。

    4. 使用分布式锁:在查询数据前先使用分布式锁进行加锁,保证只有一个线程能够访问数据库,其他线程需要等待锁释放后才能进行查询。

    5. 使用数据库的缓存机制:一些数据库提供了自己的缓存机制,可以将查询结果缓存到内存中,减少对数据库的访问次数。

    二、布隆过滤器

    1、原理

    布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,

    假设布隆过滤器判断这个数据不存在,则直接返回

    这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突

    2、优缺点

    • 优点:内存占用较少,没有多余key
    • 缺点:
      • 实现复杂
      • 存在误判可能

    3、流程图

    在这里插入图片描述

    三、缓存空对象

    1、缓存空对象思路分析

    当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了

    2、优缺点

    • 优点:实现简单,维护方便
    • 缺点:
      • 额外的内存消耗
      • 可能造成短期的不一致

    3、流程图:

    在这里插入图片描述

    四、设置热点数据永不过期

    1、说明

    对于缓存穿透问题,设置热点数据永不过期是一种解决方法。热点数据指的是被频繁访问的数据,如果将这些数据的过期时间设置为永久或者相对较长的时间,即使缓存未命中,也不会影响数据的一致性。

    2、具体实现方式:

    • 定义一个包含热点数据的 Map 对象,使用 synchronized 关键字保证并发安全。
    private static Map<String, Object> hotDataMap = new ConcurrentHashMap<>();
    
    • 1
    • 在添加热点数据时,设置过期时间为永久:
    hotDataMap.put("key", value);
    hotDataMap.put("key", value); // 可以重复添加
    
    • 1
    • 2
    • 在查询数据时,先从缓存中获取热点数据,如果缓存中不存在,则从数据库中查询并加入缓存:
    Object value = hotDataMap.get("key");
    if (value == null) {
        // 从数据库中查询数据
        value = getValueFromDatabase();
        hotDataMap.put("key", value);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 将热点数据放入缓存中:
    cache.put(key, value);
    
    • 1

    需要注意的是:

    如果设置了热点数据的永不过期,需要定期清理缓存中的无用数据,以避免占用过多内存。

    五、使用分布式锁

    1 说明

    使用分布式锁可以解决缓存穿透问题。缓存穿透是指查询一个不存在的数据,由于缓存和数据库都没有命中,导致每次请求都需要从数据库中读取数据,增加了数据库的负担。而分布式锁可以在多台服务器之间协调对某个资源的操作,保证同一时间只有一个线程可以对该资源进行操作。

    2 实现步骤

    1. 引入分布式锁框架,如 Redis 的分布式锁、Zookeeper 等。

    2. 在查询数据前先使用分布式锁进行加锁,保证只有一个线程能够访问数据库,其他线程需要等待锁释放后才能进行查询。

    3. 如果查询结果为空,则释放锁并返回空对象;如果查询结果不为空,则将结果存入缓存中。

    4. 在更新数据时,也需要使用分布式锁进行加锁,保证只有一个线程能够对缓存进行更新。

    5. 在释放锁时,需要确保所有线程都已经完成了对数据的处理。

    3 Redis分布式锁的基本流程:

    1. 客户端尝试获取锁,向Redis服务器发送SETNX命令(SET if Not eXists)。

    2. Redis服务器收到SETNX命令,尝试为客户端设置锁,如果该锁不存在,Redis会将锁设置为1,表示客户端获取了锁;否则,Redis返回0,表示客户端未能获取锁。

    3. 客户端收到Redis服务器返回的结果,如果结果为1,则表示客户端已经成功获取了锁,可以执行后续操作;如果结果为0,则表示客户端未能获取锁,需要再次尝试获取或者等待其他客户端释放锁。

    4. 客户端在执行完任务后,需要释放锁,向Redis服务器发送DEL命令,告诉Redis服务器该客户端已经完成任务,锁不再需要。

    5. Redis服务器收到DEL命令,将该客户端的锁删除,其他客户端可以继续尝试获取锁。

    需要注意的是,在分布式环境中,需要使用带有超时时间的锁,以防止锁死。在获取锁时,需要设置一个超时时间,如果在指定时间内未能成功获取锁,则需要放弃获取锁。同时,在释放锁时,需要检查该锁是否属于当前客户端,避免误删其他客户端的锁。

    在这里插入图片描述

    4、缺点

    • 使用分布式锁会增加系统的复杂度和运行成本。
    • 分布式锁也有可能出现死锁等问题,需要进行合理的设计和调试。

    六、具体运用

    1、从没有使用到使用缓存NULL值的流程变化

    在这里插入图片描述

    2、未使用缓存NULL时的代码

     @Override
        public Result queryShopById(Long id) {
            //1 从redis获取
            String shopStr = redisTemplate.opsForValue().get(RedisKey.CACHE_SHOP_PRE + id);
            if (MyStrUtil.isNotEmpty(shopStr)) {
                return Result.ok(JSONUtil.toBean(shopStr, Shop.class));
            }
    
            Shop shop = this.getById(id);
            if (shop == null) {
                return Result.fail("店铺不存在");
            }
            redisTemplate.opsForValue().set(RedisKey.CACHE_SHOP_PRE + id, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);
    
            return Result.ok(shop);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    3、使用缓存NULL时的代码

    @Override
        public Result queryShopById(Long id) {
            //1 从redis获取
            String shopStr = redisTemplate.opsForValue().get(RedisKey.CACHE_SHOP_PRE + id);
            if (StringUtils.isNotBlank(shopStr)) {
                return Result.ok(JSONUtil.toBean(shopStr, Shop.class));
            }
    
            //空字符串
            if(shopStr != null){
                return Result.fail("店铺不存在");
            }
    
            Shop shop = this.getById(id);
            if (shop == null) {
                redisTemplate.opsForValue().set(RedisKey.CACHE_SHOP_PRE + id, "", 1, TimeUnit.MINUTES);
                return Result.fail("店铺不存在");
            }
            redisTemplate.opsForValue().set(RedisKey.CACHE_SHOP_PRE + id, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);
    
            return Result.ok(shop);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    七、源码下载:

    gitee.com/charlinchenlin/koo-erp

  • 相关阅读:
    Wireshark TS | 网络路径不一致传输丢包问题
    esxi 6.7下安装黑裙
    pgsql_全文检索_使用空间换时间的方法支持中文搜索
    网络工程师----第三十六天
    msys如何编译64位ffmpeg 带x264 x265
    蓝桥等考Python组别二级004
    【JDBC】01-JDBC概述
    机器学习:用一个例子通俗理解变量消除法VE原理(附Python实验)
    GO实现Redis:GO实现内存数据库(3)
    雷达编程实战之提高探测速度
  • 原文地址:https://blog.csdn.net/lovoo/article/details/130789257