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);
}
}
至此,我们完成了“隐藏式”想法的实施。 当然,您还需要解决其他问题。
例如,您可以执行此实现并立即在线程上触发调用。 这样可以确保在第一时间刷新缓存。 如果过时是您的主要问题,则应该这样做。
我喜欢调度程序,因为它也可以作为一种限制机制 。 因此,如果您一遍又一遍地进行相同的呼叫,则后读调度程序会将这些呼叫折叠为一个呼叫
运行示例代码
- 先决条件:已安装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 cache