通过READ-BEHIND CACHE来控制缓慢的生产者
在我们的互联世界中,我们经常使用我们不拥有或无权改善的API中的数据。 如果一切顺利,他们的表现就会很好,每个人都会感到高兴。 但是太多次,我们不得不使用延迟小于最佳延迟的 API。
当然,答案是缓存该数据 。 但是,您不知道何时过时的缓存是很危险的事情,因此这不是一个适当的解决方案。
因此,我们陷入困境。 我们需要习惯于等待页面加载,或者投资一个非常好的微调器来招待用户等待数据。 还是……是吗? 如果为一个较小的,经过计算的折衷而又使用相同的缓慢生成器可以达到期望的性能,该怎么办?
我想每个人都听说过后写式缓存。 它是高速缓存的一种实现,该高速缓存注册了将异步发生的写操作,在对后台任务执行写操作的同时,调用者可以自由地继续其业务。
如果我们将这个想法用于问题的阅读方面该怎么办。 让我们为慢速生产者提供一个后置缓存 。
合理警告 :此技术仅适用于我们可以在有限数量的请求中提供过时的数据。 因此,如果您可以接受您的数据将是“ 最终新鲜的 ”,则可以应用此数据。
我将使用Spring Boot来构建我的应用程序。 可以在GitHub上访问所有提供的代码: https : //github.com/bulzanstefan/read-behind-presentation 。 在实施的不同阶段有3个分支。
代码示例仅包含相关的行,以简化操作。
现状
分支机构:现状
因此,我们将从现状开始。 首先,我们有一个缓慢的生产者,它接收URL参数。 为了简化此过程,我们的生产者将睡眠5秒钟,然后返回一个时间戳(当然,这不是低变化数据的一个很好的示例,但是出于我们的目的,尽快检测到数据是有用的) 。
public static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat( "HH:mm:ss.SSS" ); @GetMapping String produce(@RequestParam String name) throws InterruptedException { Thread. sleep (5000); return name + " : " + SIMPLE_DATE_FORMAT. format (new Date()); }
在消费者中,我们只是致电生产者:
//ConsumerController .java @GetMapping public String consume(@RequestParam(required = false ) String name) { return producerClient.performRequest(ofNullable(name).orElse( "default" )); } //ProducerClient .java @Component class ProducerClient { public String performRequest(String name) { return new RestTemplate().getForEntity( " http://localhost:8888/producer?name= {name}" , String.class, name) .getBody(); } }
简单缓存
分支:简单缓存
为了在Spring启用简单的缓存 ,我们需要添加以下内容
- 依赖
org.springframework.boot:spring-boot-starter-cache
- 在application.properties中启用缓存:
spring.cache.type= simple
- 将
@EnableCaching
注解添加到您的Spring Application主类 - 将
@Cacheable("cacheName")
添加到要缓存的方法中
现在我们有一个简单的缓存表示。 这也适用于分布式缓存 ,但是在此示例中,我们将坚持使用内存中的缓存。 使用者将缓存数据,并且在第一次调用后,等待时间消失了。 但是数据很快就会过时 ,没有人将其逐出。 我们可以做得更好!
接听电话
分行:硕士
我们需要做的下一件事是在发生呼叫时对其进行拦截,而不管是否将其缓存。
为了做到这一点,我们需要
- 创建一个自定义注释:
@ReadBehind
- 注册一个方面,该方面将拦截以
@ReadBehind
注释的方法调用
因此,我们创建了注释并将其添加到performRequest
方法
@ReadBehind @Cacheable(value = CACHE_NAME, keyGenerator = "myKeyGenerator" ) public String performRequest(String name) {
如您所见,定义了一个CACHE_NAME常量。 如果需要动态设置缓存名称,则可以使用CacheResolver和配置。 同样,为了控制密钥结构,我们需要定义一个密钥生成器。
@Bean KeyGenerator myKeyGenerator() { return (target, method, params) -> Stream.of(params) .map(String::valueOf) .collect(joining( "-" )); }
此外,为了添加方面,我们需要
- 将依赖项添加到
org.springframework.boot:spring-boot-starter-aop
- 创建方面类
- 我们需要实现Ordered接口并为getOrder方法返回1。 即使在值已经存在于高速缓存中时高速缓存机制将抑制方法的调用,方面也需要启动
@Aspect @Component public class ReadBehindAdvice implements Ordered { @Before( "@annotation(ReadBehind)" ) public Object cacheInvocation(JoinPoint joinPoint) { ... @Override public int getOrder() { return 1; }
现在,我们可以拦截所有对@ReadBehind方法的调用。
记住电话
现在有了调用,我们需要保存所有需要的数据,以便能够从另一个线程调用它。
为此,我们需要保留:
- 被称为豆
- 参数调用
- 方法名称
@Before( "@annotation(ReadBehind)" ) public Object cacheInvocation(JoinPoint joinPoint) { invocations.addInvocation(new CachedInvocation(joinPoint)); return null; }
public CachedInvocation(JoinPoint joinPoint) { targetBean = joinPoint.getTarget(); arguments = joinPoint.getArgs(); targetMethodName = joinPoint.getSignature().getName(); }
我们将这些对象保留在另一个bean中
@Component public class CachedInvocations { private final Set<CachedInvocation> invocations = synchronizedSet(new HashSet<>()); public void addInvocation(CachedInvocation invocation) { invocations.add(invocation); } }
我们将调用保持在一个集合中,并且我们有一个计划的工作以固定的速率处理这些调用,这一事实将给我们带来一个很好的副作用,即限制了对外部API的调用。
安排落后的工作
现在我们知道执行了哪些调用,我们可以开始计划的作业以接听这些调用并刷新缓存中的数据
为了在Spring Framework中安排工作,我们需要
- 在您的Spring应用程序类中添加注释
@EnableScheduling
- 使用
@Scheduled
注释的方法创建作业类
@Component @RequiredArgsConstructor public class ReadBehindJob { private final CachedInvocations invocations; @Scheduled(fixedDelay = 10000) public void job() { invocations.nextInvocations() .forEach(this::refreshInvocation); } }
刷新缓存
现在我们已经收集了所有信息,我们可以对后读线程进行真正的调用并更新缓存中的信息。
首先,我们需要调用real方法 :
private Object execute(CachedInvocation invocation) { final MethodInvoker invoker = new MethodInvoker(); invoker.setTargetObject(invocation.getTargetBean()); invoker.setArguments(invocation.getArguments()); invoker.setTargetMethod(invocation.getTargetMethodName()); try { invoker.prepare(); return invoker.invoke(); } catch (Exception e) { log.error( "Error when trying to reload the cache entries " , e); return null; } }
现在我们有了新数据,我们需要更新缓存
首先, 计算 缓存密钥 。 为此,我们需要使用为缓存定义的密钥生成器。
现在,我们拥有所有信息来更新缓存,让我们获取缓存参考并更新值
private final CacheManager cacheManager; ... private void refreshForInvocation(CachedInvocation invocation) { var result = execute(invocation); if (result != null) { var cacheKey = keyGenerator.generate(invocation.getTargetBean(), invocation.getTargetMethod(), invocation.getArguments()); var cache = cacheManager.getCache(CACHE_NAME); cache.put(cacheKey, result); } }
至此,我们完成了“隐藏式”想法的实施。 当然,您仍然需要解决其他问题。
例如,您可以执行此实现并立即在线程上触发调用。 这样可以确保在第一时间刷新缓存。 如果过时的时间是您的主要问题,则应该这样做。
我喜欢调度程序,因为它也可以作为一种限制机制 。 因此,如果您一遍又一遍地进行相同的呼叫,则后读调度程序会将这些呼叫折叠为一个呼叫
运行示例代码
- 先决条件:已安装Java 11+
- 下载或克隆代码https://github.com/bulzanstefan/read-behind-presentation
- 构建生产者:
mvnw package or mvnw.bat package
- 运行生产者:
java -jar target\producer.jar
- 构建使用者:
mvnw package or mvnw.bat package
- 运行使用者:
java -jar target\consumer.jar
- 访问生产者: http:// localhost:8888 / producer?name = test
- 访问使用者: http:// localhost:8080 / consumer?name = abc
- 使用者将在约15秒后(10秒调度程序,5 –新请求)返回更新后的值,但在首次呼叫后不应看到任何延迟 。
警告
就像我在本文开头所说的那样,在实现read-behind时您应该注意一些事情。
另外,如果您负担不起最终的一致性 ,请不要这样做
这适用于具有 低频变化 API的高频读取
如果API实现了某种ACL ,则需要在缓存键中添加用于发出请求的用户名。 否则,可能会发生非常糟糕的事情。
因此,请仔细分析您的应用程序,并仅在适当的地方使用此想法。
翻译自: https://www.javacodegeeks.com/2019/12/take-control-your-slow-producers-read-behind-cache.html
通过READ-BEHIND CACHE来控制缓慢的生产者相关推荐
- HttpContext.Current.Cache在控制台下不工作
说明: Cache 类不能在 ASP.NET 应用程序外使用.它是为在 ASP.NET 中用于为 Web 应用程序提供缓存而设计和测试的.在其他类型的应用程序(如控制台应用程序或 Windows 窗体 ...
- 白话Elasticsearch52-深入聚合数据分析之fielddata内存控制、circuit breaker短路器、fielddata filter、预加载机制以及序号标记预加载
文章目录 概述 官网 fielddata核心原理 fielddata内存限制 监控fielddata内存使用 circuit breaker fielddata filter的细粒度内存加载控制 fi ...
- 计算机缓存Cache以及Cache Line详解
转载: 计算机缓存Cache以及Cache Line详解 - 围城的文章 - 知乎 https://zhuanlan.zhihu.com/p/37749443 L1,L2,L3 Cache究竟在哪里? ...
- ARM存储器之:高速缓冲存储器Cache
当第一代RISC微处理器刚出现时,标准存储器元件的速度比当时微处理器的速度快.很快,半导体工艺技术的进展被用来提高微处理器的速度.标准DRAM部件虽然也快了一些,但其发展的主要精力则放在提高存储容量上 ...
- QT学习:多线程控制
实现线程的互斥与同步常使用的类有QMutex.QMutexLocker.QReadWriteLocker.QReadLocker. QWriteLocker.QSemaphore和QWaitCondi ...
- ElasticSearch 2 (37) - 信息聚合系列之内存与延时
ElasticSearch 2 (37) - 信息聚合系列之内存与延时 摘要 控制内存使用与延时 版本 elasticsearch版本: elasticsearch-2.x 内容 Fielddata ...
- Spring Boot 3.0.0-M1 Reference Documentation(Spring Boot中文参考文档)-附录A-C
附录 附录A:常用的应用程序属性 多种属性可以指定到application.properties文件,application.yml文件内,或者作为命令行开关.这个附录提供常用的Spring Boot ...
- Ceph性能测试、优化及硬件选型详解|万字长文
一般基准测试原则 基准测试的主要测试案例是: 线性读写(大块,长队列),单位MB/s IOPS(每秒输入/输出操作数)中的小块(4-8kb,iodepth=32-128)的高度并行随机读写 IOPS中 ...
- 曾经运维生涯中的几个“最”
建荣写过一篇<维护之夜,说点故事和经验>,讲了一些维护工作中碰到的事情,虽然我的本职工作不是DBA,但是已经从事了十多年的应用运维工作,应用同样需要各种维护,包括上线投产.应急处置.迁移. ...
最新文章
- VS2010中文注释带红色下划线的解决方法
- pythonweb开发-Web开发
- makefile中模式规则的引入和介绍------%:%.cpp
- 云计算学习(5-1)云平台产品介绍-华为的FusionCloud产品
- epoll和poll的C++11多线程练习
- HDOJ水题集合6:杂题
- git回滚到某个版本操作
- python数据分析与发展常用哪些软件_常用数据分析软件比较
- Zabbix5.0监控服务器并设置邮件告警(安装Agent)
- 读 孙卫琴《Tomcat与Javaweb开发技术详解》
- 请注意:黑客开始用云隐藏IP地址
- 自媒体推广的方法和技巧有哪些?
- 创建视图簇SE54并SE93赋予事务代码
- java swing背景_java swing 设置背景图片的方法一
- get 到的html代码如何转码,爬虫网页转码逻辑
- 程序员练级攻略(2018):前端基础和底层原理
- 【Pyecharts|GEO-Lines】全球航线图的绘制
- 全面提升市域社会治理现代化建设发展水平的关键措施
- 主编编辑器怎么做出滑动样式?
- 2019年第一件大事儿! 华为Twitter事故真相了!