缓存一致性、缓存穿透、缓存雪崩、缓存击穿代码解决方案

1.Redis的内存淘汰机制(保持数据库和redis一致性)

1.1 内存淘汰

Redis 提供了多种内存淘汰策略,当内存使用达到配置的最大限制时,Redis 会根据策略决定如何删除某些键,以释放内存空间。

常见的内存淘汰策略有:

  • volatile-lru:在设置了过期时间的键中,优先删除最近最少使用的键。
  • allkeys-lru:在所有键中,优先删除最近最少使用的键。
  • volatile-lfu:在设置了过期时间的键中,优先删除最不常使用的键。
  • allkeys-lfu:在所有键中,优先删除最不常使用的键。
  • volatile-random:在设置了过期时间的键中,随机删除键。
  • allkeys-random:在所有键中,随机删除键。
  • volatile-ttl:在设置了过期时间的键中,优先删除剩余生存时间(TTL)最短的键。
  • noeviction:不删除任何键,而是返回错误。
1.2 超时剔除

Redis允许为每个键设置一个过期时间(TTL),当键超过其生存时间时,Redis会自动删除它。可以使用 EXPIRE 命令设置TTL。

自动剔除过期键的过程是由Redis的定期扫描和惰性删除两种机制共同完成的:

  • 定期扫描:Redis每隔一段时间会随机检查一部分键,并删除已过期的键。
  • 惰性删除:当访问一个键时,如果该键已过期,则Redis会立即删除该键。

上面两个方式都是通过对redis进行设置即可

1.3 主动更新

主动更新机制指的是在缓存中主动更新某些键的值,以确保缓存中的数据是最新的。
主动更新策略有3个问题需要考虑。

在这里插入图片描述

在这里插入图片描述

这里首先介绍单机模式下的缓存与数据库的一致性。(单机模式下选择 1.将缓存与数据库操作放在一个事务 2.先更新数据库再删除缓存)
为什么先更新数据库再删除缓存?

原因一:并发问题

假设有两个并发请求A和B,如果顺序是先删除缓存,再更新数据库,可能会出现以下情况:

  1. 请求A删除缓存。
  2. 请求B读取缓存,发现缓存已删除,读取数据库中的旧数据,并将其写入缓存。
  3. 请求A更新数据库,此时缓存中的数据是旧的,导致缓存与数据库不一致。

原因二:缓存雪崩

如果顺序是先删除缓存,再更新数据库,短时间内大量请求会直接访问数据库,增加数据库负载,甚至可能导致数据库崩溃。这种现象被称为缓存雪崩。

如果顺序是先删除缓存,再更新数据库,短时间内大量请求会直接访问数据库,增加数据库负载,甚至可能导致数据库崩溃。这种现象被称为缓存雪崩。

 @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if (id == null){
            return Result.fail("店铺id不能为空");
        }
        //1.更新数据库
        updateById(shop);
        //2.删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
        return null;
    }
1.4结合实际场景选择上述的缓存更新策略

在这里插入图片描述

2.缓存穿透

2.1解决方案一 缓存空字符串

解决策略:使用存空值如缓存实现 先查缓存,如果缓存存在redis且不为null则返回。 若不为null,则表明是空字符串,则返回‘店铺信息不存在’。 为空值,则将空字符串缓存存到redis,显示‘店铺不存在’,返回前端。

    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY +id;
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 判断命中的是否是空字符串
        if(shopJson != null){
            //返回错误信息
            return Result.fail("店铺信息不存在");
        }

        //4.不存在,根据id查询数据库
        Shop shop = getById(id);

        //5.数据库中不存在,返回错误
        if (shop == null) {
            //将空字符串写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            //返回错误信息
            return Result.fail("店铺不存在");
        }

        //6.存在,写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        //7.返回店铺信息
        return Result.ok(shop);
    }
2.2解决方案二 手写布隆过滤
import java.nio.charset.StandardCharsets;
import java.util.BitSet;
import java.util.Random;

public class BloomFilter {
    private BitSet bitSet;
    private int bitSetSize;
    private int numHashFunctions;
    private int[] hashSeeds;

    public BloomFilter(int bitSetSize, int numHashFunctions) {
        this.bitSetSize = bitSetSize;
        this.numHashFunctions = numHashFunctions;
        this.bitSet = new BitSet(bitSetSize);
        this.hashSeeds = new int[numHashFunctions];

        // 初始化哈希函数的种子
        Random random = new Random();
        for (int i = 0; i < numHashFunctions; i++) {
            hashSeeds[i] = random.nextInt();
        }
    }

    public void add(String value) {
        byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
        for (int seed : hashSeeds) {
            int hash = hash(bytes, seed);
            bitSet.set(Math.abs(hash % bitSetSize), true);
        }
    }

    public boolean mightContain(String value) {
        byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
        for (int seed : hashSeeds) {
            int hash = hash(bytes, seed);
            if (!bitSet.get(Math.abs(hash % bitSetSize))) {
                return false;
            }
        }
        return true;
    }

    private int hash(byte[] bytes, int seed) {
        int hash = 0;
        for (byte b : bytes) {
            hash = seed * hash + b;
        }
        return hash;
    }

    public static void main(String[] args) {
        BloomFilter bloomFilter = new BloomFilter(1000, 3);

        // 添加元素到布隆过滤器
        bloomFilter.add("hello");
        bloomFilter.add("world");

        // 检查元素是否在布隆过滤器中
        System.out.println(bloomFilter.mightContain("hello")); // true
        System.out.println(bloomFilter.mightContain("world")); // true
        System.out.println(bloomFilter.mightContain("java"));  // false
    }
}

如果你不想手写布隆过滤器,则可以使用例如hutool包中封装的BloomFilter。

​ 布隆过滤器(Bloom Filter)是一种高效的概率型数据结构,用于测试一个元素是否在一个集合中。它具有较高的空间效率和查询效率,但允许一定的误判率(即可能会误判一个不存在的元素为存在)。布隆过滤器不会产生假阴性(false negative),即如果它认为一个元素不存在,该元素一定不存在,但会产生假阳性(false positive),即它可能会认为一个不存在的元素存在。

2.3布隆过滤器的基本原理
  1. 数据结构:布隆过滤器由一个位数组(bit array)和一组独立的哈希函数组成。
  2. 哈希函数:布隆过滤器使用 kkk 个不同的哈希函数,每个哈希函数将输入元素映射到位数组中的一个位置。
  3. 添加元素
    • 当添加一个元素到布隆过滤器时,使用 kkk 个哈希函数分别计算该元素的哈希值,并将对应的 kkk 个位置的位设置为1。
    • 例如,对于一个元素 xxx,假设位数组长度为 mmm,使用的哈希函数为 h1,h2,…,hkh_1, h_2, …, h_kh1,h2,…,hk,则计算 h1(x),h2(x),…,hk(x)h_1(x), h_2(x), …, h_k(x)h1(x),h2(x),…,hk(x),并将这些位置设置为1。
  4. 查询元素
    • 当查询一个元素是否在布隆过滤器中时,同样使用 kkk 个哈希函数计算该元素的哈希值,检查对应的 kkk 个位置的位是否都为1。
    • 如果所有的位置都为1,则该元素可能在集合中;如果有任意一个位置为0,则该元素一定不在集合中。

​ 5.优点

  • 空间效率高:相比于其他数据结构,如哈希表,布隆过滤器可以在较小的空间内表示大量元素。
  • 查询效率高:查询操作只需要计算 kkk 个哈希函数并检查 kkk 个位置,非常高效。6.

​ 6.缺点

  • 误判率:布隆过滤器可能会误判一个不存在的元素为存在。
  • 删除困难:由于哈希冲突的存在,删除元素会影响其他元素的查询结果。
2.4如何将数据批量写入布隆过滤器

bloomFilter.add();写入数据的效率过低。如何高效写入数据?

Lua脚本实现。

-- bloom_filter_batch_add.lua
local key = KEYS[1]
local values = ARGV

for i, value in ipairs(values) do
    redis.call('BF.ADD', key, value)
end

return true

具体的lua脚本语法,可自行去学习。

2.5避免缓存穿透,前期预防也很重要
  1. 增强id的复杂度,避免被猜测id规律
  2. 做好数据的基础格式校验

3.缓存雪崩

在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

3.1解决方案
  1. 给不同的Key的TTL添加随机值
  2. 利用Redis集群提高服务的可用性
  3. 给缓存业务添加降级限流策略
  4. 给业务添加多级缓存

目前未在实际项目中使用,后续补充代码实现

4.缓存击穿

4.1互斥锁实现缓存重建

解决策略:添加互斥锁实现缓存重建 先查缓存,如果缓存存在redis且不为null则返回。 如果为空字符,则尝试获取互斥锁,如果获取锁成功,则继续执行。查询数据库,查到写入redis,查不到则返回错误信息。 如果获取锁失败,则休眠一短时间,重新从头开始执行查询操作。

//线程池
    private static final ExecutorService CACAHE_REBUID_EXECUTOR = Executors.newFixedThreadPool(10);

//互斥锁代码逻辑
    public Shop queryWithMutex(Long id){
        String key = CACHE_SHOP_KEY +id;
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 判断命中的是否是空字符串
        if(shopJson != null){
            //返回错误信息
            return Result.fail("店铺信息不存在");
            return null;
        }

        //4.实现缓存重建
        //4.1获取互斥锁
        String lockKey = "lock:shop:" + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            //4.2判断是否获取成功
            if(!isLock){
                //4.3失败,休眠并重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }

            //4.4成功,根据id查询数据库
            shop = getById(id);
            //  模拟重建的延时
            Thread.sleep(200);
            //5.数据库中不存在,返回错误
            if (shop == null) {
                //将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                //返回错误信息
                return null;
            }

            //6.存在,写入redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //7.释放互斥锁
        unlock(lockKey);
        //8.返回店铺信息
        return shop;
    }
    
     //获取锁方法
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    //释放锁方法
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

4.2添加过期时间expire 解决热点数据过期问题

解决策略:添加过期时间expire 先查缓存,如果缓存不存在,直接返回空。 查到缓存,则查询过期时间expire是否过期。 如果未过期,则返回店铺信息,已经过期则获取互斥锁,实现缓存重建,然后释放锁。 获取互斥锁失败,则返回过期的店铺信息。

//添加逻辑过期解决缓存击穿
    public Shop queryWithLogicalExpire(Long id){
        String key = CACHE_SHOP_KEY +id;
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            //3.不存在,直接返回
            return null;
        }
        //4.命中,需要把json反序列为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //5.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //5.1 未过期,直接返回店铺信息
            return shop;
        }
        //5.2已过期,需要缓存重建
        //6.缓存重建
        //6.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        //6.2判断是否获取锁成功
        if (isLock) {
            //6.3.成功,开启独立线程,实现缓存重建
            CACAHE_REBUID_EXECUTOR.submit(()->{
                try {
                    //重建缓存
                    this.saveShop2Redis(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }
        //6.4.失败,返回过期的店铺信息
        return shop;
    }
    
    //获取锁方法
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    //释放锁方法
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值