read cache_通过READ-BEHIND CACHE控制您的慢速生产者

read 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依赖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); 
         } 
     } 

至此,我们完成了“隐藏式”想法的实施。 当然,您还需要解决其他问题。

例如,您可以执行此实现并立即在线程上触发调用。 这样可以确保在第一时间刷新缓存。 如果过时是您的主要问题,则应该这样做。

我喜欢调度程序,因为它也可以作为一种限制机制 。 因此,如果您一遍又一遍地进行相同的呼叫,则后读调度程序会将这些呼叫折叠为一个呼叫

运行示例代码

警告

正如我在本文开头所说的那样,在实现read-behind时您应该注意一些事情。

另外,如果您负担不起最终的一致性 ,请不要这样做

这适用于具有 低频变化 API的高频读取

如果API实现了某种ACL ,则需要在缓存键中添加用于发出请求的用户名 否则,可能会发生非常糟糕的事情。

因此,请仔细分析您的应用程序,并仅在适当的地方使用此想法。

翻译自: https://www.javacodegeeks.com/2019/12/take-control-your-slow-producers-read-behind-cache.html

read cache

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值