Heim  >  Artikel  >  Java  >  Die Java-Implementierung stellt die Konsistenz des doppelten Schreibens zwischen Cache und Datenbank sicher

Die Java-Implementierung stellt die Konsistenz des doppelten Schreibens zwischen Cache und Datenbank sicher

coldplay.xixi
coldplay.xixinach vorne
2020-12-30 17:51:022765Durchsuche

Einfaches Java-TutorialDie Spalte stellt die Konsistenz des doppelten Schreibens zwischen Cache und Datenbank sicher

Die Java-Implementierung stellt die Konsistenz des doppelten Schreibens zwischen Cache und Datenbank sicher

Bitte hebe deinen Kopf, meine Prinzessin, sonst fällt die Krone.

Der verteilte Cache ist heutzutage eine unverzichtbare Komponente in vielen verteilten Anwendungen, aber die Verwendung des verteilten Caches kann eine doppelte Speicherung und doppeltes Schreiben von Cache und Datenbank beinhalten. Solange Sie doppelt schreiben, wird es ein Problem mit der Konsistenz geben Lösen Sie das Problem der Konsistenz?

Cache-Aside-Muster

Der klassischste Caching- + Datenbank-Lese- und Schreibmodus ist das Cache-Aside-Muster.
Lesen Sie beim Lesen zuerst den Cache, lesen Sie die Datenbank, nehmen Sie dann die Daten heraus, legen Sie sie in den Cache und geben Sie gleichzeitig die Antwort zurück.

Aktualisieren Sie beim Aktualisieren zuerst die Datenbank und löschen Sie dann den Cache.

Warum den Cache löschen, anstatt den Cache zu aktualisieren?

Der Grund ist sehr einfach. In komplexen Caching-Szenarien ist der Cache nicht nur der Wert, der direkt aus der Datenbank entnommen wird.

Zum Beispiel kann ein Feld einer bestimmten Tabelle aktualisiert werden, und dann muss der entsprechende Cache die Daten der anderen beiden Tabellen abfragen und Operationen ausführen, um den neuesten Wert des Caches zu berechnen.

Außerdem sind die Kosten für die Aktualisierung des Caches teilweise sehr hoch. Bedeutet das, dass bei jeder Änderung der Datenbank der entsprechende Cache aktualisiert werden muss? Dies kann in einigen Szenarien der Fall sein, bei komplexeren Berechnungsszenarien für zwischengespeicherte Daten ist dies jedoch nicht der Fall. Wenn Sie häufig mehrere an einem Cache beteiligte Tabellen ändern, wird auch der Cache häufig aktualisiert. Die Frage ist jedoch: Wird auf diesen Cache häufig zugegriffen?

Wenn beispielsweise ein Feld einer in einem Cache enthaltenen Tabelle 20 Mal oder 100 Mal in 1 Minute geändert wird, wird der Cache 20 Mal oder 100 Mal aktualisiert, dieser Cache wird jedoch nur 1 Mal gelesen. Es gibt viele kalte Daten. Wenn Sie den Cache einfach löschen, wird der Cache nur einmal in einer Minute neu berechnet und der Overhead wird erheblich reduziert. Der Cache wird nur zur Berechnung des Caches verwendet.

Tatsächlich ist das Löschen des Caches anstelle des Aktualisierens eine faule Berechnungsidee. Führen Sie komplexe Berechnungen nicht jedes Mal erneut durch, unabhängig davon, ob sie verwendet werden, sondern lassen Sie sie neu berechnen, wenn sie verwendet werden müssen. Wie Mybatis und Hibernate haben sie alle die Idee des Lazy Loading. Bei der Abfrage einer Abteilung bringt die Abteilung eine Liste der Mitarbeiter mit. Es ist nicht nötig zu erwähnen, dass bei jeder Abfrage der Abteilung auch die Daten der 1.000 darin enthaltenen Mitarbeiter gefunden werden. In 80 % der Fälle erfordert die Überprüfung dieser Abteilung lediglich den Zugriff auf die Informationen dieser Abteilung. Überprüfen Sie zunächst die Abteilung und müssen gleichzeitig auf die darin enthaltenen Mitarbeiter zugreifen. Erst wenn Sie auf die Mitarbeiter im Inneren zugreifen möchten, fragen Sie die Datenbank nach 1.000 Mitarbeitern ab.

Das grundlegendste Problem und die Lösung der Cache-Inkonsistenz

Problem: Ändern Sie zuerst die Datenbank und löschen Sie dann den Cache. Wenn das Löschen des Caches fehlschlägt, entstehen neue Daten in der Datenbank und alte Daten im Cache, was zu Dateninkonsistenzen führt.
Die Java-Implementierung stellt die Konsistenz des doppelten Schreibens zwischen Cache und Datenbank sicher

Lösung: Löschen Sie zuerst den Cache und ändern Sie dann die Datenbank. Wenn die Datenbankänderung fehlschlägt, befinden sich alte Daten in der Datenbank und der Cache ist leer, sodass die Daten nicht inkonsistent sind. Da beim Lesen kein Cache vorhanden ist, werden die alten Daten in der Datenbank gelesen und dann im Cache aktualisiert.

Analyse komplexerer Dateninkonsistenzprobleme

Die Daten haben sich geändert. Löschen Sie zuerst den Cache und ändern Sie dann die Datenbank. Zu diesem Zeitpunkt wurden sie nicht geändert. Wenn eine Anfrage eingeht, lese ich den Cache und stelle fest, dass der Cache leer ist. Ich frage die Datenbank ab, finde die alten Daten vor der Änderung und lege sie in den Cache. Anschließend schließt das Datenänderungsprogramm die Änderung der Datenbank ab.

Es ist vorbei, die Daten in der Datenbank und im Cache sind unterschiedlich. . .

Warum hat der Cache dieses Problem in einem Szenario mit hoher Parallelität und Hunderten Millionen Datenverkehr?

Dieses Problem kann nur auftreten, wenn Daten gleichzeitig gelesen und geschrieben werden. Wenn Ihre Parallelität sehr niedrig ist, insbesondere wenn die Lese-Parallelität mit nur 10.000 Besuchen pro Tag sehr niedrig ist, tritt in seltenen Fällen das gerade beschriebene inkonsistente Szenario auf. Das Problem besteht jedoch darin, dass bei Hunderten Millionen Datenverkehr pro Tag und Zehntausenden gleichzeitigen Lesevorgängen pro Sekunde die oben erwähnte Datenbank- und Cache-Inkonsistenz auftreten kann, solange jede Sekunde Datenaktualisierungsanforderungen vorliegen.

Die Lösung lautet wie folgt:

Wenn Sie Daten aktualisieren, leiten Sie den Vorgang basierend auf der eindeutigen Kennung der Daten weiter und senden Sie ihn an eine interne JVM-Warteschlange. Wenn beim Lesen von Daten festgestellt wird, dass sich die Daten nicht im Cache befinden, werden die Daten erneut gelesen und der Cache aktualisiert. Nach dem Routing werden sie gemäß der eindeutigen Kennung auch an dieselbe interne JVM-Warteschlange gesendet .

Eine Warteschlange entspricht einem Arbeitsthread. Jeder Arbeitsthread ruft die entsprechenden Vorgänge seriell ab und führt sie dann einzeln aus. In diesem Fall wird bei einem Datenänderungsvorgang zunächst der Cache gelöscht und dann die Datenbank aktualisiert, die Aktualisierung ist jedoch noch nicht abgeschlossen. Wenn zu diesem Zeitpunkt eine Leseanforderung eingeht und der leere Cache gelesen wird, kann die Cache-Aktualisierungsanforderung zuerst an die Warteschlange gesendet werden. Zu diesem Zeitpunkt wird sie in der Warteschlange zurückgehalten und dann synchron auf die Cache-Aktualisierung gewartet vollendet.

Hier gibt es einen Optimierungspunkt. Es macht eigentlich keinen Sinn, mehrere Aktualisierungs-Cache-Anfragen aneinanderzureihen. Daher kann eine Filterung durchgeführt werden, wenn festgestellt wird, dass bereits eine Anfrage zum Aktualisieren des Caches vorhanden ist , dann ist es nicht nötig, eine weitere Aktualisierungsanforderung einzugeben, und Sie können einfach warten, bis die vorherige Aktualisierungsanforderung abgeschlossen ist.

Nachdem der dieser Warteschlange entsprechende Arbeitsthread die Änderung der Datenbank für den vorherigen Vorgang abgeschlossen hat, führt er den nächsten Vorgang aus, nämlich den Cache-Aktualisierungsvorgang. Zu diesem Zeitpunkt wird der neueste Wert aus der Datenbank gelesen In den Cache geschrieben.

Wenn die Anfrage noch innerhalb des Wartezeitbereichs liegt und eine kontinuierliche Abfrage ergibt, dass der Wert abgerufen werden kann, wird direkt zurückgegeben, wenn die Anfrage länger als eine bestimmte Zeit wartet, dann wird der aktuelle alte Wert zurückgegeben direkt aus der Datenbank gelesen werden.

In Szenarien mit hoher Parallelität müssen bei dieser Lösung folgende Probleme beachtet werden:

1. Leseanfragen werden für lange Zeit blockiert.

Da die Leseanfragen sehr leicht asynchron sind, müssen Sie auf das Lese-Timeout achten Problem. Jede Leseanfrage muss innerhalb des Timeout-Zeitraums zurückgegeben werden.

Der größte Risikopunkt dieser Lösung besteht darin, dass die Daten möglicherweise häufig aktualisiert werden, was zu einem großen Rückstand an Aktualisierungsvorgängen in der Warteschlange führt und dann eine große Anzahl von Zeitüberschreitungen bei Leseanforderungen auftritt, was schließlich zu einer großen Anzahl von Zeitüberschreitungen führt Anzahl der Anfragen, die direkt an die Datenbank gehen. Führen Sie unbedingt einige simulierte reale Tests durch, um zu sehen, wie oft die Daten aktualisiert werden.

Ein weiterer Punkt: Da es möglicherweise einen Rückstand an Aktualisierungsvorgängen für mehrere Datenelemente in einer Warteschlange gibt, müssen Sie diese entsprechend Ihren eigenen Geschäftsbedingungen testen. Möglicherweise müssen Sie mehrere Dienste bereitstellen, und jeder Dienst teilt einige Datenaktualisierungen Operationen. Wenn eine Speicherwarteschlange tatsächlich die Bestandsänderungsvorgänge von 100 Produkten komprimiert und jeder Bestandsänderungsvorgang 10 ms dauert, kann die Leseanforderung für das letzte Produkt 10 * 100 = 1000 ms = 1 s warten, bevor die Daten abgerufen werden können. Dieses Mal führt dies zu einer langfristigen Blockierung von Leseanforderungen.

Führen Sie unbedingt einige Stresstests durch und simulieren Sie die Online-Umgebung basierend auf dem tatsächlichen Betrieb des Geschäftssystems, um zu sehen, wie viele Aktualisierungsvorgänge die Speicherwarteschlange während der geschäftigsten Zeit belasten kann, was dazu führen kann, dass der letzte Aktualisierungsvorgang wie lange dauert Wird die entsprechende Leseanforderung hängen bleiben? Wenn die Leseanforderung innerhalb von 200 ms zurückgegeben wird, wenn Sie berechnen, dass selbst zum Zeitpunkt der höchsten Auslastung ein Rückstand von 10 Aktualisierungsvorgängen vorliegt und die maximale Wartezeit 200 ms beträgt, ist das in Ordnung.

Wenn in einer Speicherwarteschlange möglicherweise viele Aktualisierungsvorgänge zurückbleiben, müssen Sie weitere Maschinen hinzufügen, damit die auf jeder Maschine bereitgestellten Dienstinstanzen weniger Daten verarbeiten. Dann wird der Rückstand an Aktualisierungsvorgängen in jeder Speicherwarteschlange verringert werden immer größer.

Tatsächlich ist die Häufigkeit des Datenschreibens aufgrund früherer Projekterfahrungen im Allgemeinen sehr gering, sodass normalerweise nur sehr wenige Aktualisierungsvorgänge in der Warteschlange zurückbleiben sollten. Für Projekte wie dieses, die auf eine hohe Lese-Parallelität und Lese-Caching-Architektur abzielen, gibt es im Allgemeinen nur sehr wenige Schreibanfragen. Es wäre gut, wenn die QPS pro Sekunde einige Hundert erreichen könnten.

Eine grobe Berechnung

Wenn es 500 Schreibvorgänge pro Sekunde gibt, wenn es in 5 Zeitscheiben aufgeteilt wird, gibt es alle 200 ms 100 Schreibvorgänge, und diese werden in 20 Speicherwarteschlangen platziert. Jede Speicherwarteschlange kann überlastet sein 5 Schreibvorgänge. Nach jedem Leistungstest für Schreibvorgänge ist dieser normalerweise in etwa 20 ms abgeschlossen, sodass die Leseanforderung für die Daten in jeder Speicherwarteschlange höchstens eine Weile hängen bleibt und definitiv innerhalb von 200 ms zurückgegeben wird.

Nach der einfachen Berechnung wissen wir, dass die von einer einzelnen Maschine unterstützten Schreib-QPS im Hunderterbereich kein Problem darstellen. Wenn die Schreib-QPS um das Zehnfache erhöht werden, wird die Maschine um das Zehnfache erweitert, und zwar für jede Maschine wird 20 Warteschlangen haben.

2. Die Parallelität der Leseanforderungen ist zu hoch

Hier muss ein Stresstest durchgeführt werden, um sicherzustellen, dass im oben genannten Fall ein weiteres Risiko besteht, nämlich dass plötzlich eine große Anzahl von Leseanforderungen hängen bleibt eine Verzögerung von mehreren zehn Millisekunden. In Bezug auf den Dienst hängt es davon ab, ob der Dienst damit umgehen kann und wie viele Maschinen erforderlich sind, um den Höhepunkt der maximalen Extremsituation zu bewältigen.

Da jedoch nicht alle Daten gleichzeitig aktualisiert werden und der Cache nicht gleichzeitig abläuft, kann der Cache einer kleinen Anzahl von Daten jedes Mal ungültig sein, und die diesen Daten entsprechenden Leseanforderungen werden dann nicht ungültig Die Parallelität wird sehr groß sein.

3. Anforderungsrouting für die Bereitstellung mehrerer Dienstinstanzen

Möglicherweise stellt dieser Dienst mehrere Instanzen bereit. Dann muss sichergestellt werden, dass Anforderungen zur Durchführung von Datenaktualisierungsvorgängen und Cache-Aktualisierungsvorgängen über den übergeordneten Nginx-Server an dieselbe Dienstinstanz weitergeleitet werden.

Zum Beispiel werden alle Lese- und Schreibanfragen für dasselbe Produkt an dieselbe Maschine weitergeleitet. Sie können Ihr eigenes Hash-Routing zwischen Diensten gemäß einem bestimmten Anforderungsparameter durchführen oder die Hash-Routing-Funktion von Nginx usw. verwenden.

4. Das Routing-Problem heißer Produkte führt zu verzerrten Anforderungen

Wenn die Lese- und Schreibanforderungen eines bestimmten Produkts besonders hoch sind und alle an dieselbe Warteschlange derselben Maschine gesendet werden, kann dies zu einer übermäßigen Belastung eines Produkts führen bestimmte Maschine. Das heißt, da der Cache nur geleert wird, wenn die Produktdaten aktualisiert werden, und dann Lese- und Schreibgleichzeitigkeit auftritt, hängt es tatsächlich vom Geschäftssystem ab, ob die Aktualisierungshäufigkeit nicht zu hoch ist Das Problem ist nicht besonders groß. Es stimmt jedoch, dass die Auslastung einiger Maschinen höher sein kann.

Das obige ist der detaillierte Inhalt vonDie Java-Implementierung stellt die Konsistenz des doppelten Schreibens zwischen Cache und Datenbank sicher. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen