Hacer un disyuntor basado en caché simple en primavera

Este artículo es para aquellos que usan un caché efectivo en su aplicación y desean agregar estabilidad no solo a la aplicación, sino a todo el entorno simplemente agregando 1 clase al proyecto.

Si te reconoces, sigue leyendo.

¿Qué es un disyuntor?


Fotograma de la película Regreso al futuro

El tema está trillado como el mundo y no te aburriré, aumentando la entropía y repitiendo lo mismo. Desde mi punto de vista, Martin Fowler habló lo mejor de todo aquí , pero trataré de ajustar la definición en una oración:
funcionalidad que evita las solicitudes condenados a un servicio no disponible, lo que le permite "ponerse de rodillas" y continuar la operación normal .

Idealmente, para evitar solicitudes condenadas, el disyuntor (en adelante CB) no debe interrumpir su aplicación. En cambio, es una buena práctica devolver, si no los datos más actuales, pero aún son relevantes ("no incorrectos") o, si esto no es posible, algún valor predeterminado.

Objetivos


Destacamos lo principal:

  1. Es necesario permitir que la fuente de datos se recupere, deteniendo las consultas por un tiempo
  2. En caso de detener las solicitudes al servicio de destino, debe proporcionar, si no la última, pero aún datos relevantes
  3. Si el servicio de destino no está disponible y no hay datos relevantes, proporcione una estrategia de comportamiento (devolver el valor predeterminado u otra estrategia adecuada para un caso particular)

Mecanismo de implementación


Caso: el servicio está disponible (primera solicitud)


  1. Vamos al caché. Por clave (CRT ver abajo). Vemos que no hay nada en el caché
  2. Vamos al servicio objetivo. Obtenemos el valor
  3. Almacenamos el valor en la memoria caché, lo configuramos en un TTL que cubra el tiempo máximo posible de que el servicio objetivo no esté disponible, pero al mismo tiempo no debe exceder el período de relevancia de los datos que está listo para proporcionar al cliente en caso de pérdida de conexión con el servicio objetivo.
  4. El tiempo de actualización de la memoria caché (CRT) se almacena en la memoria caché para el valor de la cláusula 3, el tiempo después del cual debe intentar ir al servicio de destino y actualizar el valor
  5. Devuelva el valor del elemento 2 al usuario

Caso: CRT no expiró


  1. Vamos al caché. Por la clave encontramos CRT. Vemos que es relevante
  2. Obtenga el valor del caché.
  3. Devuelve el valor al usuario.

Caso: CRT expiró, el servicio objetivo está disponible


  1. Vamos al caché. Por la clave encontramos CRT. Vemos que es irrelevante
  2. Vamos al servicio objetivo. Obtenemos el valor
  3. Actualización del valor en el caché y su TTL
  4. Actualice CRT para ello agregando el período de actualización de caché (CRP): este es el valor que debe agregarse a CRT para obtener el siguiente CRT
  5. Devuelve el valor al usuario.

Caso: CRT caducado, servicio de destino no disponible


  1. Vamos al caché. Por la clave encontramos CRT. Vemos que es irrelevante
  2. Vamos al servicio objetivo. No esta disponible
  3. Obtenga el valor del caché. No es el más fresco (con un CRT podrido), pero sigue siendo relevante, ya que su TTL aún no ha expirado
  4. Se lo devolvemos al usuario

Caso: CRT caducado, servicio de destino no disponible, nada en caché


  1. Vamos al caché. Por la clave encontramos CRT. Vemos que es irrelevante
  2. Vamos al servicio objetivo. No esta disponible
  3. Obtenga el valor del caché. El no es
  4. Estamos tratando de aplicar una estrategia especial para tales casos. Por ejemplo, devolver un valor predeterminado para un campo específico o un valor especial del tipo "Esta información no está disponible actualmente". En general, si esto es posible, es mejor devolver algo y no romper la aplicación. Si esto no es posible, entonces debe aplicar la estrategia de lanzamiento de excepción y una respuesta rápida al usuario de excepción.

Lo que usaremos


Utilizo Spring Boot 1.5 en mi proyecto, todavía no he encontrado el tiempo para actualizar a la segunda versión.

Que el artículo no resultó 2 veces más largo, usaré Lombok.

Como almacenamiento de valor clave (en lo sucesivo, simplemente denominado KV), uso Redis 5.0.3, pero estoy seguro de que Hazelcast o un análogo funcionará. Lo principal es que hay una implementación de la interfaz CacheManager. En mi caso, este es RedisCacheManager de spring-boot-starter-data-redis.

Implementación


Arriba, en la sección “Mecanismo de implementación”, se hicieron dos definiciones importantes: CRT y CRP. Los volveré a escribir con más detalle, porque son muy importantes para entender el siguiente código:

El tiempo de actualización de caché ( CRT ) es una entrada separada en KV (clave + postfix "_crt"), que muestra el momento en que es hora de ir al servicio de destino para obtener datos nuevos. A diferencia de TTL, el inicio de CRT no significa que sus datos estén "podridos", sino que es probable que se vuelvan más recientes en el servicio de destino. Se puso fresco, bueno, si no, y la corriente bajará.

El período de actualización de caché ( CRP ) es un valor que se agrega a la CRT después de sondear el servicio de destino (no importa si es exitoso o no). Gracias a ella, un servicio remoto tiene la capacidad de "recuperar el aliento" y restaurar su trabajo en caso de una caída.

Entonces, tradicionalmente, comenzamos diseñando la interfaz principal. Es a través de él que necesitará trabajar con el caché si necesita lógica CB. Debería ser lo más simple posible:

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

Parámetros de interfaz

 @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; } 

Así es como lo usaremos:

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

Si necesita borrar el caché, entonces:

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

Ahora lo más interesante es la implementación (explicada en los comentarios en el código):

 @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); } 

Si el servicio de destino no está disponible, intente obtener los datos aún relevantes del caché:

 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(); } 

Si no había datos reales en el caché (fueron eliminados por TTL, y el servicio de destino todavía no está disponible), entonces usamos DisasterStrategy:

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

Eliminar del caché no es nada interesante, lo daré aquí solo para completar:

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

Eliminar del caché no es nada interesante, lo daré aquí solo para completar:

 @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); } 

Estrategia de desastre


Fotograma de la película Regreso al futuro

De hecho, esta es la lógica que ocurre si el CRT caduca, el servicio de destino no está disponible y no hay nada en la memoria caché.

Quería describir esta lógica por separado, porque muchos no tienen en sus manos pensar en cómo implementarlo. Pero esto, de hecho, es lo que hace que nuestro sistema sea realmente estable.

¿No quieres sentir esa sensación de orgullo en tu creación cuando todo lo que solo puede fallar es rechazado y tu sistema aún funciona? Incluso a pesar del hecho de que, por ejemplo, no se mostrará el precio real de los productos en el campo "precio", sino la inscripción: "se está especificando ahora", pero ¿cuánto mejor es esto que la respuesta "El servicio 500 no está disponible". Después de todo, por ejemplo, los 10 campos restantes: descripción del producto, etc. Regresaste. ¿Cuánto cambia la calidad de un servicio de este tipo? .. Mi llamado es prestar más atención a los detalles para mejorarlos.

Terminando la digresión lírica. Entonces, la interfaz de la estrategia será la siguiente:

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

Debe elegir la implementación según el caso específico. Por ejemplo, si puede devolver algún valor predeterminado, puede hacer algo como esto:

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

O, si en un caso particular no tiene que devolver nada, puede lanzar una excepción:

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

En este caso, el CRT no se incrementará y la próxima solicitud seguirá nuevamente al servicio de destino.

Conclusión


Me adhiero al siguiente punto de vista: si tiene la oportunidad de usar una solución preparada y no hacer un escándalo, de hecho, aunque simple, pero aún en bicicleta en este artículo, hágalo. Use este artículo para comprender cómo funciona, y no como una guía de acción.

Hay tantas soluciones listas para usar, especialmente si está utilizando Spring Boot 2, como Hystrix.

Lo más importante a entender es que esta solución se basa en la memoria caché y su eficacia es igual a la efectividad de la memoria caché. Si el caché es ineficaz (pocos golpes, muchos fallos), entonces este disyuntor será igualmente ineficaz: cada fallo de caché irá acompañado de un viaje al servicio de destino, que, quizás en este momento está en agonía y agonía, tratando de elevarse.

Asegúrese de medir la efectividad de su caché antes de aplicar este enfoque. Esto se puede hacer mediante “Cache Hit Rate” = hits / (hits + miss), debe tender a 1, no a 0.

Y sí, nadie lo molesta para mantener varias variedades de CB en su proyecto a la vez, utilizando el que mejor resuelva el problema específico.

Source: https://habr.com/ru/post/451858/


All Articles