在我们的互联世界中,我们经常使用我们不拥有或无权改善的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来控制缓慢的生产者相关推荐

  1. HttpContext.Current.Cache在控制台下不工作

    说明: Cache 类不能在 ASP.NET 应用程序外使用.它是为在 ASP.NET 中用于为 Web 应用程序提供缓存而设计和测试的.在其他类型的应用程序(如控制台应用程序或 Windows 窗体 ...

  2. 白话Elasticsearch52-深入聚合数据分析之fielddata内存控制、circuit breaker短路器、fielddata filter、预加载机制以及序号标记预加载

    文章目录 概述 官网 fielddata核心原理 fielddata内存限制 监控fielddata内存使用 circuit breaker fielddata filter的细粒度内存加载控制 fi ...

  3. 计算机缓存Cache以及Cache Line详解

    转载: 计算机缓存Cache以及Cache Line详解 - 围城的文章 - 知乎 https://zhuanlan.zhihu.com/p/37749443 L1,L2,L3 Cache究竟在哪里? ...

  4. ARM存储器之:高速缓冲存储器Cache

    当第一代RISC微处理器刚出现时,标准存储器元件的速度比当时微处理器的速度快.很快,半导体工艺技术的进展被用来提高微处理器的速度.标准DRAM部件虽然也快了一些,但其发展的主要精力则放在提高存储容量上 ...

  5. QT学习:多线程控制

    实现线程的互斥与同步常使用的类有QMutex.QMutexLocker.QReadWriteLocker.QReadLocker. QWriteLocker.QSemaphore和QWaitCondi ...

  6. ElasticSearch 2 (37) - 信息聚合系列之内存与延时

    ElasticSearch 2 (37) - 信息聚合系列之内存与延时 摘要 控制内存使用与延时 版本 elasticsearch版本: elasticsearch-2.x 内容 Fielddata ...

  7. Spring Boot 3.0.0-M1 Reference Documentation(Spring Boot中文参考文档)-附录A-C

    附录 附录A:常用的应用程序属性 多种属性可以指定到application.properties文件,application.yml文件内,或者作为命令行开关.这个附录提供常用的Spring Boot ...

  8. Ceph性能测试、优化及硬件选型详解|万字长文

    一般基准测试原则 基准测试的主要测试案例是: 线性读写(大块,长队列),单位MB/s IOPS(每秒输入/输出操作数)中的小块(4-8kb,iodepth=32-128)的高度并行随机读写 IOPS中 ...

  9. 曾经运维生涯中的几个“最”

    建荣写过一篇<维护之夜,说点故事和经验>,讲了一些维护工作中碰到的事情,虽然我的本职工作不是DBA,但是已经从事了十多年的应用运维工作,应用同样需要各种维护,包括上线投产.应急处置.迁移. ...

最新文章

  1. VS2010中文注释带红色下划线的解决方法
  2. pythonweb开发-Web开发
  3. makefile中模式规则的引入和介绍------%:%.cpp
  4. 云计算学习(5-1)云平台产品介绍-华为的FusionCloud产品
  5. epoll和poll的C++11多线程练习
  6. HDOJ水题集合6:杂题
  7. git回滚到某个版本操作
  8. python数据分析与发展常用哪些软件_常用数据分析软件比较
  9. Zabbix5.0监控服务器并设置邮件告警(安装Agent)
  10. 读 孙卫琴《Tomcat与Javaweb开发技术详解》
  11. 请注意:黑客开始用云隐藏IP地址
  12. 自媒体推广的方法和技巧有哪些?
  13. 创建视图簇SE54并SE93赋予事务代码
  14. java swing背景_java swing 设置背景图片的方法一
  15. get 到的html代码如何转码,爬虫网页转码逻辑
  16. 程序员练级攻略(2018):前端基础和底层原理
  17. 【Pyecharts|GEO-Lines】全球航线图的绘制
  18. 全面提升市域社会治理现代化建设发展水平的关键措施
  19. 主编编辑器怎么做出滑动样式?
  20. 2019年第一件大事儿! 华为Twitter事故真相了!

热门文章

  1. 【并查集】【最小生成树】【贪心】给水(jzoj 2015)
  2. SpringCloud Zuul(六)之PRE Filter
  3. 设计数据库表时,你真的会选数据类型吗
  4. Java synchronized 中的while 和 notifyAll
  5. java并发编程(二十一)----(JUC集合)CopyOnWriteArraySet和ConcurrentSkipListSet介绍
  6. 如何改变Idea的背景
  7. springboot从控制器请求至页面时js失效的解决方法
  8. 时间胶囊——给未来的留言板
  9. .net三层架构开发步骤
  10. JavaScript实现四则运算