构建高效且可伸缩的结果缓存 (上)

简介: 构建高效且可伸缩的结果缓存 (上)

上周发布了《当Synchronized遇到这玩意儿,有个大坑,要注意!》这篇文章。

文章的最后,我提到了《Java并发编程实战》的第 5.6 节的内容,说大家可以去看看。

我不知道有几个同学去看了,但是我知道绝大部分同学都没去看的,所以这篇文章我也给大家安排一下,怎么去比较好的实现一个缓存功能。

感受一下大师的代码方案演进的过程。


需求


这不都二月中旬了嘛,马上就要出考研成绩了,我就拿这个来举个例子吧。

需求很简单:从缓存中查询,查不到则从数据库获取,并放到缓存中去,供下次使用。

核心代码大概就是这样的:

Integer score = map.get("why");
if(score == null){
   score = loadFormDB("why");
   map.put("why",score);
}

有了核心代码,所以我把代码补全之后应该是这样的:

public class ScoreQueryService {
    private final Map<String, Integer> SCORE_CACHE = new HashMap<>();
    public Integer query(String userName) throws InterruptedException {
        Integer result = SCORE_CACHE.get(userName);
        if (result == null) {
            result = loadFormDB(userName);
            SCORE_CACHE.put(userName, result);
        }
        return result;
    }
    private Integer loadFormDB(String userName) throws InterruptedException {
        System.out.println("开始查询userName=" + userName + "的分数");
        //模拟耗时
        TimeUnit.SECONDS.sleep(1);
        return ThreadLocalRandom.current().nextInt(380, 420);
    }
}

然后搞一个 main 方法测试一下:

public class MainTest {
    public static void main(String[] args) throws InterruptedException {
        ScoreQueryService scoreQueryService = new ScoreQueryService();
        Integer whyScore = scoreQueryService.query("why");
        System.out.println("whyScore = " + whyScore);
        whyScore = scoreQueryService.query("why");
        System.out.println("whyScore = " + whyScore);
    }
}

把代码儿跑起来:

image.png

好家伙,第一把就跑了个 408 分,我考研要是真能考到这个分数,怕是做梦都得笑醒。


Demo 很简单,但是请你注意,我要开始变形了。

首先把 main 方法修改为这样:

public class MainTest {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        ScoreQueryService scoreQueryService = new ScoreQueryService();
        for (int i = 0; i < 3; i++) {
            executorService.execute(()->{
                try {
                    Integer why = scoreQueryService.query("why");
                    System.out.println("why = " + why);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

利用线程池提交任务,模拟同一时间发起三次查询请求,由于 loadFormDB 方法里面有模拟耗时的操作,那么这三个请求都不会从缓存中获取到数据。

具体是不是这样的呢?

看一下运行结果:

image.png

输出三次,得到了三个不同的分数,说明确实执行了三次 loadFormDB 方法。

好,同学们,那么问题就来了。

很明显,在这个场景下,我只想要一个线程执行 loadFormDB 方法就行了,那么应该怎么操作呢?

看到这个问题的瞬间,不知道你的脑袋里面有没有电光火石般的想起缓存问题三连击:缓存雪崩、缓存击穿、缓存穿透。

毕竟应对缓存击穿的解决方案之一就是只需要一个请求线程去做构建缓存,其他的线程就轮询等着。

然后脑海里面自然而然的就浮现出了 Redis 分布式锁的解决方案,甚至还想到了应该用 setNX 命令来保证只有一个线程放行成功。嘴角漏出一丝不易察觉的笑容,甚至想要关闭这篇文章。

不好意思,收起你的笑容,不能用 Redis,不能用第三方组件,只能用 JDK 的东西。

image.png

别问为什么,问就是没有引入。

这个时候你怎么办?


初始方案


听说不能用第三方组件之后,你也一点不慌,大喊一声:键来。

image.png

image.png

加上一个 synchronized 关键字就算完事,甚至你还记得程序员的自我修养,完成了一波自测,发现确实没有问题:

image.png

loadFromDB 方法只执行了一次。

但是,朋友,你有没有想过你这个锁的粒度有点太大了啊。

直接把整个方法给锁了。

本来一个好好的并行方法,你给咔一下,搞成串行的了:

image.png

而且你这是无差别乱杀啊,比如上面这个示意图,你要是说当第二次查询 why 的成绩的时候,把这个请求给拦下来,可以理解。

但是你同时也把第一次查询 mx 的成绩给拦截了。弄得 mx 同学一脸懵逼,搞不清啥情况。

注意,这个时候自然而然就会想到缩小锁的粒度,把锁的范围从全局修改为局部,拿出比如用 why 对象作为锁的这一类解决方案。

比如伪代码改成这样:

Integer score = map.get("why");
if(score == null){
   synchronized("why"){
       score = loadFormDB("why");
       map.put("why",score);
   }    
}

如果到这里你还没反应过来,那么我再换个例子。

假设我这里的查询条件变 Integer 类型的编号呢?

比如我的编号是 200,是不是伪代码就变成了这样:

Integer score = map.get(200);
if(score == null){
   synchronized(200){
       score = loadFormDB(200);
       map.put(200,score);
   }    
}

看到这里你要是还没反应过来的话我只能大喊一声:你个假读者!之前发的文章肯定没看吧?

之前的《当Synchronized遇到这玩意儿,有个大坑,要注意!》这篇文章不全篇都在说这个事儿吗?

你要不知道问题是啥,你就去翻一下。

这篇文章肯定也不会往这个方向去写。不能死盯着 synchronize 不放,不然思路打不开。

image.png

我们这里不能用 synchronized 这个玩意。

但是你仔细一看,如果不用 synchronized 的话,这个 map 也不行啊:

private final Map<String, Integer> SCORE_CACHE = new HashMap<>();

这是个 HashMap,不是线程安全的呀。

怎么办?

演进呗。


演进一下


image.png

这一步非常简单,和最开始的程序相比,只是把 HashMap 替换为 ConcurrentHashMap。

然后就啥也没干了。

是不是感觉有点懵逼,甚至感觉有一定被耍了的感觉?

有就对了,因为这一步改变就是书里面的一个方案,我第一次看到的时候反正也是感觉有点懵逼:

image.png

image.png

image.png

image.png

因为根本就不能满足“相同的请求下,如果缓存中没有,只有一个请求线程执行 loadFormDB 方法”这个需求,比如 why 的短时间内的两次查询操作就执行两次 loadFormDB 方法。

它的毛病在哪儿呢?

如果多个线程都是查 why 这个人成绩的前提下,如果一个线程去执行 loadFormDB 方法了,而另外的线程根本感知不到有线程在执行该方法,那么它们冲进来后一看:我去,缓存里面压根没有啊?那我也去执行 loadFormDB 方法吧。

完犊子了,重复执行了。

那么在 JDK 原生的方法里面有没有一种机制来表示已经有一个请求查询 why 成绩的线程在执行 loadFormDB 方法了,那么其他的查询 why 成绩的线程就等这个结果就行了,没必要自己也去执行一遍。

这个时候就考验你的知识储备了。

你想到了什么?


image.png


目录
相关文章
|
17天前
|
存储 缓存 NoSQL
构建高性能Web应用:缓存的重要性及其实现
构建高性能Web应用:缓存的重要性及其实现
|
2月前
|
Java 开发者 JavaScript
Struts 2 开发者的秘籍:隐藏的表单标签库功能,能否成为你下个项目的大杀器?
【8月更文挑战第31天】Struts 2表单标签库是提升Web页面交互体验的神器。它提供丰富的标签,如`&lt;s:textfield&gt;`和`&lt;s:select&gt;`,简化表单元素创建与管理,支持数据验证和动态选项展示。结合示例代码,如创建文本输入框并与Action类属性绑定,显著提升开发效率和用户体验。通过自定义按钮样式等功能,Struts 2表单标签库让开发者更专注于业务逻辑实现。
45 0
|
2月前
|
缓存 NoSQL 数据库
【超实用秘籍】FastAPI高手教你如何通过最佳实践构建高效Web应用:从代码组织到异步编程与缓存优化的全方位指南!
【8月更文挑战第31天】FastAPI凭借出色性能和易用性成为现代Web应用的首选框架。本文通过示例代码介绍构建高效FastAPI应用的最佳实践,包括开发环境搭建、代码模块化组织、异步编程及性能优化等。通过模块化设计和异步数据库操作,结合缓存技术,大幅提升应用性能与可维护性,助您轻松应对高并发场景。
108 0
|
4月前
|
敏捷开发 缓存 测试技术
阿里云云效产品使用问题之构建Vue3项目,怎么让node_modules缓存下来
云效作为一款全面覆盖研发全生命周期管理的云端效能平台,致力于帮助企业实现高效协同、敏捷研发和持续交付。本合集收集整理了用户在使用云效过程中遇到的常见问题,问题涉及项目创建与管理、需求规划与迭代、代码托管与版本控制、自动化测试、持续集成与发布等方面。
|
3月前
|
敏捷开发 缓存 测试技术
阿里云云效产品使用合集之在自建构建机中,如何不使用缓存进行构建
云效作为一款全面覆盖研发全生命周期管理的云端效能平台,致力于帮助企业实现高效协同、敏捷研发和持续交付。本合集收集整理了用户在使用云效过程中遇到的常见问题,问题涉及项目创建与管理、需求规划与迭代、代码托管与版本控制、自动化测试、持续集成与发布等方面。
|
3月前
|
缓存 NoSQL Java
使用Java构建高效的分布式缓存系统
使用Java构建高效的分布式缓存系统
|
3月前
|
存储 缓存 NoSQL
使用Java构建高性能的分布式缓存系统
使用Java构建高性能的分布式缓存系统
|
17天前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
53 1
|
17天前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
33 2
数据的存储--Redis缓存存储(二)
|
13天前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
50 6