在Spring中制作一个基于缓存的简单断路器

本文适用于那些在其应用程序中使用有效缓存并希望不仅向应用程序而且向整个环境增加稳定性的人,只需向项目中添加1类即可。

如果您认识自己,请继续阅读。

什么是断路器?


电影中的相框回到未来

这个主题像世界一样刻薄,我不会厌倦您,增加熵并重复同样的事情。 从我的角度来看,马丁·福勒(Martin Fowler)在这里说得最好,但我会尽力将其定义写成一句话:
该功能可防止对不可用服务的明知注定的请求,从而使其“站起来”并继续正常运行

理想情况下,为防止失败的请求,断路器(以下简称CB)不应破坏您的应用程序。 取而代之的是,如果不是最新数据,但仍然相关(“不犯规”),或者如果无法做到,则返回一些默认值,这是一个很好的做法。

目标


我们选出主要内容:

  1. 必须允许数据源恢复,暂时停止对其查询
  2. 如果停止对目标服务的请求,则需要提供(即使不是最新的但仍是相关的)数据
  3. 如果目标服务不可用并且没有相关数据,请提供行为策略(返回默认值或其他适合特定情况的策略)

执行机制


案例:服务可用(第一个请求)


  1. 让我们去缓存。 按键(CRT,请参见下文)。 我们看到缓存中没有任何内容
  2. 我们去了目标服务。 我们获得价值
  3. 我们将值存储在缓存中,将其设置为TTL,它将覆盖目标服务不可用的最大可能时间,但是同时,它应该不超过您准备提供给客户端的数据的相关性期限,以防与目标服务的连接断开
  4. 高速缓存刷新时间(CRT)存储在高速缓存中,以获取第3条中的值-在该时间之后,您需要尝试转到目标服务并更新该值
  5. 将第2项中的值返回给用户

案例:CRT没有过期


  1. 让我们去缓存。 通过键,我们找到了CRT。 我们看到这是相关的
  2. 从缓存中获取它的值。
  3. 将值返回给用户。

案例:CRT过期,目标服务可用


  1. 让我们去缓存。 通过键,我们找到了CRT。 我们看到这无关紧要
  2. 我们去了目标服务。 我们获得价值
  3. 更新缓存中的值及其TTL
  4. 通过添加缓存刷新周期(CRP)为其更新CRT-这是需要添加到CRT以获得下一个CRT的值
  5. 将值返回给用户。

案例:CRT过期,目标服务不可用


  1. 让我们去缓存。 通过键,我们找到了CRT。 我们看到这无关紧要
  2. 我们去了目标服务。 他不可用
  3. 从缓存中获取值。 不是最新的(CRT烂了),但仍然有意义,因为它的TTL尚未过期
  4. 我们将其退还给用户

案例:CRT过期,目标服务不可用,缓存中没有任何内容


  1. 让我们去缓存。 通过键,我们找到了CRT。 我们看到这无关紧要
  2. 我们去了目标服务。 他不可用
  3. 从缓存中获取值。 他不是
  4. 对于这种情况,我们正在尝试采用特殊的策略。 例如,返回指定字段的默认值或“此信息当前不可用”类型的特殊值。 通常,如果可能,最好返回一些内容而不破坏应用程序。 如果不可能,那么您需要对异常用户应用异常抛出策略和快速响应。

我们将使用什么


我在项目中使用Spring Boot 1.5,仍然没有时间升级到第二个版本。

那篇文章没有再发表2遍,我将使用Lombok。

作为键值存储(以下简称为KV),我使用Redis 5.0.3,但是我敢肯定,Hazelcast或类似产品都可以。 最主要的是,有一个CacheManager接口的实现。 就我而言,这是spring-boot-starter-data-redis中的RedisCacheManager。

实作


上面的“实施机制”部分中,做了两个重要的定义:CRT和CRP。 我将再次详细介绍它们,因为 它们对于理解以下代码非常重要:

缓存刷新时间( CRT )是KV中的一个单独条目(键+后缀“ _crt”),它显示了到目标服务获取最新数据的时间。 与TTL不同,CRT的出现并不意味着您的数据“烂了”,而只是意味着它很可能在目标服务中变得更新。 新鲜了-好吧,如果没有的话,电流会下降。

缓存刷新周期( CRP )是在轮询目标服务后添加到CRT的值(无论成功与否都无关紧要)。 多亏了她,远程服务人员能够在摔倒时“屏住呼吸”并恢复工作。

因此,传统上,我们从设计主界面开始。 通过它,如果需要CB逻辑,就需要使用缓存。 它应该尽可能简单:

public interface CircuitBreakerService { <T> T getStableValue(StableValueParameter parameter); void evictValue(EvictValueParameter parameter); } 

接口参数:

 @Getter @AllArgsConstructor public class StableValueParameter<T> { private String cachePrefix; //    private String objectCacheKey; private long crpInSeconds; // Cache Refresh Period private Supplier<T> targetServiceAction; //      private DisasterStrategy disasterStrategy; //   : CRT ,   ,     public StableValueParameter( String cachePrefix, String objectCacheKey, long crpInSeconds, Supplier<T> targetServiceAction ) { this.cachePrefix = cachePrefix; this.objectCacheKey = objectCacheKey; this.crpInSeconds = crpInSeconds; this.targetServiceAction = targetServiceAction; this.disasterStrategy = new ThrowExceptionDisasterStrategy(); } } 

 @Getter @AllArgsConstructor public class EvictValueParameter { private String cachePrefix; private String objectCacheKey; } 

这是我们将如何使用它:

 public AccountDataResponse findAccount(String accountId) { final StableValueParameter<?> parameter = new StableValueParameter<>( ACCOUNT_CACHE_PREFIX, accountId, properties.getCrpInSeconds(), () -> bankClient.findById(accountId) ); return circuitBreakerService.getStableValue(parameter); } 

如果您需要清除缓存,则:

 public void evictAccount(String accountId) { final EvictValueParameter parameter = new EvictValueParameter( ACCOUNT_CACHE_PREFIX, accountId ); circuitBreakerService.evictValue(parameter); } 

现在,最有趣的是实现(在代码的注释中说明):

 @Override public <T> T getStableValue(StableValueParameter parameter) { final Cache cache = cacheManager.getCache(parameter.getCachePrefix()); if (cache == null) { return logAndThrowUnexpectedCacheMissing(parameter.getCachePrefix(), parameter.getObjectCacheKey()); } //   .   CRT final String crtKey = parameter.getObjectCacheKey() + CRT_CACHE_POSTFIX; //  CRT  ,    final LocalDateTime crt = Optional.ofNullable(cache.get(crtKey, LocalDateTime.class)) .orElseGet(() -> DateTimeUtils.now().minusSeconds(1)); if (DateTimeUtils.now().isBefore(crt)) { //  CRT   ,     final Optional<T> valueFromCache = getFromCache(parameter, cache); if (valueFromCache.isPresent()) { return valueFromCache.get(); } } //  CRT  ,        return getFromTargetServiceAndUpdateCache(parameter, cache, crtKey, crt); } private static <T> Optional<T> getFromCache(StableValueParameter parameter, Cache cache) { return (Optional<T>) Optional.ofNullable(cache.get(parameter.getObjectCacheKey())) .map(Cache.ValueWrapper::get); } 

如果目标服务不可用,请尝试从缓存中获取仍然相关的数据:

 private <T> T getFromTargetServiceAndUpdateCache( StableValueParameter parameter, Cache cache, String crtKey, LocalDateTime crt ) { T result; try { result = getFromTargetService(parameter); } /* Circuit breaker exceptions */ catch (WebServiceIOException ex) { log.warn( "[CircuitBreaker] Service responded with error: {}. Try get from cache {}: {}", ex.getMessage(), parameter.getCachePrefix(), parameter.getObjectCacheKey()); result = getFromCacheOrDisasterStrategy(parameter, cache); } cache.put(parameter.getObjectCacheKey(), result); cache.put(crtKey, crt.plusSeconds(parameter.getCrpInSeconds())); return result; } private static <T> T getFromTargetService(StableValueParameter parameter) { return (T) parameter.getTargetServiceAction().get(); } 

如果缓存中没有实际数据(它们已被TTL删除,并且目标服务仍然不可用),则我们使用DisasterStrategy:

 private <T> T getFromCacheOrDisasterStrategy(StableValueParameter parameter, Cache cache) { return (T) getFromCache(parameter, cache).orElseGet(() -> parameter.getDisasterStrategy().getValue()); } 

从缓存中删除没有什么有趣的,这里仅出于完整性考虑:

 private <T> T getFromCacheOrDisasterStrategy(StableValueParameter parameter, Cache cache) { return (T) getFromCache(parameter, cache).orElseGet(() -> parameter.getDisasterStrategy().getValue()); } 

从缓存中删除没有什么有趣的,这里仅出于完整性考虑:

 @Override public void evictValue(EvictValueParameter parameter) { final Cache cache = cacheManager.getCache(parameter.getCachePrefix()); if (cache == null) { logAndThrowUnexpectedCacheMissing(parameter.getCachePrefix(), parameter.getObjectCacheKey()); return; } final String crtKey = parameter.getObjectCacheKey() + CRT_CACHE_POSTFIX; cache.evict(crtKey); } 

灾难策略


电影中的相框回到未来

实际上,这是在CRT过期,目标服务不可用以及缓存中没有任何内容时发生的逻辑。

我想分别描述这种逻辑,因为 许多人不愿考虑如何实施它。 但这实际上是使我们的系统真正稳定的原因。

当所有只能失败的事物都被拒绝并且您的系统仍然可以运行时,您是否不想对自己的想法感到自豪? 即使事实上,例如,并非在“价格”字段中显示商品的实际价格,而是题词:“现在正在指定”,但这比答案“ 500服务不可用”要好得多。 毕竟,例如剩余的10个字段:产品说明等。 你回来了。 这样的服务质量会在多大程度上发生变化?..我的电话是要更加注意细节,使细节更好。

完成抒情离题。 因此,策略界面如下:

 public interface DisasterStrategy<T> { T getValue(); } 

您应根据具体情况选择实现。 例如,如果您可以返回一些默认值,则可以执行以下操作:

 public class DefaultValueDisasterStrategy implements DisasterStrategy<String> { @Override public String getValue() { return "   "; } } 

或者,如果在特定情况下根本不需要返回任何内容,则可以引发异常:

 public class ThrowExceptionDisasterStrategy implements DisasterStrategy<Object> { @Override public Object getValue() { throw new CircuitBreakerNullValueException("Ops! Service is down and there's null value in cache"); } } 

在这种情况下,CRT将不会增加,并且下一个请求将再次跟随目标服务。

结论


我坚持以下观点-如果您有机会使用现成的解决方案,而不必大惊小怪,实际上,虽然很简单,但在本文中还是值得一提的。 使用本文了解其工作原理,而不是作为操作指南。

现成的解决方案有很多,特别是如果您使用的是Spring Boot 2,例如Hystrix。

要了解的最重要的事情是,该解决方案基于缓存,其有效性等于缓存的有效性。 如果缓存无效(很少命中,很多未命中),那么断路器将同样无效:每次缓存未命中都会伴随着对目标服务的访问,这可能在此时此刻是痛苦的和痛苦的,试图增加。

在应用此方法之前,请务必评估缓存的有效性。 可以通过“缓存命中率” =命中数/(命中数+未命中数)来完成,它应该趋向于1,而不是0。

是的,没有人会打扰您一次使用最能解决特定问题的CB品种。

Source: https://habr.com/ru/post/zh-CN451858/


All Articles