So können Sie einen Monolithen in Dienste zerlegen und die Leistung von In-Memory-Caches aufrechterhalten, ohne die Konsistenz zu verlieren


Hallo allerseits. Mein Name ist Alexander, ich bin Java-Entwickler in der Tinkoff-Unternehmensgruppe.

In diesem Artikel möchte ich meine Erfahrungen bei der Lösung von Problemen im Zusammenhang mit der Synchronisierung des Cachezustands in verteilten Systemen mitteilen. Wir sind ihnen begegnet und haben unsere monolithische Anwendung in Mikrodienste aufgeteilt . Offensichtlich werden wir über das Zwischenspeichern von Daten auf JVM-Ebene sprechen, da bei externen Caches Synchronisationsprobleme außerhalb des Anwendungskontexts gelöst werden.

In diesem Artikel werde ich über unsere Erfahrungen mit der Umstellung auf eine serviceorientierte Architektur, begleitet von einem Wechsel zu Kubernetes, und über die Lösung damit zusammenhängender Probleme sprechen. Wir werden den Ansatz zur Organisation des verteilten Cachingsystems In-Memory Data Grid (IMDG) mit seinen Vor- und Nachteilen betrachten, weshalb wir beschlossen haben, eine eigene Lösung zu schreiben.

Dieser Artikel beschreibt ein Projekt, dessen Backend in Java geschrieben ist. Daher werden wir auch über Standards im Bereich des temporären In-Memory-Cachings sprechen. Wir diskutieren die JSR-107-Spezifikation, die fehlgeschlagene JSR-347-Spezifikation und die Caching-Funktionen im Frühjahr. Willkommen bei Katze!


Und lassen Sie uns die Anwendung in Dienste schneiden ...


Wir werden uns der serviceorientierten Architektur und Kubernetes zuwenden - das haben wir vor etwas mehr als 6 Monaten entschieden. Lange Zeit war unser Projekt ein Monolith, viele Probleme im Zusammenhang mit der technischen Verschuldung haben sich angesammelt, und wir haben neue Anwendungsmodule als separate Dienste geschrieben. Infolgedessen war der Übergang zu einer serviceorientierten Architektur und einem Monolithschnitt unvermeidlich.

Unsere Anwendung ist geladen, im Durchschnitt kommen 500 U / min zu Webdiensten (in der Spitze erreichen sie 900 U / min). Um das gesamte Datenmodell als Antwort auf jede Anforderung zu erfassen, müssen Sie mehrere hundert Mal zu den verschiedenen Caches wechseln.

Wir versuchen, nicht öfter als dreimal pro Anforderung zum Remote-Cache zu wechseln, je nach dem erforderlichen Datensatz. Bei internen JVM-Caches erreicht die Last 90.000 Rps pro Cache. Wir haben ungefähr 30 solcher Caches für eine Vielzahl von Entitäten und DTO-shki. Bei einigen geladenen Caches können wir es uns nicht einmal leisten, den Wert zu löschen, da dies zu einer Verlängerung der Antwortzeit von Webdiensten und zu einem Absturz der Anwendung führen kann.


So sieht die Lastüberwachung aus, die während des Tages von den internen Caches auf jedem Knoten entfernt wird. Anhand des Lastprofils ist leicht zu erkennen, dass es sich bei den meisten Anforderungen um gelesene Daten handelt. Eine gleichmäßige Schreiblast ist darauf zurückzuführen, dass die Werte in den Caches mit einer bestimmten Häufigkeit aktualisiert werden.

Ausfallzeiten gelten nicht für unsere Anwendung. Aus diesem Grund haben wir zum Zweck einer nahtlosen Bereitstellung den gesamten eingehenden Datenverkehr auf zwei Knoten verteilt und die Anwendung mithilfe der Methode "Rolling Update" bereitgestellt. Kubernetes wurde zu unserer idealen Infrastrukturlösung beim Wechsel zu Diensten. Somit haben wir mehrere Probleme auf einmal gelöst.

Das Problem, ständig Infrastruktur für neue Dienste zu bestellen und einzurichten


Wir erhielten einen Namespace im Cluster für jede Schaltung, für die wir drei haben: dev - für Entwickler, qa - für Tester, prod - für Kunden.

Wenn der Namespace markiert ist, müssen beim Hinzufügen eines neuen Dienstes oder einer neuen Anwendung vier Manifeste erstellt werden: Deployment, Service, Ingress und ConfigMap.

Hohe Belastungstoleranz


Das Geschäft wächst und wächst stetig - vor einem Jahr war die durchschnittliche Auslastung doppelt so hoch wie die derzeitige.

Die horizontale Skalierung in Kubernetes ermöglicht es Ihnen, die Skaleneffekte mit zunehmender Arbeitsbelastung des entwickelten Projekts auszugleichen.

Wartung, Protokollierung und Überwachung


Das Leben wird viel einfacher, wenn Sie keine Protokolle zum Protokollierungssystem hinzufügen müssen, wenn Sie jeden Knoten hinzufügen, den Messbereich konfigurieren (es sei denn, Sie haben ein Push-Überwachungssystem), Netzwerkeinstellungen vornehmen und einfach die für den Betrieb erforderliche Software installieren.

Natürlich kann dies alles mit Ansible oder Terraform automatisiert werden, aber letztendlich ist es viel einfacher, mehrere Manifeste für jeden Service zu schreiben.

Hohe Zuverlässigkeit


Dank des integrierten k8s-Mechanismus für Liveness- und Readiness-Samples können Sie sich keine Sorgen machen, dass die Anwendung langsamer wird oder gar nicht mehr reagiert.

Kubernetes kontrolliert jetzt den Lebenszyklus von Herd-Pods, die Anwendungscontainer enthalten, und den Verkehr, der zu ihnen geleitet wird.

Zusammen mit den beschriebenen Annehmlichkeiten mussten wir eine Reihe von Problemen lösen, um die Dienste für die horizontale Skalierung und die Verwendung eines gemeinsamen Datenmodells für viele Dienste geeignet zu machen. Es mussten zwei Probleme gelöst werden:

  1. Der Status der Anwendung. Wenn das Projekt im k8s-Cluster bereitgestellt wird, werden Pods mit Containern der neuen Version der Anwendung erstellt, die sich nicht auf den Status der Pods der vorherigen Version beziehen. Neue Anwendungs-Pods können auf beliebigen Clusterservern erstellt werden, die die angegebenen Einschränkungen erfüllen. Außerdem kann jetzt jeder Anwendungscontainer, der im Kubernetes-Pod ausgeführt wird, jederzeit zerstört werden, wenn der Liveness-Test einen Neustart anzeigt.
  2. Datenkonsistenz. Auf allen Knoten müssen Konsistenz und Datenintegrität gewährleistet sein. Dies gilt insbesondere dann, wenn mehrere Knoten in einem einzigen Datenmodell arbeiten. Es ist inakzeptabel, dass inkonsistente Daten beim Client eingehen, wenn Anforderungen an verschiedene Knoten der Anwendung in der Antwort gestellt werden.

In der modernen Entwicklung skalierbarer Systeme ist die zustandslose Architektur die Lösung für die oben genannten Probleme. Das erste Problem wurde behoben, indem alle statischen Daten in den S3-Cloud-Speicher verschoben wurden.

Aufgrund der Notwendigkeit, ein komplexes Datenmodell zu aggregieren und die Antwortzeit unserer Webservices zu verkürzen, konnten wir das Speichern von Daten in In-Memory-Caches jedoch nicht ablehnen. Um das zweite Problem zu lösen, haben sie eine Bibliothek geschrieben, um den Zustand der internen Caches einzelner Knoten zu synchronisieren.

Wir synchronisieren Caches auf separaten Knoten


Als Ausgangsdaten haben wir ein verteiltes System bestehend aus N Knoten. Jeder Knoten verfügt über ungefähr 20 In-Memory-Caches, deren Daten mehrmals pro Stunde aktualisiert werden.

Die meisten Caches verfügen über eine TTL-Datenaktualisierungsrichtlinie (Time-to-Live). Einige Daten werden aufgrund der hohen Auslastung alle 20 Minuten mit einer CRON-Operation aktualisiert. Die Arbeitsbelastung der Caches variiert zwischen mehreren Tausend U / min in der Nacht und mehreren Zehntausend im Laufe des Tages. Die Spitzenlast überschreitet in der Regel 100.000 U / min nicht. Die Anzahl der Datensätze im temporären Speicher überschreitet nicht mehrere Hunderttausend und wird auf dem Heap eines Knotens abgelegt.

Unsere Aufgabe ist es, die Datenkonsistenz zwischen demselben Cache auf verschiedenen Knoten sowie die kürzestmögliche Antwortzeit zu erreichen. Überlegen Sie, wie Sie dieses Problem im Allgemeinen lösen können.

Die erste und einfachste Lösung besteht darin, alle Informationen in einem Remote-Cache abzulegen. In diesem Fall können Sie den Status der Anwendung vollständig aufheben, sich keine Gedanken über die Probleme beim Erreichen der Konsistenz machen und einen einzigen Zugriffspunkt auf ein temporäres Data Warehouse haben.


Diese Methode der temporären Datenspeicherung ist recht einfach und wird von uns verwendet. Wir speichern einen Teil der Daten in Redis , einem NoSQL-Datenspeicher im RAM. In Redis zeichnen wir normalerweise ein Web-Service-Antwort-Framework auf und müssen diese Daten für jede Anforderung mit relevanten Informationen anreichern, für die wir mehrere hundert Anforderungen an den lokalen Cache senden müssen.

Offensichtlich können wir die Daten interner Caches nicht für die Remotespeicherung entfernen, da die Kosten für die Übertragung eines solchen Verkehrsvolumens über das Netzwerk es uns nicht ermöglichen, die erforderliche Antwortzeit einzuhalten.

Die zweite Option ist die Verwendung eines In-Memory Data Grid (IMDG), bei dem es sich um einen verteilten In-Memory-Cache handelt. Das Schema einer solchen Lösung ist wie folgt:


Die IMDG-Architektur basiert auf dem Prinzip der Datenpartitionierung interner Caches einzelner Knoten. Tatsächlich kann dies eine Hash-Tabelle genannt werden, die auf einem Cluster von Knoten verteilt ist. IMDG gilt als eine der schnellsten Implementierungen von temporärem verteiltem Speicher.

Es gibt viele IMDG-Implementierungen, die beliebtesten sind Hazelcast . Mit dem verteilten Cache können Sie Daten im RAM auf mehreren Anwendungsknoten mit einem akzeptablen Maß an Zuverlässigkeit und Wahrung der Konsistenz speichern, was durch die Datenreplikation erreicht wird.

Die Aufgabe, einen solchen verteilten Cache zu erstellen, ist nicht einfach. Die Verwendung einer vorgefertigten IMDG-Lösung könnte jedoch ein guter Ersatz für JVM-Caches sein und die Probleme der Replikation, Konsistenz und Datenverteilung zwischen allen Anwendungsknoten beseitigen.

Die meisten IMDG-Anbieter für Java-Anwendungen implementieren JSR-107 , die Standard-Java-API für die Arbeit mit internen Caches. Im Allgemeinen hat dieser Standard eine ziemlich große Geschichte, auf die ich im Folgenden näher eingehen werde.

Es waren einmal Ideen, Ihre Schnittstelle für die Interaktion mit IMDG - JSR 347 zu implementieren. Die Implementierung einer solchen API wurde jedoch von der Java-Community nicht ausreichend unterstützt, und jetzt verfügen wir über eine einzige Schnittstelle für die Interaktion mit In-Memory-Caches, unabhängig von der Architektur unserer Anwendung. Gut oder schlecht ist eine andere Frage, aber es ermöglicht uns, alle Schwierigkeiten bei der Implementierung eines verteilten In-Memory-Caches zu ignorieren und damit als Cache einer monolithischen Anwendung zu arbeiten.

Trotz der offensichtlichen Vorteile der Verwendung von IMDG ist diese Lösung immer noch langsamer als der Standard-JVM-Cache, da die fortlaufende Replikation von Daten, die auf mehrere JVM-Knoten verteilt sind, sowie die Sicherung dieser Daten überflüssig wird. In unserem Fall war die Datenmenge für die temporäre Speicherung nicht so groß, und die Daten mit einem Spielraum passen in den Speicher einer Anwendung. Daher schien die Zuordnung zu mehreren JVMs eine übermäßige Lösung zu sein. Zusätzlicher Netzwerkverkehr zwischen Anwendungsknoten unter hoher Last kann die Leistung erheblich beeinträchtigen und die Antwortzeit von Webdiensten verlängern. Am Ende haben wir beschlossen, eine eigene Lösung für dieses Problem zu schreiben.

Wir haben In-Memory-Caches als temporären Datenspeicher belassen und zur Wahrung der Konsistenz den RabbitMQ-Warteschlangenmanager verwendet. Wir haben das Designmuster Publisher-Subscriber übernommen und die Relevanz der Daten beibehalten, indem wir den geänderten Eintrag aus dem Cache jedes Knotens gelöscht haben. Das Lösungsschema lautet wie folgt:


Das Diagramm zeigt einen Cluster von N Knoten, von denen jeder über einen Standard-In-Memory-Cache verfügt. Alle Knoten verwenden ein gemeinsames Datenmodell und müssen konsistent sein. Beim ersten Zugriff auf den Cache über einen beliebigen Schlüssel fehlt der Wert im Cache, und wir geben den tatsächlichen Wert aus der Datenbank in den Cache ein. Bei jeder Änderung - löschen Sie den Datensatz.

Tatsächliche Informationen in der Cache-Antwort werden hier durch Synchronisieren des Löschens eines Eintrags bereitgestellt, wenn dieser an einem der Knoten geändert wird. Jeder Knoten im System hat eine Warteschlange im RabbitMQ-Warteschlangenmanager. Die Aufzeichnung in allen Warteschlangen erfolgt über einen gemeinsamen Zugriffspunkt vom Typ Thema. Dies bedeutet, dass an Topic gesendete Nachrichten in alle damit verbundenen Warteschlangen fallen. Wenn Sie also den Wert auf einem beliebigen Knoten des Systems ändern, wird dieser Wert aus dem temporären Speicher jedes Knotens gelöscht, und durch nachfolgenden Zugriff wird der aktuelle Wert aus der Datenbank in den Cache geschrieben.

Übrigens gibt es in Redis einen ähnlichen PUB / SUB-Mechanismus. Aber meiner Meinung nach ist es immer noch besser, den Warteschlangenmanager für die Arbeit mit Warteschlangen zu verwenden, und RabbitMQ war perfekt für unsere Aufgabe.

JSR 107 Standard und dessen Implementierung


Die Standard-Java-Cache-API für die temporäre Speicherung von Daten im Speicher (Spezifikation JSR-107 ) hat eine ziemlich lange Geschichte und wurde seit 12 Jahren entwickelt.

In so langer Zeit haben sich die Ansätze zur Softwareentwicklung geändert, die Monolithen wurden durch Mikroservice-Architekturen ersetzt. Aufgrund eines so langen Mangels an Spezifikationen für die Cache-API gab es sogar Anfragen, API-Caches für verteilte Systeme JSR-347 (Data Grids für die Java-Plattform) zu entwickeln. Nach der lang erwarteten Veröffentlichung von JSR-107 und der Veröffentlichung von JCache wurde die Aufforderung zur Erstellung einer separaten Spezifikation für verteilte Systeme zurückgezogen.

Im Laufe der langen 12 Jahre auf dem Markt hat sich der Ort für die temporäre Datenspeicherung mit der Veröffentlichung von Java 1.5 von HashMap zu ConcurrentHashMap geändert, und später wurden viele fertige Open-Source-Implementierungen von In-Memory-Caching veröffentlicht.

Nach der Veröffentlichung von JSR-107 begannen Anbieterlösungen, die neue Spezifikation schrittweise umzusetzen. Für JCache gibt es sogar Anbieter, die sich auf verteiltes Caching spezialisiert haben - die eigentlichen Data Grids, deren Spezifikation noch nie implementiert wurde.

Überlegen Sie, woraus das Paket javax.cache besteht und wie Sie eine Cache-Instanz für unsere Anwendung erhalten:
CachingProvider provider = Caching.getCachingProvider("org.cache2k.jcache.provider.JCacheProvider"); CacheManager cacheManager = provider.getCacheManager(); CacheConfiguration<Integer, String> config = new MutableConfiguration<Integer, String>() .setTypes(Integer.class, String.class) .setReadThrough(true) . . .; Cache<Integer, String> cache = cacheManager.createCache(cacheName, config); 

Hier ist Caching ein Bootloader für CachingProvider.

In unserem Fall wird JCacheProvider, die Cache2k-Implementierung des JSR-107-Anbieters SPI , von ClassLoader geladen. Für den Loader müssen Sie möglicherweise nicht die Provider-Implementierung angeben, aber dann wird versucht, die Implementierung zu laden, in der sich der Loader befindet
META-INF / services / javax.cache.spi.CachingProvider

In jedem Fall sollte es in ClassLoader eine einzige Implementierung des CachingProviders geben.

Wenn Sie die Bibliothek javax.cache ohne Implementierung verwenden, wird beim Versuch, JCache zu erstellen, eine Ausnahme ausgelöst. Der Anbieter erstellt und verwaltet den Lebenszyklus von CacheManager, der seinerseits für die Verwaltung und Konfiguration der Caches verantwortlich ist. Um einen Cache zu erstellen, müssen Sie folgendermaßen vorgehen:


Die mit CacheManager erstellten Standard-Caches müssen implementierungskompatibel konfiguriert sein. Die von javax.cache bereitgestellte standardmäßige parametrisierte CacheConfiguration kann auf eine bestimmte CacheProvider-Implementierung erweitert werden.

Heutzutage gibt es Dutzende verschiedener Implementierungen der JSR-107-Spezifikation: Ehcache , Guava , Koffein , Cache2k . Bei vielen Implementierungen handelt es sich um In-Memory- Datenraster in verteilten Systemen - Hazelcast , Oracle Coherence .

Es gibt auch viele temporäre Speicherimplementierungen, die die Standard-API nicht unterstützen. In unserem Projekt haben wir lange Zeit Ehcache 2 verwendet, das nicht mit JCache kompatibel ist (Implementierung der Spezifikation erschien mit Ehcache 3). Die Notwendigkeit eines Übergangs zu einer JCache-kompatiblen Implementierung ergab sich mit der Notwendigkeit, den Status von In-Memory-Caches zu überwachen. Mit der Standard-MetricRegistry konnte die Überwachung nur mit Hilfe der JCacheGaugeSet-Implementierung beschleunigt werden, die Metriken aus dem Standard-JCache sammelt.

Wie wähle ich die passende In-Memory-Cache-Implementierung für mein Projekt aus? Vielleicht sollten Sie folgendes beachten:

  1. Benötigen Sie Unterstützung für die JSR-107-Spezifikation?
  2. Es lohnt sich auch, auf die Geschwindigkeit der ausgewählten Implementierung zu achten. Unter hoher Last kann sich die Leistung interner Caches erheblich auf die Reaktionszeit Ihres Systems auswirken.
  3. Unterstützung im Frühling. Wenn Sie das bekannte Framework in Ihrem Projekt verwenden, sollten Sie berücksichtigen, dass nicht jede JVM-Cache-Implementierung im Frühjahr über einen kompatiblen CacheManager verfügt.

Wenn Sie Spring genau wie wir in Ihrem Projekt aktiv einsetzen, folgen Sie beim Zwischenspeichern von Daten höchstwahrscheinlich dem aspektorientierten Ansatz (AOP) und verwenden die Annotation @Cacheable. Spring verwendet ein eigenes CacheManager-SPI, damit Aspekte funktionieren. Die folgende Bean ist erforderlich, damit Spring Caches funktionieren:
 @Bean public org.springframework.cache.CacheManager cacheManager() { CachingProvider provider = Caching.getCachingProvider(); CacheManager cacheManager = provider.getCacheManager(); return new JCacheCacheManager(cacheManager); } 

Um mit Caches im AOP-Paradigma arbeiten zu können, müssen auch Transaktionsaspekte berücksichtigt werden. Der Spring-Cache muss unbedingt das Transaktionsmanagement unterstützen. Zu diesem Zweck erbt spring CacheManager die AbstractTransactionSupportingCacheManager-Eigenschaften, mit denen Put- / Evict-Vorgänge innerhalb einer Transaktion synchronisiert und erst ausgeführt werden können, nachdem eine erfolgreiche Transaktion festgeschrieben wurde.

Das obige Beispiel zeigt die Verwendung des JCacheCacheManager-Wrappers für den Cache-Spezifikationsmanager. Dies bedeutet, dass jede JSR-107-Implementierung auch mit Spring CacheManager kompatibel ist. Dies ist ein weiterer Grund, sich für einen In-Memory-Cache mit Unterstützung der JSR-Spezifikation für Ihr Projekt zu entscheiden. Wenn Sie diese Unterstützung jedoch immer noch nicht benötigen, aber wirklich @Cacheable verwenden möchten, stehen Ihnen zwei weitere interne Cache-Lösungen zur Verfügung: EhCacheCacheManager und CaffeineCacheManager.

Bei der Auswahl der Implementierung des speicherinternen Caches haben wir, wie bereits erwähnt, die Unterstützung von IMDG für verteilte Systeme nicht berücksichtigt. Um die Leistung von JVM-Caches auf unserem System aufrechtzuerhalten, haben wir unsere eigene Lösung geschrieben.

Löschen von Caches in einem verteilten System


Mit modernen IMDGs, die in Projekten mit Mikroservice-Architektur verwendet werden, können Sie Daten im Arbeitsspeicher über eine skalierbare Datenpartitionierung mit der erforderlichen Redundanzstufe auf alle Arbeitsknoten des Systems verteilen.

In diesem Fall treten viele Probleme im Zusammenhang mit der Synchronisation, der Datenkonsistenz usw. auf, ganz zu schweigen von der Zunahme der Zugriffszeit auf den temporären Speicher. Ein solches Schema ist redundant, wenn die Menge der verwendeten Daten in den RAM eines Knotens passt. Um die Konsistenz der Daten zu gewährleisten, reicht es aus, diesen Eintrag auf allen Knoten zu löschen, wenn sich der Cache-Wert ändert.

Bei der Implementierung einer solchen Lösung fällt zunächst die Idee ein, einen EventListener zu verwenden. In JCache gibt es einen CacheEntryRemovedListener für den Fall, dass ein Eintrag aus dem Cache gelöscht wird. Es ist anscheinend ausreichend, eine eigene Listener-Implementierung hinzuzufügen, die beim Löschen des Datensatzes Nachrichten an das Thema sendet, und der Eutect-Cache auf allen Knoten ist bereit - vorausgesetzt, jeder Knoten wartet auf Ereignisse aus der Warteschlange, die dem allgemeinen Thema zugeordnet sind (siehe Abbildung) oben.

Bei Verwendung dieser Lösung sind die Daten auf verschiedenen Knoten inkonsistent, da EventLists in jedem JCache-Implementierungsprozess nach einem Ereignis auftreten. Wenn sich für den angegebenen Schlüssel kein Datensatz im lokalen Cache befindet und für denselben Schlüssel ein Datensatz auf einem anderen Knoten vorhanden ist, wird das Ereignis nicht an das Thema gesendet.


Überlegen Sie, wie Sie das Ereignis abfangen können, wenn ein Wert aus dem lokalen Cache gelöscht wird.

Im Paket javax.cache.event befindet sich neben EventListeners auch ein CacheEntryEventFilter, mit dem gemäß JavaDoc vor dem Senden dieses Ereignisses an den CacheEntryListener geprüft wird, ob es sich um einen Datensatz, eine Löschung, eine Aktualisierung oder ein Ereignis im Zusammenhang mit dem Ablauf des Datensatzes handelt im Cache. Bei Verwendung des Filters bleibt unser Problem bestehen, da die Logik ausgeführt wird, nachdem das CacheEntryEvent-Ereignis protokolliert wurde und nachdem die CRUD-Operation im Cache ausgeführt wurde.

Trotzdem ist es möglich, die Auslösung eines Ereignisses abzufangen, um einen Datensatz aus dem Cache zu löschen. Verwenden Sie dazu das in JCache integrierte Tool, mit dem Sie API-Spezifikationen zum Schreiben und Laden von Daten aus einer externen Quelle verwenden können, wenn diese sich nicht im Cache befinden. Dafür gibt es im Paket javax.cache.integration zwei Schnittstellen:

  • CacheLoader - um die vom Schlüssel angeforderten Daten zu laden, wenn sich keine Einträge im Cache befinden.
  • CacheWriter - zum Schreiben, Löschen und Aktualisieren von Daten auf einer externen Ressource beim Aufrufen der entsprechenden Cache-Vorgänge.

Um die Konsistenz zu gewährleisten, sind die CacheWriter-Methoden in Bezug auf die entsprechende Cache-Operation atomar. Wir scheinen eine Lösung für unser Problem gefunden zu haben.

Jetzt können wir die Konsistenz der Reaktion von In-Memory-Caches auf Knoten beibehalten, wenn Sie unsere Implementierung von CacheWriter verwenden, die Ereignisse an das RabbitMQ-Thema sendet, wenn Änderungen im Datensatz im lokalen Cache vorgenommen werden.

Fazit


Bei der Entwicklung eines Projekts muss bei der Suche nach einer geeigneten Lösung für aufkommende Probleme dessen Spezifität berücksichtigt werden. In unserem Fall war es aufgrund der charakteristischen Merkmale des Projektdatenmodells, des vererbten Legacy-Codes und der Art der Auslastung nicht möglich, eine der vorhandenen Lösungen für das verteilte Caching-Problem zu verwenden.

Es ist sehr schwierig, eine universelle Implementierung auf ein entwickeltes System anzuwenden. Für jede solche Implementierung gibt es optimale Einsatzbedingungen. In unserem Fall führten die Besonderheiten des Projekts zu der in diesem Artikel beschriebenen Lösung. Wenn jemand ein ähnliches Problem hat, teilen wir Ihnen gerne unsere Lösung mit und veröffentlichen sie auf GitHub.

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


All Articles