Heim  >  Artikel  >  Datenbank  >  Eine kurze Diskussion über das Kommunikationsprinzip von MySQL JDBC StreamResult

Eine kurze Diskussion über das Kommunikationsprinzip von MySQL JDBC StreamResult

不言
不言nach vorne
2018-10-19 16:33:332741Durchsuche

Dieser Artikel bietet Ihnen eine kurze Diskussion des MySQL JDBC StreamResult-Kommunikationsprinzips. Ich hoffe, dass er für Freunde hilfreich ist.

Wer MySQL JDBC zum Lesen großer Datenmengen (z. B. mehr als 1 GB) verwendet hat, sollte wissen, dass der Speicher beim Lesen wahrscheinlich den Java-Heap überläuft, und unsere Lösung ist Anweisung. setFetchSize(Integer.MIN_VALUE) und stellen Sie sicher, dass der Cursor schreibgeschützt ist und vorwärts scrollt (der Standardwert des Cursors). Sie können den Typ auch in com.mysql.jdbc.StatementImpl umwandeln und dann seine interne Methode aufrufen: enableStreamingResults(). ) Auf diese Weise hängt das Lesen des Datenspeichers nicht und der von beiden erzielte Effekt ist der gleiche. Natürlich kann auch useCursorFetch verwendet werden, aber die Testergebnisleistung dieser Methode ist viel langsamer als die von StreamResult. Warum? In diesem Artikel werden die allgemeinen Prinzipien erläutert.

Ich habe in einigen früheren Artikeln und Büchern vorgestellt, dass der interne Verarbeitungscode von MySQL JDBC in drei verschiedene Klassen unterteilt ist, aber ich habe mich nie mit der Beziehung zwischen Datenbank und JDBC befasst zwischen ihnen? Eine Zeit lang habe ich immer gedacht, dass dies ein serverseitiges Verhalten oder ein kooperatives Verhalten zwischen dem Client und dem Server ist, aber das ist nicht der Fall. Heute werden wir darüber sprechen, was dieses Verhalten ist.

[Überprüfen Sie zuerst die einfache Kommunikation]:

Die Kommunikation zwischen JDBC und der Datenbank wird über Socket abgeschlossen, sodass wir die Datenbank als SocketServer-Anbieter Square behandeln können Wenn SocketServer Daten zurückgibt (ähnlich der Rückgabe von SQL-Ergebnissätzen), lautet der Prozess: Serverprogrammdaten (Datenbank) -> Client-Socket-Puffer -> JDBC befindet sich)

Bisher ist der JDBC, den jeder in der IT-Branche gesehen hat: MySQL JDBC, SQL Server JDBC, PG JDBC, Oracle JDBC. Auch für NoSQL-Clients: Redis Client, MongoDB Client, Memcached folgt die Datenrückgabe grundsätzlich der gleichen Logik.

Eine kurze Diskussion über das Kommunikationsprinzip von MySQL JDBC StreamResult

[Warum bleibt es hängen, wenn MySQL JDBC standardmäßig zum direkten Lesen von Daten verwendet wird? 】

(1) Der von MySQL Server initiierte SQL-Ergebnissatz gibt alle Daten über OutputStream aus, dh er schreibt Daten in den Socket-Puffer, der dem lokalen Kennel entspricht. Dies ist eine Speicherkopie Das Kopieren steht nicht im Mittelpunkt dieses Artikels).

(2) Wenn der Kennel-Puffer Daten enthält, werden die Daten über die TCP-Verbindung (Socket-Verbindung, die aktiv von JDBC initiiert wird) zurückgesendet. Zu diesem Zeitpunkt werden die Daten an die Maschine zurückgesendet, auf der sich JDBC befindet Wenn es eingeschaltet ist, gelangt es zunächst in den Zwingerbereich und dann in einen Pufferbereich.

(3) Nachdem JDBC die SQL-Operation initiiert hat, blockiert der Java-Code die Operation inputStream.read(), wenn sich Daten im Puffer befinden, und liest dann die Daten im Puffer. Für den Java-Speicher handelt es sich hierbei um eine Speicherkopie auf der JDBC-Seite.

(4) Als nächstes liest MySQL JDBC weiterhin die Pufferdaten in den Java-Speicher und MySQL Server sendet weiterhin Daten. Beachten Sie, dass der vom Client initiierte SQL-Vorgang nicht antwortet, bevor die Daten vollständig zusammengestellt sind. Dies bedeutet, dass Sie das Gefühl haben, dass der MySQL-Server noch nicht geantwortet hat. Tatsächlich wurden die Daten lokal gesendet, JDBC jedoch Die Ergebnismenge wurde noch nicht an die Stelle zurückgegeben, an der die Ausführungsmethode als erstes Datenelement aufgerufen wurde, sondern liest kontinuierlich Daten aus dem Puffer.

(5) Der Schlüssel ist, dass dieser Dummkopf nach dem Lesen der Daten den Inhalt der gesamten Tabelle in den Java-Speicher liest, unabhängig davon, ob er zu Hause gespeichert ist. Der nächste Schritt ist der Speicher Überlauf.

[Das Festlegen von useCursorFetch=true für die JDBC-Parameter kann das Problem lösen]

Diese Lösung in Kombination mit der FetchSize-Einstellung kann das Problem tatsächlich lösen Wie viele Daten möchte ich auf dem MySQL-Server verwenden? Genau wie in unserem Leben gilt: Was ich brauche, ist, in den Supermarkt zu gehen und so viel zu kaufen, wie man braucht. Diese Art der Interaktion ist jedoch nicht mit Online-Einkäufen vergleichbar. Sie können Dinge einfach zu Hause liefern (Netzwerkverbindung), was bedeutet, dass die Daten 100 Millionen Daten umfassen. Setzen Sie FetchSize auf: Wenn es 1000 ist, gibt es 100.000 Round-Trip-Kommunikationen; wenn die Netzwerkverzögerung 0,02 ms im selben Computerraum beträgt, dann fügen 100.000 Kommunikationen 2 Sekunden hinzu, was keine große Sache ist. Wenn die Verzögerungszeit in allen Computerräumen 2 ms beträgt, ist sie 200 Sekunden länger (d. h. 3 Minuten und 20 Sekunden). Wenn die Verzögerungszeit in allen Städten Chinas 10 bis 40 ms beträgt, beträgt die Zeit 1000 bis 4000 Sekunden . Was ist, wenn es länderübergreifend 200 bis 300 ms beträgt? Die Zeit wird sich um mehr als zehn Stunden verlängern.

In den Berechnungen hier haben wir die erhöhte Anzahl von Systemaufrufen, die erhöhte Anzahl von Kontexten zum Warten und Aufwecken von Threads und die Auswirkungen von Netzwerkpaket-Neuübertragungen auf die Gesamtleistung nicht berücksichtigt Es scheint vernünftig, aber die Leistung ist tatsächlich nicht sehr gut.

Da MySQL außerdem nicht weiß, wann der Client die Daten verbraucht hat, und die entsprechende Tabelle möglicherweise über DML-Schreibvorgänge verfügt, muss MySQL einen temporären Tabellenbereich erstellen, um die Daten zu speichern, die entfernt werden müssen. Wenn Sie also useCursorFetch zum Lesen einer großen Tabelle aktivieren, werden Sie bei MySQL mehrere Phänomene beobachten:

(1) IOPS-Anstiege aufgrund einer großen Menge an E/A-Lesevorgängen Kann zu Jitter beim Geschäftsschreiben führen. (2) Dieser temporäre Speicherplatz kann größer sein als die ursprüngliche Tabelle. Wenn diese Tabelle einen großen Anteil in der gesamten Datenbank einnimmt, kann dies zu Schreibvorgängen auf der Datenbank führen voll ist, wird der Speicherplatz von MySQL zurückgefordert, nachdem die Ergebnismenge gelesen wurde oder wenn der Client Result.close() initiiert.

(3) CPU und Speicher erhöhen sich abhängig von den Fähigkeiten der CPU um einen bestimmten Prozentsatz.

(4) Nachdem der Client-JDBC SQL initiiert hat, wartet er lange auf SQL-Antwortdaten. Während dieser Zeit bereitet der Server Daten vor. Dieses Warten unterscheidet sich von der ursprünglichen JDBC-Methode, bei der keine Daten festgelegt werden Es zeigt auch das Warten an. Ersteres liest weiterhin Daten aus dem Netzwerkpuffer und reagiert nicht auf den temporären Datenraum.

[Stream-Lesedaten]

Wir wissen, dass die erste Methode dazu führt, dass Java hängen bleibt, und die zweite Methode ineffizient ist und größere Auswirkungen auf die MySQL-Datenbank hat . Die Antwort des Clients ist ebenfalls langsam und kann das Problem nur lösen. Schauen wir uns nun die Stream-Lesemethode an.

Wie bereits erwähnt, können Sie bei Verwendung von „statement.setFetchSize(Integer.MIN_VALUE)“ oder „com.mysql.jdbc.StatementImpl.enableStreamingResults()“ aktivieren, dass „FetchSize“ vor der Initiierung nicht verwendet werden kann Führen Sie es manuell aus und stellen Sie sicher, dass der Cursor FORWARD_ONLY ist.

Diese Methode ist erstaunlich. Es scheint, dass der Speicher nicht mehr blockiert, die Antwort schneller ist und die Auswirkungen auf MySQL auch geringer sind ist weg. Früher sah ich nur separaten Code in JDBC und dachte, es sei ein anderes Kommunikationsprotokoll zwischen MySQL und JDBC. Ich wusste nicht, dass es sich um „Client-Verhalten“ handelte. Verhalten.

Wenn es enableStreamingResults() initiiert, erfolgt kaum eine Interaktion mit dem Server. Das heißt, der Server gibt die Daten gemäß Methode 1 zurück, dann schiebt der Server die Daten hart in den Puffer Der Kunde wird Wie halten Sie dem Druck stand?

Wenn Sie in JDBC die Stream-Ergebnismengenverarbeitung aktivieren, werden nicht alle Daten auf einmal in den Java-Speicher eingelesen, d. h. die Daten in Abbildung 1 werden nicht auf einmal in den Java-Speicher eingelesen buffer liest jeweils ein Paket (dieses Paket kann in Java als Byte[]-Array verstanden werden). Es kann bis zu so viele auf einmal lesen und prüft dann, ob es weiter nach unten lesen soll, um die Integrität des Pakets sicherzustellen Daten. Der Geschäftscode wird auf Bytebasis in Zeilen zerlegt und von der Geschäftsseite verwendet.

Der Server beginnt gerade damit, Daten in den Puffer zu übertragen, und die Daten füllen auch den Kernelpuffer des Clients. Wenn die Puffer auf beiden Seiten voll sind, versucht ein Puffer auf dem Server, TCP zu passieren Wenn Daten an den Empfänger gesendet werden, ist der Puffer des Verbrauchers zu diesem Zeitpunkt ebenfalls voll, sodass der Thread des Absenders blockiert wird und darauf wartet, dass die andere Partei einen Teil davon verbraucht Es. Der Zusammenhang scheint darin zu bestehen, dass, bevor die JDBC-Stream-Daten verbraucht werden können, wenn die Pufferdaten voll sind, der Thread von MySQL, der Daten sendet, blockiert wird, wodurch ein Gleichgewicht gewährleistet wird (zu diesem Zweck können Sie Javas Socket verwenden, um dies zu versuchen). der Fall unten).

Für den JDBC-Client werden die Daten jedes Mal im lokalen Kernel-Puffer abgerufen, der nur einen Katzensprung vom Express-Paketkasten in der Community entfernt ist, sodass er natürlich jedes Mal teurer ist als der RT Der Supermarkt ist viel kleiner, und bei diesem Prozess handelt es sich um vorbereitete Daten, sodass es keinen E/A-Blockierungsprozess gibt (es sei denn, die vom MySQL-Server übergebenen Daten sind nicht so schnell wie der Verbraucher, der die Daten verarbeitet, dann tut dies im Allgemeinen nur der Verbraucher Dies geschieht nur, wenn der Testcode die Daten direkt aufgibt. Unabhängig davon, ob es sich um Computerräume, Regionen oder Länder handelt, werden die Daten kontinuierlich weitergeleitet, solange der Server zu reagieren beginnt vorbei, und diese Aktion ist sogar die erste. Die Methode ist auch ein Prozess, den man erleben muss.

Im Vergleich zur ersten Methode verursacht JDBC bei Verwendung keinen Speicherüberlauf. Selbst wenn eine große Tabelle ohne Speicherüberlauf gelesen wird, dauert es lange, bis sie reagiert. Diese Methode ist jedoch genauer als die Methode 1. Die Auswirkungen auf die Datenbank sind relativ groß. Die entsprechenden Datenzeilen werden gesperrt (um Änderungen zu verhindern). Bei Verwendung von MyISAM werden vollständige Tabellensperren hinzugefügt Geschäftsbehinderung.

[Theoretisch kannst du noch weiter gehen, solange du dazu bereit bist]

Theoretisch ist diese Methode besser, aber in Sachen Perfektionismus können wir weitermachen. Lass uns darüber reden Für faule Leute haben wir nicht einmal die Motivation, zum Express-Paketkasten unten in der Gemeinde zu gehen, um es zu holen, wenn jemand es zu mir nach Hause bringt und es mir in den Mund steckt Mein Mund. Es wäre großartig, wenn ich ihn auseinanderbrechen würde.

Theoretisch ist dies tatsächlich möglich, da JDBC Zeit benötigt, um den Speicher vom Kernel nach Java zu kopieren. Wenn eine andere Person dies tut, funktioniert es nicht, während ich zu Hause andere Dinge erledige Wenn ich es brauche, komme ich direkt von zu Hause. Das spart Zeit. Jeder Fehler wird zwar für Sie gespeichert, aber die Frage ist, wer ihn verschickt?

Dazu muss dem Programm ein Thread hinzugefügt werden, die Kerneldaten in den Anwendungsspeicher kopiert und sogar in Datenzeilen analysiert werden, damit die Anwendung sie direkt verwenden kann. Aber ist das unbedingt perfekt? Tatsächlich gibt es dazwischen ein Koordinationsproblem: Wenn Sie beispielsweise zu Hause kochen möchten und Ihnen eine Packung Gewürze fehlt, hätten Sie sie unten selbst kaufen können, müssen aber jemanden bitten, sie zu liefern Wenn zu diesem Zeitpunkt alle anderen Gerichte fertig sind und nur noch eine Packung Gewürze übrig ist, bleibt Ihnen nichts anderes übrig, als auf die Lieferung der Gewürze nach Hause zu warten, bevor Sie mit der Zubereitung fortfahren können nächster Schritt des Kochens. Unter idealen Umständen kann daher viel Zeit beim Kopieren des Speichers eingespart werden, es erhöht jedoch den Aufwand für die Koordinationssperre.

Ist es also möglich, Daten direkt aus dem Kernel-Puffer zu lesen?

Theoretisch ist es möglich, bevor wir dieses Problem erklären, wollen wir zunächst verstehen, was es außer dieser Speicherkopie noch gibt:

JDBC liest die Daten aus dem Kernel-Puffer im Binärformat Beim Abrufen wird es weiter in spezifische strukturierte Daten analysiert, da zu diesem Zeitpunkt die strukturierten Daten der spezifischen Zeilen des ResultSets an die Geschäftspartei zurückgegeben werden müssen, dh es muss eine Kopie der von RowData generierten Daten vorhanden sein. und JDBC gibt bestimmte Objekttypen zurück (z. B. Byte []-Array), in der Implementierung einiger Szenarien möchte es nicht, dass Sie den Inhalt von Byte [] im zurückgegebenen Ergebnis (Byte [1]) ändern ] = 0xFF) durch den Ergebnissatz, um den Inhalt des ResultSet selbst zu ändern. Während der Verwendung von Geschäftscode werden auch Zeichenfolgen, Netzwerkausgaben usw. erstellt Diese sind auf geschäftlicher Ebene unvermeidbar, aber es ist einfach trivial, also haben wir es nicht gemacht, da wir dachten, dass es im Großen und Ganzen trivial ist, es sei denn, der Engpass Ihres Programms liegt hier .

Daher ist das Kopieren des Speichers im Großen und Ganzen unvermeidlich, und der Overhead ist technisch gesehen größer. Wir können dies direkt aus dem Kernel-Status tun ; aber zu diesem Zeitpunkt müssen die Daten nach Bytes aus dem Puffer entfernt werden, damit mehr Daten aus der Ferne übertragen werden können. Es gibt keinen dritten Speicherort, an dem der Puffer gespeichert werden kann, da er sonst vom Kernel in den Speicher zurückkehrt die Anwendung.

Relativ gesehen kann der Server Daten direkt über direkte E/A optimieren und übertragen (aber das Datenprotokoll stimmt auf diese Weise mit dem Datenspeicherformat überein, was offensichtlich nur theoretisch ist Um ein benutzerdefiniertes Protokoll wirklich zu implementieren). Um Daten direkt über den Kernelstatus zu senden, müssen Sie das Dateisystemprotokoll auf Betriebssystemebene ändern, um den Zweck der Konvertierung zu erreichen.


Das obige ist der detaillierte Inhalt vonEine kurze Diskussion über das Kommunikationsprinzip von MySQL JDBC StreamResult. 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