分布式缓存击穿以及本地击穿解决方案

缓存击穿

1、定义

缓存的某个热点数据过期,此时大量的用户请求过来,缓存未命中,然后都打到数据库去,导致数据库压力骤增。

2、解决方案

原因就是此时大量请求未命中缓存,导致都打到数据库,此时数据库压力剧增,解决方案分为是单机还是分布式情况部署。

2.1、双重检索(本地方案)

一、基本实现原理

java 复制代码
public Object getData(String key) {
    // 第一次检查(无锁)
    Object value = cache.get(key);
    if (value == null) {
        synchronized (this) {
            // 第二次检查(有锁)
            value = cache.get(key);
            if (value == null) {
                value = db.query(key);  // 查询数据库
                cache.set(key, value);  // 写入缓存
            }
        }
    }
    return value;
}

二、技术细节分析

  1. 双重检查的意义
    • 第一次检查:无锁快速判断,解决大多数缓存命中情况
    • 第二次检查:防止多个线程同时通过第一次检查后重复创建缓存
  2. 与简单同步方案的对比
java 复制代码
// 简单同步方案(性能较差)
public synchronized Object getDataSlow(String key) {
    Object value = cache.get(key);
    if (value == null) {
        value = db.query(key);
        cache.set(key, value);
    }
    return value;
}

性能对比:

双重检查:只在缓存未命中时加锁

简单同步:每次调用都加锁,导致性能上面的浪费。

2.2、互斥锁(分布式方案)

核心思想:只允许一个请求重建缓存,其他请求等待

流程就是:先查询缓存是否命中,未命中,尝试获取互斥锁,获取锁成功,进行缓存重构,未获取锁的就进行递归,进行递归判断是否缓存命中,命中直接返回,未命中继续尝试获取锁,失败继续递归。

java 复制代码
  @Nullable
    // todo 3、缓存击穿 -> 互斥锁:只能由一个线程进行缓存构建,其他线程等待,吞吐量较低
    private Shop huchi(Long id) {
        String shopJsonStr = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        if (StrUtil.isNotBlank(shopJsonStr)) {
            return JSONUtil.toBean(shopJsonStr, Shop.class);
        }
        // 未命中获取锁
        String tryLockKey = "cache:shop:lock:" + id;
        Shop shop = null;
        try {
            boolean tryLock = getLock(tryLockKey);
            // 未命中:不断休眠直至获取成功
            if(!tryLock) {
                Thread.sleep(50);
               return huchi(id);
            }
            // 获取互斥锁,进行缓存的构建
            shop = getById(id);
            if (shop == null) {
                // 数据库中也不存在时候,进行空字符串缓存
                stringRedisTemplate.opsForValue().set("cache:shop:" + id, "", 2, TimeUnit.MINUTES);
                return null;
            }
            stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop), 2, TimeUnit.MINUTES);

        } catch (Exception e) {
            e.getStackTrace();
        } finally {
            unLock(tryLockKey);
        }

        return shop;
    }

优点:保证数据一致性

缺点:可能出现递归导致栈溢出情况,但是这种情况较少可能性。

2.3、逻辑过期(分布式方案)

步骤:

  1. 进行缓存预热:将设置逻辑过期的写入缓存中
  2. 然后获取redis中的值,根据缓存逻辑设置时间是否过期来决定是否进行缓存的重新构建。如果未过期,直接返回。过期,设置互斥锁,获取锁成功的单独开设一个子线程去进行缓存的更新构建,主线程直接返回结果。获取锁失败,直接返回旧数据
    代码实现:
java 复制代码
@Nullable
    // todo 3、缓存击穿 -> 逻辑过期:通过设置逻辑过期时间,然后判断是否过期来确定是否进行缓存更新
    private Shop exLogical(Long id) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        String shopJsonStr = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        // 如果不存在那就是一定不存在
        if (StrUtil.isBlank(shopJsonStr)) {
            return null;
        }
        //
        RedisDate redisDate = JSONUtil.toBean(shopJsonStr, RedisDate.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisDate.getObject(), Shop.class);
        // 未逻辑过期
        if (redisDate.getEx().isAfter(LocalDateTime.now())) {
            return shop;
        }
        // 逻辑过期
        //  缓存重建
        String tryLockKey = "cache:shop:lock:" + id;
        boolean tryLock = getLock(tryLockKey);
        if (tryLock) {
            // 开启独立的线程去独立的进行缓存
            executorService.submit(() -> {
                try {
                    this.saveShopRedis(id, 20L);
                } finally {
                    unLock(tryLockKey);
                }
            });
        }
        return shop;
    }

    // 手动设置逻辑过期时间
    private void saveShopRedis(Long id, Long ex) {
        Shop shop = getById(id);
        RedisDate redisDate = new RedisDate();
        redisDate.setEx(LocalDateTime.now().plusSeconds(ex));
        redisDate.setObject(shop);
        stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(redisDate));
    }

优点:无等待时间,性能较好

缺点:可能会出现短暂的数据性不一致的情况

相关推荐
北执南念1 分钟前
如何在 Spring Boot 中设计和返回树形结构的组织和部门信息
java·spring boot·后端
遗憾皆是温柔5 分钟前
19. 重载的方法能否根据返回值类型进行区分
java·开发语言·面试·学习方法
ts码农5 分钟前
model层实现:
java·服务器·前端
泰勒疯狂展开30 分钟前
Java研学-RabbitMQ(六)
java·rabbitmq·java-rabbitmq
Warren981 小时前
Java Record 类 — 简化不可变对象的写法
java·开发语言·jvm·分布式·算法·mybatis·dubbo
SimonKing1 小时前
流式数据服务端怎么传给前端,前端怎么接收?
java·后端·程序员
Laplaces Demon1 小时前
Spring 源码学习(十)—— DispatcherServlet
java·后端·学习·spring
哈基米喜欢哈哈哈1 小时前
进程和线程
java·linux·windows·笔记
咕白m6251 小时前
Java 高效实现 Word 转 PDF - 掌握关键转换选项
java
都叫我大帅哥2 小时前
谁说数据库不能“直播”?用Debezium玩转实时数据流!
java