基于single flight来解决缓存击穿

news2024/12/25 9:11:59

目录

    • 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中缓存的数据。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024年设计、数字化技术与新闻传播国际学术会议(ICDDTJ 2024)

2024年设计、数字化技术与新闻传播国际学术会议(ICDDTJ 2024) 2024 International Conference on Design, Digital Technology and Journalism 会议地点&#xff1a;哈尔滨&#xff0c;中国 网址&#xff1a;www.icddtj.com 邮箱: icddtjsub-conf.com 投稿主题请注明:ICDD…

Vue46-render函数

一、非单文件和单文件的main.js对比 1-1、非单文件的main.js 1-2、 单文件的main.js 将单文件的main.js中的render函数变成非单文件的main.js中的template形式&#xff0c;报如下错误&#xff1a; 解决方式&#xff1a; 二、解决方式 2-1、引入完成版的vue.js 精简版的vue&a…

Elixir学习笔记——Erlang 库

Elixir 提供了与 Erlang 库的出色互操作性。事实上&#xff0c;Elixir 不鼓励简单地包装 Erlang 库&#xff0c;而是直接与 Erlang 代码交互。在本节中&#xff0c;我们将介绍一些 Elixir 中没有的最常见和最有用的 Erlang 功能。 Erlang 模块的命名约定与 Elixir 不同&#x…

Windows电脑部署Jellyfin服务端并进行远程访问配置详细教程

文章目录 前言1. Jellyfin服务网站搭建1.1 Jellyfin下载和安装1.2 Jellyfin网页测试 2.本地网页发布2.1 cpolar的安装和注册2.2 Cpolar云端设置2.3 Cpolar本地设置 3.公网访问测试4. 结语 前言 本文主要分享如何使用Windows电脑本地部署Jellyfin影音服务并结合cpolar内网穿透工…

建筑效果图为啥要用渲染100?渲染100邀请码1a12

建筑效果图是建筑设计师向客户展示方案的重要手段&#xff0c;通常在完成建模和材质贴图后&#xff0c;设计师会把它通过本地电脑渲染出来&#xff0c;不过本地渲染效率低&#xff0c;时间长&#xff0c;所以很多时候设计师也会使用网渲平台&#xff0c;今天我们介绍的渲染100就…

onnx基本概念

onnx基本概念 参考 文章目录 onnx基本概念Input, Output, Node, Initializer, AttributesSerialization with protobuf元数据List of available operators and domains支持的类型Opset版本Subgraphs, tests and loopsExtensibilityFunctionsShape (and Type) Inferencetools O…

元宇宙三维虚拟场景制作平台为数字化营销发展注入了新的活力

​在数字化浪潮的推动下&#xff0c;我们迎来了全新的3D元宇宙场景在线制作编辑器&#xff0c;为您带来前所未有的创作体验。这款轻量级实时创作工具&#xff0c;让您轻松构建丰富的3D元宇宙场景&#xff0c;实现全网全终端的展示。 3D元宇宙场景在线制作编辑器拥有海量的3D模…

Go 并发控制:RWMutex 实战指南

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

HTML5新标签

HTML5 特点 新标签 <header>...<header> 头部标签 <footer>...<footer> 尾部标签 <section>...<section> 内容区块 <article>...&#xff0c;article> 表示页面中的独立内容 <aside>...<aside> 标签定义其所处…

掌控未来:用决策树算法揭秘胜利者的必胜策略!

掌控未来&#xff1a;用决策树算法揭秘胜利者的必胜策略&#xff01; 一、引言1.1. 决策树的定义1.2. 发展历程1.3. 当前应用概况1.4. 本文内容安排 二、决策树的基本概念2.1 节点和叶节点2.2 决策树的结构结构图示不同结构的决策树 三、决策树的算法原理3.1 基本思想3.2 核心算…

使用 Nstbrowser 管理多个帐户 - 2024 年最佳反检测浏览器

每个人一定都看过那些房间里全是窃听器的老间谍电影&#xff0c;对吧&#xff1f;现在这些电影可能看起来有点好笑&#xff0c;但互联网并没有好到哪里去&#xff01; 事实上&#xff0c;每个你打开的页面在你浏览时都在被监控&#xff01;此外&#xff0c;当你管理多个账户时…

Web应用安全测试-防护功能缺失

Web应用安全测试-防护功能缺失 1、Cookie属性问题 漏洞描述&#xff1a; Cookie属性缺乏相关的安全属性&#xff0c;如Secure属性、HttpOnly属性、Domain属性、Path属性、Expires属性等。 测试方法&#xff1a; 通过用web扫描工具进行对网站的扫描&#xff0c;如果存在相关…

成都某展厅2套2x2透明OLED拼接屏项目

成都某展厅的2套2x2透明OLED拼接屏展示设计具有独特的技术魅力和视觉效果。以下是关于这一展示设计的详细介绍&#xff1a; 1.产品规格 类型&#xff1a;透明OLED拼接屏 尺寸与配置&#xff1a;每套为2x2拼接&#xff0c;即每套由4块屏幕组成。 2.应用场景 成都某展厅&#…

实战 | 基于YOLOv10的车辆追踪与测速实战【附源码+步骤详解】

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

linux部署运维3——centos7.9离线安装部署配置涛思taos2.6时序数据库TDengine以及java项目链接问题处理(二)

上一篇讲了centos7.9如何安装涛思taos2.6时序数据库的操作步骤和方案&#xff0c;本篇主要讲解taos数据库的初始化&#xff0c;相关配置说明&#xff0c;数据库和表的创建问题以及java项目连接问题。 centos7.9如何离线安装taos2.6&#xff0c;请点击下方链接详细查看&#xf…

zotero style最新(可全文翻译)

问题&#xff1a;在下载zotero style的时候&#xff0c;总会出现各种奇奇怪怪的问题&#xff0c;不是期刊没有级别&#xff0c;就是没有IF之类的&#xff1b; 解决&#xff1a;https://github.com/MuiseDestiny/zotero-style/releases 在这里下载最新的版本 若要使用全文翻译…

【IPython使用技巧整理】内省功能历史命令执行Shell命令运行脚本导出为其他格式

本人详解 作者:王文峰,参加过 CSDN 2020年度博客之星,《Java王大师王天师》 公众号:JAVA开发王大师,专注于天道酬勤的 Java 开发问题中国国学、传统文化和代码爱好者的程序人生,期待你的关注和支持!本人外号:神秘小峯 山峯 转载说明:务必注明来源(注明:作者:王文峰…

最佳Google Chrome扩展和Mozilla Firefox扩展自动解决验证码

在这个信息爆炸的时代&#xff0c;我们每天都要处理大量的在线内容&#xff0c;验证码已成为不可避免的挑战。尽管它们旨在保护网站安全&#xff0c;但也常常成为我们获取信息的障碍。那么&#xff0c;有没有更简单的方法绕过这些验证码呢&#xff1f;答案是肯定的。通过使用一…

本地安装nightingale监控分析服务并发布公网详细流程

文章目录 前言1. Linux 部署Nightingale2. 本地访问测试3. Linux 安装cpolar4. 配置Nightingale公网访问地址5. 公网远程访问Nightingale管理界面6. 固定Nightingale公网地址 前言 本文主要介绍如何在本地Linux系统部署 Nightingale 夜莺监控并结合cpolar内网穿透工具实现远程…

Linux——ansible里的变量

在ansible里&#xff0c;变量干嘛用的 本身&#xff0c;ansible就是致力于&#xff0c;用尽可能“通用”的剧本&#xff0c;干所有场合的工作…… ansible里的变量怎么写 字母开头&#xff0c;包括&#xff1a;字母数字下划线 变量怎么定义&#xff08;声明&#xff09; 1.…