Erstellen eines einfachen Cache-basierten Leistungsschalters im Frühjahr

Dieser Artikel richtet sich an Benutzer, die einen effektiven Cache in ihrer Anwendung verwenden und nicht nur der Anwendung, sondern der gesamten Umgebung Stabilität verleihen möchten, indem sie dem Projekt einfach eine Klasse hinzufügen.

Wenn Sie sich selbst erkennen, lesen Sie weiter.

Was ist ein Leistungsschalter?


Rahmen aus dem Film Zurück in die Zukunft

Das Thema ist abgedroschen wie die Welt, und ich werde Sie nicht langweilen, die Entropie erhöhen und dasselbe wiederholen. Aus meiner Sicht hat Martin Fowler hier am besten gesprochen, aber ich werde versuchen, die Definition in einen Satz zu fassen:
Funktionalität, die wissentlich zum Scheitern verurteilte Anfragen an einen nicht verfügbaren Dienst verhindert und es ihm ermöglicht, „auf die Knie zu gehen“ und den normalen Betrieb fortzusetzen .

Im Idealfall sollte der Leistungsschalter (im Folgenden: CB) Ihre Anwendung nicht beschädigen, um zum Scheitern verurteilte Anforderungen zu verhindern. Stattdessen empfiehlt es sich, wenn nicht die aktuellsten Daten, aber immer noch relevant („nicht schlecht“) oder, falls dies nicht möglich ist, einen Standardwert zurückzugeben.

Ziele


Wir heben die Hauptsache hervor:

  1. Es ist erforderlich, die Wiederherstellung der Datenquelle zuzulassen und die Abfragen für eine Weile anzuhalten
  2. Wenn Sie Anforderungen an den Zieldienst stoppen möchten, müssen Sie, wenn nicht die neuesten, aber dennoch relevanten Daten angeben
  3. Wenn der Zieldienst nicht verfügbar ist und keine relevanten Daten vorhanden sind, geben Sie eine Verhaltensstrategie an (Rückgabe des Standardwerts oder einer anderen für einen bestimmten Fall geeigneten Strategie).

Implementierungsmechanismus


Fall: Service ist verfügbar (erste Anfrage)


  1. Gehen wir zum Cache. Per Schlüssel (CRT siehe unten). Wir sehen, dass sich nichts im Cache befindet
  2. Wir gehen zum Zieldienst. Wir bekommen den Wert
  3. Wir speichern den Wert im Cache und setzen ihn auf eine solche TTL, die die maximal mögliche Zeit abdeckt, in der der Zieldienst nicht verfügbar ist. Gleichzeitig sollte er jedoch den Zeitraum der Relevanz der Daten nicht überschreiten, die Sie dem Client im Falle eines Verbindungsverlusts mit dem Zieldienst bereitstellen möchten
  4. Die Cache-Aktualisierungszeit (CRT) wird im Cache für den Wert aus Abschnitt 3 gespeichert - die Zeit, nach der Sie versuchen müssen, zum Zieldienst zu wechseln und den Wert zu aktualisieren
  5. Geben Sie den Wert von Punkt 2 an den Benutzer zurück

Fall: CRT ist nicht abgelaufen


  1. Gehen wir zum Cache. Durch den Schlüssel finden wir CRT. Wir sehen, dass es relevant ist
  2. Holen Sie sich den Wert dafür aus dem Cache.
  3. Geben Sie den Wert an den Benutzer zurück.

Fall: CRT abgelaufen, Zieldienst verfügbar


  1. Gehen wir zum Cache. Durch den Schlüssel finden wir CRT. Wir sehen, dass es irrelevant ist
  2. Wir gehen zum Zieldienst. Wir bekommen den Wert
  3. Aktualisieren des Werts im Cache und seiner TTL
  4. Aktualisieren Sie die CRT dafür, indem Sie Cache Refresh Period (CRP) hinzufügen. Dies ist der Wert, der der CRT hinzugefügt werden muss, um die nächste CRT zu erhalten
  5. Geben Sie den Wert an den Benutzer zurück.

Fall: CRT abgelaufen, Zieldienst nicht verfügbar


  1. Gehen wir zum Cache. Durch den Schlüssel finden wir CRT. Wir sehen, dass es irrelevant ist
  2. Wir gehen zum Zieldienst. Er ist nicht verfügbar
  3. Holen Sie sich den Wert aus dem Cache. Nicht die frischeste (mit einer faulen CRT), aber immer noch relevant, da die TTL noch nicht abgelaufen ist
  4. Wir geben es an den Benutzer zurück

Fall: CRT abgelaufen, Zieldienst nicht verfügbar, nichts im Cache


  1. Gehen wir zum Cache. Durch den Schlüssel finden wir CRT. Wir sehen, dass es irrelevant ist
  2. Wir gehen zum Zieldienst. Er ist nicht verfügbar
  3. Holen Sie sich den Wert aus dem Cache. Er ist nicht da
  4. Wir versuchen, für solche Fälle eine spezielle Strategie anzuwenden. Geben Sie beispielsweise einen Standardwert für ein angegebenes Feld oder einen speziellen Wert vom Typ "Diese Informationen sind derzeit nicht verfügbar" zurück. Wenn dies möglich ist, ist es im Allgemeinen besser, etwas zurückzugeben und die Anwendung nicht zu beschädigen. Wenn dies nicht möglich ist, müssen Sie die Ausnahmestrategie und die schnelle Antwort auf den Ausnahmebenutzer anwenden.

Was wir verwenden werden


Ich verwende Spring Boot 1.5 in meinem Projekt und habe noch keine Zeit gefunden, auf die zweite Version zu aktualisieren.

Daß der Artikel nicht zweimal länger ausfiel, werde ich Lombok verwenden.

Als Schlüsselwertspeicher (im Folgenden einfach als KV bezeichnet) verwende ich Redis 5.0.3, bin mir aber sicher, dass Hazelcast oder ein Analogon dies tun wird. Die Hauptsache ist, dass es eine Implementierung der CacheManager-Schnittstelle gibt. In meinem Fall ist dies RedisCacheManager von Spring-Boot-Starter-Data-Redis.

Implementierung


Oben wurden im Abschnitt „Implementierungsmechanismus“ zwei wichtige Definitionen vorgenommen: CRT und CRP. Ich werde sie noch einmal ausführlicher schreiben, weil Sie sind sehr wichtig für das Verständnis des folgenden Codes:

Cache Refresh Time ( CRT ) ist ein separater Eintrag in KV (Schlüssel + Postfix „_crt“), der die Zeit anzeigt, zu der es Zeit ist, zum Zieldienst zu gehen, um neue Daten zu erhalten. Im Gegensatz zu TTL bedeutet das Einsetzen der CRT nicht, dass Ihre Daten „faul“ sind, sondern nur, dass sie im Zieldienst wahrscheinlich aktueller werden. Wurde frisch - na ja, wenn nicht, und der Strom wird sinken.

Cache Refresh Period ( CRP ) ist ein Wert, der der CRT nach dem Abrufen des Zieldienstes hinzugefügt wird (es spielt keine Rolle, ob er erfolgreich ist oder nicht). Dank ihr hat ein Ferndienst die Möglichkeit, im Falle eines Sturzes „zu Atem zu kommen“ und seine Arbeit wiederherzustellen.

Daher beginnen wir traditionell mit dem Entwurf der Hauptschnittstelle. Dadurch müssen Sie mit dem Cache arbeiten, wenn Sie CB-Logik benötigen. Es sollte so einfach wie möglich sein:

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

Schnittstellenparameter:

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

So werden wir es verwenden:

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

Wenn Sie den Cache leeren müssen, dann:

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

Das Interessanteste ist nun die Implementierung (erklärt in den Kommentaren im Code):

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

Wenn der Zieldienst nicht verfügbar ist, versuchen Sie, die noch relevanten Daten aus dem Cache abzurufen:

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

Wenn sich keine tatsächlichen Daten im Cache befanden (sie wurden von TTL gelöscht und der Zieldienst ist immer noch nicht verfügbar), verwenden wir DisasterStrategy:

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

Das Entfernen aus dem Cache ist nichts Interessantes, ich werde es hier nur der Vollständigkeit halber geben:

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

Das Entfernen aus dem Cache ist nichts Interessantes, ich werde es hier nur der Vollständigkeit halber geben:

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

Katastrophenstrategie


Rahmen aus dem Film Zurück in die Zukunft

Dies ist in der Tat die Logik, die auftritt, wenn die CRT abläuft, der Zieldienst nicht verfügbar ist und sich nichts im Cache befindet.

Ich wollte diese Logik separat beschreiben, weil Viele denken nicht darüber nach, wie sie es umsetzen sollen. Aber genau das macht unser System wirklich stabil.

Wollen Sie nicht das Gefühl des Stolzes auf Ihre Idee verspüren, wenn alles, was nur scheitern kann, abgelehnt wird und Ihr System immer noch funktioniert? Auch wenn zum Beispiel im Feld "Preis" nicht die tatsächlichen Kosten der Ware angezeigt werden, sondern die Aufschrift "aktuell spezifiziert", aber wie viel besser ist dies als die Antwort "500 Service ist nicht verfügbar". Immerhin zum Beispiel die restlichen 10 Felder: Produktbeschreibung usw. Du bist zurückgekehrt. Inwieweit ändert sich die Qualität eines solchen Dienstes? .. Mein Aufruf ist es, mehr auf Details zu achten, um sie besser zu machen.

Den lyrischen Exkurs beenden. Die Strategie-Schnittstelle sieht also wie folgt aus:

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

Sie sollten die Implementierung je nach Einzelfall auswählen. Wenn Sie beispielsweise einen Standardwert zurückgeben können, können Sie Folgendes tun:

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

Wenn Sie in einem bestimmten Fall überhaupt nichts zurückgeben müssen, können Sie eine Ausnahme auslösen:

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

In diesem Fall wird die CRT nicht inkrementiert und die nächste Anforderung folgt erneut dem Zieldienst.

Fazit


Ich halte mich an den folgenden Standpunkt: Wenn Sie die Möglichkeit haben, eine vorgefertigte Lösung zu verwenden und keine Aufregung zu machen, obwohl dies in diesem Artikel einfach ist, aber dennoch Fahrrad fährt, tun Sie dies. Verwenden Sie diesen Artikel, um zu verstehen, wie es funktioniert, und nicht als Leitfaden für Maßnahmen.

Es gibt so viele vorgefertigte Lösungen, insbesondere wenn Sie Spring Boot 2 wie Hystrix verwenden.

Das Wichtigste zu verstehen ist, dass diese Lösung auf dem Cache basiert und ihre Effektivität der Effektivität des Caches entspricht. Wenn der Cache unwirksam ist (wenige Treffer, viele Fehler), ist dieser Leistungsschalter gleichermaßen unwirksam: Jeder Cache-Fehler wird von einer Reise zum Zieldienst begleitet, der möglicherweise in diesem Moment in Qual und Qual ist und versucht, zu steigen.

Stellen Sie sicher, dass Sie die Effektivität Ihres Caches messen, bevor Sie diesen Ansatz anwenden. Dies kann durch "Cache-Trefferquote" = Treffer / (Treffer + Fehlschläge) erfolgen, es sollte zu 1 tendieren, nicht zu 0.

Und ja, niemand stört Sie daran, mehrere CB-Sorten gleichzeitig in Ihrem Projekt zu behalten und diejenige zu verwenden, die das spezifische Problem am besten löst.

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


All Articles