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

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

一个线程在 sleep,另外两个线程执行到了 FutureTask 的 get 方法。

sleep 的好理解,为什么另外两个线程阻塞在 get 方法上呢?

很简单,因为另外两个线程返回的 future 不是 null,这是由 putIfAbsent 方法的特性决定的:

image.png

好了,书中给出的最终方案的代码也解释完了。

但是书里面还留下了两个“坑”:

image.png

一个是不支持缓存过期机制。

一个是不支持缓存淘汰机制。

等下再说,先说说我的另一个方案。


还有一个方案


其实我也还有一个方案,拿出来给大家看看:

public class ScoreQueryService2 {
    public static final Map<String, Future<Integer>> SCORE_CACHE = new ConcurrentHashMap<>();
    public Integer query(String userName) throws Exception {
        while (true) {
            Future<Integer> future = SCORE_CACHE.get(userName);
            if (future == null) {
                Callable<Integer> callable = () -> loadFormDB(userName);
                FutureTask futureTask = new FutureTask<>(callable);
                FutureTask<Integer> integerFuture = (FutureTask) SCORE_CACHE.computeIfAbsent(userName, key -> futureTask);
                future = integerFuture;
                integerFuture.run();
            }
            try {
                return future.get();
            } catch (CancellationException e) {
                SCORE_CACHE.remove(userName, future);
            } catch (Exception e) {
                throw e;
            }
        }
    }
    private Integer loadFormDB(String userName) throws InterruptedException {
        System.out.println("开始查询userName=" + userName + "的分数");
        //模拟耗时
        TimeUnit.SECONDS.sleep(1);
        return ThreadLocalRandom.current().nextInt(380, 420);
    }
}

和书中给出的方案差异点在于用 computeIfAbsent 代替了 putIfAbsent:

V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)

computeIfAbsent,首先它也是一个线程安全的方法,这个方法会检查 Map 中的 Key,如果发现 Key 不存在或者对应的值是 null,则调用 Function 来产生一个值,然后将其放入 Map,最后返回这个值;否则的话返回 Map 已经存在的值。

putIfAbsent,如果 Key 不存在或者对应的值是 null,则将 Value 设置进去,然后返回 null;否则只返回 Map 当中对应的值,而不做其他操作。

所以这二者的区别之一在于返回值上。

用了 computeIfAbsent 之后,每次返回的都是同一个 FutureTask,但是由于 FutureTask 的生命周期,或者说是状态扭转的存在,即使三个线程都调用了它的 run 方法,这个 FutureTask 也只会执行成功一次。

可以看一下,这个 run 方法的源码,一进来就是状态和当前操作线程的判断:

image.png

但是从程序实现的优雅角度来说,还是 putIfAbsent 方法更好。


坑怎么办?


前面不是说最终的方案有两个坑嘛:

  • 一个是不支持缓存过期机制。
  • 一个是不支持缓存淘汰机制。

在使用 ConcurrentHashMap 的前提下,这两个特性如果要支持的话,需要进行对应的开发,比如引入定时任务来解决,想想就觉得麻烦。

同时也我想到了 spring-cache,我知道这里面有 ConcurrentHashMap 作为缓存的实现方案。

我想看看这个组件里面是怎么解决这两个问题的。

二话不说,我先把代码拉下来看看:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

由于 spring-cache 也不是本文重点,所以我就直接说关键地方的源码了。

至于是怎么找到这里来的,就不详细介绍了,以后安排文章详细解释。

另外我不得不说一句:spring-cache 这玩意真的是优雅的一比,不论是源码还是设计模式的应用,都非常的好。

image.png

首先,我们可以看到 @Cacheable 注解里面有一个参数叫做 sycn,默认值是 false:

image.png

关于这个参数,官网上的解释是这样的:

https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache-annotations-cacheable-cache-resolver


image.png

就是针对我们前面提到的缓存如何维护的情况的一个处理方案。使用方法也很简单。

该功能对应的核心部分的源码在这个位置:

org.springframework.cache.interceptor.CacheAspectSupport#execute(org.springframework.cache.interceptor.CacheOperationInvoker, java.lang.reflect.Method, org.springframework.cache.interceptor.CacheAspectSupport.CacheOperationContexts)


image.png

在上面这个方法中会判断是不是 sync=true 的方法,如果是则进入到 if 分支里面。

接着会执行到下面这个重要的方法:

org.springframework.cache.interceptor.CacheAspectSupport#handleSynchronizedGet


image.png

而我关心的是 ConcurrentMapCache 实现,点进去一看,好家伙,这方法我熟啊:

org.springframework.cache.concurrent.ConcurrentMapCache#get


image.png

computeIfAbsent 方法,我们不是刚刚才说了嘛。但是我左翻右翻就是找不到设置过期时间和淘汰策略的地方。

于是,我又去翻官网了,发现答案就直接写在官网上的:

https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache-specific-config


image.png

这里说了,官方提供的是一个缓存的抽象,而不是具体的实现。而缓存过期和淘汰机制不属于抽象的范围内。

为什么呢?

比如拿 ConcurrentHashMap 来说,假设我提供了缓存过期和淘汰机制的抽象,那你说 ConcurrentHashMap 怎么去实现这个抽象方法?

实现不了,因为它本来就不支持这个机制。

所以官方认为这样的功能应该由具体的缓存实现类去实现而不是提供抽象方法。

这里也就回复了前面的最终方案引申出的这两个问题:

  • 一个是不支持缓存过期机制。
  • 一个是不支持缓存淘汰机制。

別问,问就是原生的方法里面是支持不了的。如果要实现自己去写代码,或者换一个缓存方案。


再说两个点


最后,再补充两个点。

第一个点是之前的《当Synchronized遇到这玩意儿,有个大坑,要注意!》这篇文章里面,有一个地方写错了。

image.png

框起来的地方是我后面加上来的。

image.jpeg

上周的文章发出去后,大概有十来个读者给我反馈这个问题。

我真的特别的开心,因为真的有人把我的示例代码拿去跑了,且认真思考了,然后来和我讨论,帮我指正我写的不对的地方。

再给大家分享一下我的这篇文章《当我看技术文章的时候,我在想什么?》

里面表达了我对于看技术博客的态度:

看技术文章的时候多想一步,有时候会有更加深刻的理解。

带着怀疑的眼光去看博客,带着求证的想法去证伪。

多想想 why,总是会有收获的。

第二个点是这样的。

关于 ConcurrentHashMap 的 computeIfAbsent 我其实也专门写过文章的:《震惊!ConcurrentHashMap里面也有死循环,作者留下的“彩蛋”了解一下?》

老读者应该是读到过这篇文章的。

之前在 seata 官网上闲逛的时候,看到了这篇博客:

https://seata.io/zh-cn/blog/seata-dsproxy-deadlock.html

名字叫做《ConcurrentHashMap导致的Seata死锁问题》,我就随便这么点进去一看:

image.png

这里提到的这篇文章,就是我写的。

在 seata 官网上偶遇自己的文章是一种很神奇的体验。

四舍五入,我也算是给 seata 有过贡献的男人。

而且你看这篇文章其实也提到了我之前写过的很多文章,这些知识都通过一个小小的点串起来了,由点到线,由线到面,这也是我坚持写作的原因。

共勉之。

最后,呼应一下文章的开头部分,考研马上要查分了,我知道我的读者里面还是有不少是今年考研的。

如果你看到了这里,那么下面这个图送给你:

image.png

本文已收录至个人博客,更多原创好文,欢迎大家来玩:

https://www.whywhy.vip/

目录
相关文章
|
29天前
|
缓存 NoSQL 数据库
运用云数据库 Tair 构建缓存为应用提速,完成任务得苹果音响、充电套装等好礼!
本活动将带大家了解云数据库 Tair(兼容 Redis),通过体验构建缓存以提速应用,完成任务,即可领取罗马仕安卓充电套装,限量1000个,先到先得。邀请好友共同参与活动,还可赢取苹果 HomePod mini、小米蓝牙耳机等精美好礼!
|
1月前
|
存储 缓存 前端开发
利用 Webpack 5 的持久化缓存来提高构建效率
【10月更文挑战第23天】利用 Webpack 5 的持久化缓存是提高构建效率的有效手段。通过合理的配置和管理,我们可以充分发挥缓存的优势,为项目的构建和开发带来更大的便利和效率提升。你可以根据项目的实际情况,结合以上步骤和方法,进一步优化和完善利用持久化缓存的策略,以达到最佳的构建效果。同时,不断探索和实践新的方法和技术,以适应不断变化的前端开发环境和需求。
|
2月前
|
存储 缓存 NoSQL
构建高性能Web应用:缓存的重要性及其实现
构建高性能Web应用:缓存的重要性及其实现
|
4月前
|
Java 开发者 JavaScript
Struts 2 开发者的秘籍:隐藏的表单标签库功能,能否成为你下个项目的大杀器?
【8月更文挑战第31天】Struts 2表单标签库是提升Web页面交互体验的神器。它提供丰富的标签,如`&lt;s:textfield&gt;`和`&lt;s:select&gt;`,简化表单元素创建与管理,支持数据验证和动态选项展示。结合示例代码,如创建文本输入框并与Action类属性绑定,显著提升开发效率和用户体验。通过自定义按钮样式等功能,Struts 2表单标签库让开发者更专注于业务逻辑实现。
52 0
|
4月前
|
缓存 NoSQL 数据库
【超实用秘籍】FastAPI高手教你如何通过最佳实践构建高效Web应用:从代码组织到异步编程与缓存优化的全方位指南!
【8月更文挑战第31天】FastAPI凭借出色性能和易用性成为现代Web应用的首选框架。本文通过示例代码介绍构建高效FastAPI应用的最佳实践,包括开发环境搭建、代码模块化组织、异步编程及性能优化等。通过模块化设计和异步数据库操作,结合缓存技术,大幅提升应用性能与可维护性,助您轻松应对高并发场景。
280 0
|
5月前
|
敏捷开发 缓存 测试技术
阿里云云效产品使用合集之在自建构建机中,如何不使用缓存进行构建
云效作为一款全面覆盖研发全生命周期管理的云端效能平台,致力于帮助企业实现高效协同、敏捷研发和持续交付。本合集收集整理了用户在使用云效过程中遇到的常见问题,问题涉及项目创建与管理、需求规划与迭代、代码托管与版本控制、自动化测试、持续集成与发布等方面。
|
5月前
|
缓存 NoSQL Java
使用Java构建高效的分布式缓存系统
使用Java构建高效的分布式缓存系统
|
2月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
78 6
|
26天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
27天前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构