基于single flight来解决缓存击穿

目录

    • 1. 缓存击穿
    • 2. 常见解决方案
    • 3.single flight方式
      • 3.1 模拟业务场景
      • 3.2 使用single flight的方式

缓存雪崩、缓存击穿、缓存穿透不单单是缓存领域的经典场景,更是面试当牛马时必备(背)八股文。

我们来讨论下缓存击穿场景下的解决方案。

1. 缓存击穿

高并发场景下,某个缓存到了过期时间,自动失效,导致大量请求在该缓存中查询不到值,会直接请求数据库进行查询,连接过多可能会导致数据库压力过大无法响应,从而导致系统宕机。

2. 常见解决方案

  • 缓存永不过期

既然缓存过期会导致缓存,我们可以让它没机会过期,在设置缓存过期时间时设置为永不过期就好了。

这种方式简单且方便理解,但缺点也明显。

首先缓存本身不是做永久性数据存储,要不然也不会叫做’缓’存,缓存一般使用的是内存,相对于磁盘来说是一种昂贵的资源,当需要缓存的数据很多时,永不过期的方式弊大于利。

从业务层面来说,很多时候缓存的数据都是热门数据,比如说活动页,大促商品,会吸引大量请求,需要缓存缓解数据库压力,如果活动结束,大促结束,这些数据的请求急剧降低,无需在缓存中存在,永不过期就不是理想的方案。

  • 缓存一个空值

缓存失效时,可能是因为DB的数据已删除,为了保证一致性,缓存中的数据也会删除,此时如果大量查询进来,缓存中无数据,也会打到DB。

缓存一个空值对上述场景可减轻DB压力。

  • 加分布式锁

要保证只有一个请求查询DB,显而易见的一个方案就是加锁,对于查询DB的函数,加上分布式锁来控制查询,当有请求获取到锁时,其他请求只能轮询等待。

这样当然也有很大弊端,需要不断的释放锁获取锁,对锁进行整个生命周期的管理。另外加锁也会对查询的并发带来很大的降低。


3.single flight方式

single flight设计思想,Go语言开发者应该都很熟悉,譬如go-zero框架中的, core/syncx/singleflight.go

将并发请求合并成一个请求,以减少对下层服务的压力。

将single flight应用到缓存击穿场景上,基本思想就是:

确保在缓存失效后,只有一个线程去加载数据,其余线程等待该线程完成加载后直接使用其结果。


3.1 模拟业务场景

简单用业务代码模拟一个业务场景:

优先从缓存中查询数据,如果缓存不存在再查询DB,缓存设置一定的过期时间。

代码如下:

public class CacheExpired {

    private final static JedisPool jedisPool = new JedisPool("localhost", 6379);

    public static void main(String[] args) {
        //初始化缓存
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.psetex("key", 300, "value");
        }
        CacheExpired cacheExpired = new CacheExpired();
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        List<Future<String>> futures = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Future<String> result = executorService.submit(() -> {
                //先从缓存中获取
                String s = cacheExpired.loadFromCache();
                if (s != null) {
                    return s;
                }
                //缓存中无数据时,再从DB中获取。
                s = cacheExpired.loadFromDB();
                return s;
            });
            futures.add(result);
        }

        for (Future<String> future : futures) {
            try {
                System.out.println(future.get());
            } catch (InterruptedException | ExecutionException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public String loadFromDB() {
        try {
            //从db获取数据是个耗时的操作
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //更新缓存
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.psetex("key", 200, "value");
        }
        return Thread.currentThread().getName() + ":从db获取数据成功";
    }

    public String loadFromCache() {
        try (Jedis jedis = jedisPool.getResource()) {
            //模拟从缓存中获取数据
            Thread.sleep(100);
            String cachedValue = jedis.get("key");
            if (cachedValue != null) {
                return Thread.currentThread().getName() + ":从缓存获取数据成功";
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName() + ":缓存中无数据");
        return null;
    }
}

当缓存失效时,如上,高并发场景下,DB将承担所有的查询请求,会给DB带来巨大的压力,造成缓存击穿。


3.2 使用single flight的方式

这里讨论使用single flight的方式主要就是为了替换掉加锁的逻辑,需要保证以下两点:

1.只会有一个请求查询DB

2.其他请求需要获取第一个请求查询DB后的数据

总结起来就是等待计算

恰好JDK中包含有支持此逻辑的功能:Future

Future表示异步计算的结果,Future提供了多个方法用来校验执行计算的结果是否完成,并且等待计算的完成,在计算完成之前,会一直阻塞等待。

对于上面要保证的两点,可以使用Map + Future的方式来实现。

Map用来缓存第一次请求,Key是请求参数,Value为Future包装的异步计算的结果。

后续的请求根据Key获取到第一次请求查询封装的Future,然后通过Future.get(),获取第一次查询DB的结果。

如下String singleFlight()

  1. 请求进来时,判断Map中是否有相同的请求
  2. 如果没有包装成FutureTask放入Map中。
  3. 执行FutureTaskrun()方法。
  4. 如果其他请求此时进来,Map中已有相同请求在执行,其他请求会在Future.get()处阻塞等待第一次请求的结果。

为了更好的观测执行效果,我们可以将从redis中获取缓存的逻辑去掉,直接全部请求DB。

public class CacheExpired {

    //    private final static JedisPool jedisPool = new JedisPool("localhost", 6379);

    public static void main(String[] args) {
        //        //初始化缓存
        //        try (Jedis jedis = jedisPool.getResource()) {
        //            jedis.psetex("key", 300, "value");
        //        }
        CacheExpired cacheExpired = new CacheExpired();
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        List<Future<String>> futures = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Future<String> result = executorService.submit(() -> {
                //                //先从缓存中获取
                //                String s = cacheExpired.loadFromCache();
                //                if (s != null) {
                //                    return s;
                //                }
                //全部从DB中获取。
                String s = cacheExpired.singleFlight();
                return s;
            });
            futures.add(result);
        }

        for (Future<String> future : futures) {
            try {
                System.out.println(future.get());
            } catch (InterruptedException | ExecutionException e) {
                throw new RuntimeException(e);
            }
        }
    }

    //存储正在进行或者已完成的请求,如果多个请求同时进来,可保证只有一个请求回去查询DB
    private final ConcurrentHashMap<String, Future<String>> cache = new ConcurrentHashMap<>();

    public String singleFlight() throws Exception {
        while (true) {
            Future<String> future = cache.get("key");
            if (future == null) {
                Callable<String> callable = () -> {
                    loadFromDB();
                    return "执行完成";
                };
                FutureTask<String> futureTask = new FutureTask<>(callable);
                future = cache.putIfAbsent("key", futureTask);
                if (future == null) {
                    future = futureTask;
                    futureTask.run(); // 执行加载任务
                }
            }
            try {
                return future.get(); // 等待结果
            } catch (CancellationException e) {
                cache.remove("key", future);
                System.out.println(e);
            } catch (ExecutionException e) {
                throw new Exception(e.getCause());
            }
        }
    }

    public void loadFromDB() {
        try {
            //从db获取数据是个耗时的操作
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName() + ":从db获取数据成功");
    }
}

执行结果如下,可以看到只有第一次查询请求达到了DB。

当然上述方案也是有缺点的,比如Map中数据存储请求数据的时效,需不需要自动过期删除,Map本身不支持自动过期,需要根据业务需求来处理Map中缓存的数据。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/717594.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux网络编程:epoll

1.IO多路转接---epoll 1.1.接口认识 epoll多路转接的实现是基于三个系统调用的&#xff0c;而这些系统调用底层是epoll模型的构建&#xff0c;和设置的结构体、数据结构之间的交互&#xff0c;我们需要一步步地进行epoll的学习&#xff01; epoll_create( ) 如图&#xff1a;…

从短期到长期,不同保存期限的红酒管理技巧

在葡萄酒的世界中&#xff0c;保存与管理的技巧对于确保葡萄酒的品质和口感至关重要。特别是对于云仓酒庄雷盛红酒&#xff0c;这种多类型红酒&#xff0c;更需要我们掌握一定的保存管理技巧。这篇文章将详细探讨从短期到长期&#xff0c;不同保存期限的云仓酒庄雷盛红酒的管理…

如何高效使用大型语言模型 LLMs 初学者版本 简单易上手

第一条也是最重要的一条规则是 永远不要要求LLM提供你无法自己验证的信息, 或让它完成你无法验证其正确性的任务。 唯一例外的情况是那些无关紧要的任务&#xff0c; 例如&#xff0c;让大型语言模型提供公寓装修灵感之类的是可以的 。 首先请看两个范例 不佳示范&#xff1a…

上海交通大学、中科大 开源镜像站停止 Docker Hub 仓库镜像支持后的可用替代源

上海交通大学 Linux 用户组发布公告&#xff1a; 即时起中止对 Docker Hub 仓库的镜像。Docker 相关工具默认会自动处理失效镜像的回退&#xff0c;如果对官方源有访问困难问题&#xff0c;建议尝试使用其他仍在服务的镜像源。 源加速地址 有网友表示百度的 Docker Hub 加速器…

创新实训2024.06.17日志:大模型微调总结

前段时间其实我们已经部署了大模型&#xff0c;并开放了对外的web接口。不过由于之前某几轮微调实验的大模型在对话时会有异常表现&#xff08;例如响应难以被理解&#xff09;&#xff0c;因此我在项目上线后&#xff0c;监控了数据库里存储的对话记录。确定了最近一段时间部署…

微服务开发与实战Day10 - Redis面试篇

一、Redis主从集群 1. 搭建主从集群 1.1 主从集群结构 单节点Redis的并发能力是有限的&#xff0c;要进一步提高Redis的并发能力&#xff0c;就需要搭建主从集群&#xff0c;实现读写分离。 如图所示&#xff0c;集群中有一个master节点、两个slave节点&#xff08;现在叫re…

Vector | Graph:蚂蚁首个开源Graph RAG框架设计解读

作者&#xff1a;范志东 检索增强生成&#xff08;RAG&#xff1a;Retrieval Augmented Generation&#xff09;技术旨在把信息检索与大模型结合&#xff0c;以缓解大模型推理“幻觉”的问题。近来关于RAG的研究如火如荼&#xff0c;支持RAG的开源框架也层出不穷&#xff0c;并…

AI导航网

文章目录 1、[AI导航网](https://www.ainav.cn/) 1、AI导航网 https://www.ainav.cn/

GenICam标准(二)

系列文章目录 GenICam标准&#xff08;一&#xff09; GenICam标准&#xff08;二&#xff09; GenICam标准&#xff08;三&#xff09; GenICam标准&#xff08;四&#xff09; GenICam标准&#xff08;五&#xff09; GenICam标准&#xff08;六&#xff09; 文章目录 系列文…

【蜂窝物联】物联网智能控制器助力各种自动化控制领域科学管控

【蜂窝物联】4G远程温湿度传感器科学管理利器&#xff0c;应用无处不在 2024-06-17 14:09 发布于&#xff1a;福建省 随着信息化的不断推进&#xff0c;对各行各业都是一次现代化升级的契机&#xff0c;比如工厂的温湿度监测工作&#xff0c;完全可以由无线温湿度监控方案…

【Spine学习10】之 创建新骨骼时,自动绑定图片和插槽的快捷方式

两天没更新了。 遇到一些难解的难题 用的版本是破解版 不知道为啥现在的教程非常地快 明明有些细节很重要还略过讲 所以创建骨骼这里 基本创建是都会 可是骨骼一多 实际工作中的重命名也太麻烦了 。 这就需要学习快捷创建方式&#xff1a; <将对应图片自动绑定到新骨骼上并…

Vue55-TodoList案例-本地存储

一、TodoList案例-本地存储 此时&#xff0c;修改对象里面的属性&#xff0c;watch监视不到&#xff01; 需要深度监视&#xff0c;就不能用简写形式&#xff01; 二、jeecg-boot中的本地存储 jeecg-boot中&#xff0c;浏览器的本地存储&#xff0c;存储的是token&#xff01;…

TC3xx A\B SWAP机制的小细节(1)

目录 1.汽车OTA背景 1.1 汽车为什么需要OTA 1.2 汽车OTA概念 2. MCU的硬件A\B Swap机制 3.小结 1.汽车OTA背景 1.1 汽车为什么需要OTA 谈到英飞凌TC3xx的A\B SWAP硬件机制&#xff0c;我们首先要搞懂它的应用场景--OTA。 在手机或者电脑上&#xff0c;我们几乎每天都可…

JavaEE进阶----SpringBoot快速入门

文章目录 前言一、了解Maven1.1 Maven功能- 项⽬构建- 管理依赖 1.2Maven仓库 二、第一个SpringBoot项目总结 前言 Spring Boot是一个用于构建快速、简单和可扩展的生产级应用程序的框架。它基于Spring框架&#xff0c;提供了开发微服务和独立的应用程序所需的一切。 一、了解…

一、开发环境安装 Avalonia

1、概述 官网中是这么介绍Avalonia的&#xff0c;Avalonia是一个强大的框架&#xff0c;使开发人员能够使用.NET创建跨平台应用程序。它使用自己的渲染引擎绘制UI控件&#xff0c;确保在Windows、macOS、Linux、Android、iOS和WebAssembly等不同平台上具有一致的外观和行为。这…

2024年8款最受欢迎的开源看板系统

开源看板系统有哪些&#xff1f;本文将盘点国内外主流的8款看板系统&#xff1a;PingCode、Kanboard、Worktile、Wekan、OpenProject、TAIga、Focalboard。 今天想和大家探讨的是开源看板系统。作为一个热衷于项目管理和效率提升的爱好者&#xff0c;我在这方面也是小有研究。开…

基于DPU的云原生裸金属服务快速部署及存储解决方案

1. 背景介绍 1.1. 业务背景 在云原生技术迅速发展的当下&#xff0c;容器技术因其轻量级、可移植性和快速部署的特性而成为应用部署的主流选择&#xff0c;但裸金属服务器依然有其独特的价值和应用场景&#xff0c;是云原生架构中不可或缺的一部分。 裸金属服务器是一种高级…

抛光粉尘可爆性检测 打磨粉尘喷砂粉尘爆炸下限测试

抛光粉尘可爆性检测 抛光粉尘的可爆性检测是一种安全性能测试&#xff0c;用于确定加工过程中产生的粉尘在特定条件下是否会爆炸&#xff0c;从而对生产安全构成威胁。如果粉尘具有可爆性&#xff0c;那么在生产环境中就需要采取相应的防爆措施。粉尘爆炸的条件通常包括粉尘本身…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 字符串筛选排序(100分) - 三语言AC题解(Python/Java/Cpp)

&#x1f36d; 大家好这里是清隆学长 &#xff0c;一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 &#x1f4bb; ACM银牌&#x1f948;| 多次AK大厂笔试 &#xff5c; 编程一对一辅导 &#x1f44f; 感谢大家的订阅➕ 和 喜欢&#x1f497; &#x1f…

【深度学习】智能手写数字识别系统

文章目录 一&#xff0e;实验课题背景说明1.1实验目的1.2实验环境1.2.1安装PyTorch1.2.2安装其他必要的库 二&#xff0e;模型说明2.1模型概述2.2模型结构 三&#xff0e;数据说明3.1 输入数据3.1.1输入数据特征3.1.2输入数据维度3.1.3输入数据预处理 3.2 数据格式3.2.1输出数据…