Cet article est destiné à ceux qui utilisent un cache efficace dans leur application et souhaitent ajouter de la stabilité non seulement à l'application, mais à l'environnement entier en ajoutant simplement 1 classe au projet.
Si vous vous reconnaissez, lisez la suite.
Qu'est-ce qu'un disjoncteur?

Le thème est galvaudé comme le monde et je ne vous ennuierai pas, augmentant l'entropie et répétant la même chose. De mon point de vue, Martin Fowler a parlé le mieux
ici , mais je vais essayer de rentrer la définition dans une phrase:
fonctionnalité qui empêche les demandes sciemment vouées à un service indisponible, lui permettant de «se lever de ses genoux» et de continuer à fonctionner normalement .
Idéalement, pour éviter les demandes condamnées, le disjoncteur (ci-après CB) ne devrait pas casser votre application. Au lieu de cela, il est recommandé de renvoyer, sinon les données les plus récentes, mais toujours pertinentes («pas fautes»), ou, si cela n'est pas possible, une valeur par défaut.
Buts
Nous distinguons l'essentiel:
- Il est nécessaire de permettre à la source de données de récupérer, en arrêtant les requêtes pendant un certain temps
- En cas d'arrêt des demandes au service cible, vous devez fournir, sinon les données les plus récentes mais toujours pertinentes
- Si le service cible n'est pas disponible et qu'il n'y a pas de données pertinentes, fournissez une stratégie de comportement (renvoyant la valeur par défaut ou une autre stratégie adaptée à un cas particulier)
Mécanisme de mise en œuvre
Cas: le service est disponible (première demande)
- Allons dans le cache. Par clé (CRT voir ci-dessous). On voit qu'il n'y a rien dans le cache
- Nous allons au service cible. Nous obtenons la valeur
- Nous stockons la valeur dans le cache, la définissons sur un tel TTL qui couvrira le temps maximum possible pendant lequel le service cible n'est pas disponible, mais en même temps, il ne devrait pas dépasser la période de pertinence des données que vous êtes prêt à fournir au client en cas de perte de connexion avec le service cible
- Le temps de rafraîchissement du cache (CRT) est stocké dans le cache pour la valeur de la clause 3 - le temps après lequel vous devez essayer d'accéder au service cible et mettre à jour la valeur
- Renvoyer la valeur de l'élément 2 à l'utilisateur
Cas: CRT n'a pas expiré
- Allons dans le cache. Par la clé, nous trouvons CRT. On voit que c'est pertinent
- Obtenez-en la valeur dans le cache.
- Renvoyez la valeur à l'utilisateur.
Cas: CRT a expiré, le service cible est disponible
- Allons dans le cache. Par la clé, nous trouvons CRT. Nous voyons que ce n'est pas pertinent
- Nous allons au service cible. Nous obtenons la valeur
- Mise à jour de la valeur dans le cache et son TTL
- Mettre à jour le CRT pour cela, en ajoutant la période de rafraîchissement du cache (CRP) - c'est la valeur qui doit être ajoutée au CRT pour obtenir le prochain CRT
- Renvoyez la valeur à l'utilisateur.
Cas: CRT expiré, service cible non disponible
- Allons dans le cache. Par la clé, nous trouvons CRT. Nous voyons que ce n'est pas pertinent
- Nous allons au service cible. Il n'est pas disponible
- Obtenez la valeur du cache. Pas le plus récent (avec un CRT pourri), mais toujours pertinent, car son TTL n'a pas encore expiré
- Nous le renvoyons à l'utilisateur
Cas: CRT expiré, service cible indisponible, rien dans le cache
- Allons dans le cache. Par la clé, nous trouvons CRT. Nous voyons que ce n'est pas pertinent
- Nous allons au service cible. Il n'est pas disponible
- Obtenez la valeur du cache. Il n'est pas
- Nous essayons d'appliquer une stratégie spéciale pour de tels cas. Par exemple, renvoyer une valeur par défaut pour un champ spécifié ou une valeur spéciale du type «Ces informations ne sont pas actuellement disponibles». En général, si cela est possible, il est préférable de renvoyer quelque chose et de ne pas casser l'application. Si cela n'est pas possible, vous devez appliquer la stratégie de levée d'exceptions et la réponse rapide à l'utilisateur d'exception.
Ce que nous utiliserons
J'utilise Spring Boot 1.5 dans mon projet, je n'ai toujours pas trouvé le temps de passer à la deuxième version.
Que l'article ne s'est pas avéré 2 fois plus long, j'utiliserai Lombok.
En tant que stockage de valeur-clé (ci-après simplement appelé KV), j'utilise Redis 5.0.3, mais je suis sûr que Hazelcast ou un analogue fera l'affaire. L'essentiel est qu'il existe une implémentation de l'interface CacheManager. Dans mon cas, il s'agit de RedisCacheManager de spring-boot-starter-data-redis.
Implémentation
Ci-dessus, dans la section «Mécanisme de mise en œuvre», deux définitions importantes ont été faites: CRT et CRP. Je vais les réécrire plus en détail, car ils sont très importants pour comprendre le code qui suit:
Le temps de rafraîchissement du cache (
CRT ) est une entrée distincte en KV (clé + suffixe «_crt»), qui indique l'heure à laquelle il est temps d'aller au service cible pour de nouvelles données. Contrairement à TTL, l'apparition de CRT ne signifie pas que vos données sont «pourries», mais seulement qu'elles sont susceptibles d'être plus récentes dans le service cible. Je suis frais - enfin, sinon, et le courant descendra.
La période de rafraîchissement du cache (
CRP ) est une valeur qui est ajoutée au CRT après l'interrogation du service cible (peu importe qu'elle réussisse ou non). Grâce à elle, un service à distance a la capacité de «reprendre son souffle» et de restaurer son travail en cas de chute.
Donc, traditionnellement, nous commençons par concevoir l'interface principale. C'est à travers lui que vous devrez travailler avec le cache si vous avez besoin de la logique CB. Cela devrait être aussi simple que possible:
public interface CircuitBreakerService { <T> T getStableValue(StableValueParameter parameter); void evictValue(EvictValueParameter parameter); }
Paramètres d'interface:
@Getter @AllArgsConstructor public class StableValueParameter<T> { private String cachePrefix;
@Getter @AllArgsConstructor public class EvictValueParameter { private String cachePrefix; private String objectCacheKey; }
Voici comment nous allons l'utiliser:
public AccountDataResponse findAccount(String accountId) { final StableValueParameter<?> parameter = new StableValueParameter<>( ACCOUNT_CACHE_PREFIX, accountId, properties.getCrpInSeconds(), () -> bankClient.findById(accountId) ); return circuitBreakerService.getStableValue(parameter); }
Si vous devez vider le cache, alors:
public void evictAccount(String accountId) { final EvictValueParameter parameter = new EvictValueParameter( ACCOUNT_CACHE_PREFIX, accountId ); circuitBreakerService.evictValue(parameter); }
Maintenant, la chose la plus intéressante est l'implémentation (expliquée dans les commentaires dans le code):
@Override public <T> T getStableValue(StableValueParameter parameter) { final Cache cache = cacheManager.getCache(parameter.getCachePrefix()); if (cache == null) { return logAndThrowUnexpectedCacheMissing(parameter.getCachePrefix(), parameter.getObjectCacheKey()); }
Si le service cible n'est pas disponible, essayez d'obtenir les données toujours pertinentes du cache:
private <T> T getFromTargetServiceAndUpdateCache( StableValueParameter parameter, Cache cache, String crtKey, LocalDateTime crt ) { T result; try { result = getFromTargetService(parameter); } 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(); }
S'il n'y avait pas de données réelles dans le cache (elles ont été supprimées par TTL et le service cible n'est toujours pas disponible), alors nous utilisons DisasterStrategy:
private <T> T getFromCacheOrDisasterStrategy(StableValueParameter parameter, Cache cache) { return (T) getFromCache(parameter, cache).orElseGet(() -> parameter.getDisasterStrategy().getValue()); }
Supprimer du cache n'a rien d'intéressant, je ne le donnerai ici que pour être complet:
private <T> T getFromCacheOrDisasterStrategy(StableValueParameter parameter, Cache cache) { return (T) getFromCache(parameter, cache).orElseGet(() -> parameter.getDisasterStrategy().getValue()); }
Supprimer du cache n'a rien d'intéressant, je ne le donnerai ici que pour être complet:
@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); }
Stratégie en cas de catastrophe

C'est, en fait, la logique qui se produit si le CRT expire, le service cible n'est pas disponible et il n'y a rien dans le cache.
Je voulais décrire cette logique séparément, car beaucoup n'arrivent pas à réfléchir à la manière de le mettre en œuvre. Mais c'est en fait ce qui rend notre système vraiment stable.
Ne voulez-vous pas ressentir ce sentiment de fierté dans votre idée lorsque tout ce qui ne peut qu'échouer est refusé et que votre système fonctionne toujours? Même en dépit du fait que, par exemple, dans le champ «prix», non pas le coût réel des marchandises sera affiché, mais l'inscription: «en cours de spécification», mais combien est-ce mieux que la réponse «le service 500 n'est pas disponible». Après tout, par exemple, les 10 champs restants: description du produit, etc. tu es revenu. Dans quelle mesure la qualité d'un tel service change-t-elle? .. Mon appel est de prêter plus d'attention aux détails, en les améliorant.
Fin de la digression lyrique. Ainsi, l'interface de stratégie sera la suivante:
public interface DisasterStrategy<T> { T getValue(); }
Vous devez choisir l'implémentation en fonction du cas spécifique. Par exemple, si vous pouvez renvoyer une valeur par défaut, vous pouvez faire quelque chose comme ceci:
public class DefaultValueDisasterStrategy implements DisasterStrategy<String> { @Override public String getValue() { return " "; } }
Ou, si dans un cas particulier vous n'avez rien à retourner du tout, vous pouvez lever une exception:
public class ThrowExceptionDisasterStrategy implements DisasterStrategy<Object> { @Override public Object getValue() { throw new CircuitBreakerNullValueException("Ops! Service is down and there's null value in cache"); } }
Dans ce cas, le CRT ne sera pas incrémenté et la prochaine demande sera de nouveau envoyée au service cible.
Conclusion
J'adhère au point de vue suivant - si vous avez la possibilité d'utiliser une solution toute faite, et de ne pas faire d'histoires, en fait, bien que simple, mais toujours vélo dans cet article, faites-le. Utilisez cet article pour comprendre comment il fonctionne et non pas comme guide d'action.
Il existe de nombreuses solutions prêtes à l'emploi, surtout si vous utilisez Spring Boot 2, comme Hystrix.
La chose la plus importante à comprendre est que cette solution est basée sur le cache et que son efficacité est égale à l'efficacité du cache. Si le cache est inefficace (quelques hits, beaucoup de ratés), alors ce disjoncteur sera tout aussi inefficace: chaque cache raté sera accompagné d'un voyage vers le service cible, qui, peut-être, est à l'agonie et à l'angoisse en ce moment, essayant de s'élever.
Assurez-vous de mesurer l'efficacité de votre cache avant d'appliquer cette approche. Cela peut être fait par "Cache Hit Rate" = hits / (hits + misses), il devrait avoir tendance à 1, pas à 0.
Et oui, personne ne vous dérange pour conserver plusieurs variétés de CB à la fois dans votre projet, en utilisant celle qui résout le mieux le problème spécifique.