この記事では、MySQL JDBC StreamResult の通信原理について簡単に説明します。必要な方は参考にしていただければ幸いです。
MySQL JDBC を使用して大量のデータ (たとえば、1GB 以上) を読み取ったことがある人は、読み取り時にメモリが Java ヒープでオーバーフローする可能性が高いことを知っているはずです。その解決策はステートメントです。 .setFetchSize(Integer.MIN_VALUE) を使用し、カーソルが読み取り専用で前方にスクロールすることを確認します (カーソルのデフォルト値)。タイプを com.mysql.jdbc.StatementImpl にキャストしてから、その内部メソッド enableStreamingResults を呼び出すこともできます。 () この方法では、データ メモリの読み取りがハングすることはなく、この 2 つによって達成される効果は同じです。もちろん useCursorFetch も使用できますが、このメソッドのテスト結果のパフォーマンスは StreamResult のパフォーマンスよりもはるかに遅いのはなぜでしょうか。この記事ではその一般原則について説明します。
MySQL JDBC の内部処理コードが 3 つのクラスに分割されて完成していることは、これまでの記事や書籍で紹介しましたが、データベースと JDBC の関係はどのようになっているのかまでは掘り下げたことがありませんでした。彼らの間のプロセスは?しばらくの間、これはサーバー側の動作、またはクライアントとサーバー間の連携動作であると考えてきましたが、今日はそうではなく、この動作が何であるかについて説明します。
[最初に簡単な通信を確認してください]:
JDBC とデータベース間の通信は Socket を通じて完了するため、データベースを SocketServer プロバイダー Square として扱うことができます。 SocketServer がデータを返す場合 (SQL 結果セットの返しと同様)、プロセスは次のようになります。 サーバー プログラム データ (データベース) -> カーネル ソケット バッファ -> クライアント ソケット バッファ -> クライアント エンド プログラム (JVM メモリ) JDBC の場所)
これまで、IT 業界で誰もが目にしたことのある JDBC は、MySQL JDBC、SQL Server JDBC、PG JDBC、Oracle JDBC です。 NoSQL クライアント: Redis クライアント、MongoDB クライアント、Memcached の場合でも、データは基本的に同じロジックに従います。
[デフォルトで MySQL JDBC を使用してデータを直接読み取るとハングするのはなぜですか? ]
(1) MySQL Server によって開始された SQL 結果セットはすべて、OutputStream を通じてデータを出力します。つまり、データはローカル Kennel に対応するソケット バッファーに書き込まれます。これはメモリ コピーです。コピーはこの記事の焦点ではありません)。
(2) このとき、ケンネルのバッファにデータがある場合、TCP リンク (JDBC によってアクティブに開始されるソケット リンク) を介してデータが返送されます。 JDBC が配置されているマシンでは、最初に Kennel エリアに入り、さらに Buffer エリアに入ります。
(3) JDBC が SQL 操作を開始した後、Java コードは inputStream.read() 操作をブロックします。バッファーにデータがある場合、JDBC が起動されて、バッファー内のデータを読み取ります。 Java メモリにとって、これは JDBC 側のメモリ コピーです。
(4) 次に、MySQL JDBC は引き続きバッファ データを Java メモリに読み取り、MySQL Server はデータの送信を続けます。データが完全に組み立てられる前に、クライアントによって開始された SQL 操作は応答しないことに注意してください。これは、実際には、データがローカルに送信され、JDBC が応答しているように感じられることを意味します。最初のデータ部分が実行メソッドが呼び出された場所にまだ結果セットを返していませんが、バッファからデータを継続的に読み取ります。
(5) 重要なのは、この愚か者は、テーブル全体が自宅に保存されているかどうかに関係なく、テーブル全体の内容を Java メモリに読み込むことです。次のステップは次のステップです。メモリオーバーフロー。
[JDBC パラメータで useCursorFetch=true を設定すると問題を解決できます]
このソリューションは実際に FetchSize 設定の問題を解決できます。このソリューションは実際に MySQL に指示します。必要なサーバー データ量、毎回必要なデータ量、通信プロセスは次のようになります:
これは単なるものです。私たちの生活と同じように、私に必要なのは、スーパーに行って、必要なだけ買うことです。ただし、この種のインタラクションは、自宅に居ながらにして自宅に届けられるものとは異なります。つまり、データが 1 億件ある場合、ネットワーク時間のオーバーヘッドが必要になります。 , FetchSize を 1000 に設定すると、往復の通信が 100,000 回になります。同じコンピュータ ルーム内のネットワーク遅延が 0.02ms の場合、100,000 回の通信で 2 秒追加されますが、これは大したことではありません。したがって、コンピューター室全体の遅延時間が 2ms であれば、遅延時間は 200 秒長くなります (つまり、3 分 20 秒)。中国の都市全体の遅延時間が 10 ~ 40 ミリ秒であれば、時間は 1000 ~ 4000 秒になります。国を超えて 200 ~ 300 ミリ秒の場合はどうなるでしょうか?時間は10時間以上長くなります。
ここでの計算には、システム コールの増加、スレッドが待機およびウェイクアップするコンテキストの増加、および全体的なパフォーマンスに対するネットワーク パケットの再送信の影響は含まれていません。そのため、このソリューションは、それは妥当なようですが、確かにパフォーマンスはあまり良くありません。
さらに、MySQL はクライアントがデータの消費をいつ終了するかを認識せず、対応するテーブルに DML 書き込み操作がある可能性があるため、MySQL は削除する必要があるデータを保存するための一時テーブル スペースを作成する必要があります。したがって、useCursorFetch を有効にして大きなテーブルを読み取ると、MySQL でいくつかの現象が発生します。
(1) 通常のハードディスクの場合、大量の IO 読み取りがあるため、IOPS が急増します。ビジネス文書作成にジッターが発生する可能性があります。
(2) この一時領域がデータベース全体で大きな割合を占めると、データベースのディスク書き込みが発生する可能性があります。いっぱいの場合、結果セットが読み取られた後、またはクライアントが Result.close() を開始したときに、スペースは MySQL によって再利用されます。
(3) CPU とメモリは、CPU の能力によって決まる一定の割合で増加します。
(4) クライアント JDBC は SQL を開始した後、SQL 応答データを長時間待機します。この間、サーバーはデータを準備します。この待機は、何も設定しない本来の JDBC の方法とは異なります。内部原理は異なりますが、前者はネットワーク バッファからデータを読み取り続け、MySQL データベースは一時的なデータ領域を準備しており、JDBC に応答しません。
[データの読み取りストリーム]
最初の方法は Java をハングさせる原因となり、2 番目の方法は非効率で MySQL データベースに大きな影響を与えることがわかっています。 、クライアントの応答も遅い場合は、問題を解決することしかできないため、次にストリームの読み取りメソッドを見てみましょう。
前述したように、statement.setFetchSize(Integer.MIN_VALUE) または com.mysql.jdbc.StatementImpl.enableStreamingResults() を使用すると、開始前に FetchSize を使用して結果セットを読み取ることができなくなります。手動で設定し、カーソルが FORWARD_ONLY であることを確認してください。
この方法は素晴らしいです。メモリがハングすることはなくなり、応答も速くなり、少なくとも IOPS は大きくならず、ディスク使用量も少なくなるそうです。消えてしまいます。以前は、JDBC で個別のコードしか見ていなかったので、それが MySQL と JDBC の間の別の通信プロトコルだと思っていましたが、それが「クライアントの動作」であることが判明したとは、まったく知りませんでした。そう、それはクライアントです。行動。
enableStreamingResults() を開始するとき、サーバーとの対話はほとんど行われません。つまり、サーバーはメソッド 1 に従ってデータを返し、その後サーバーはデータをバッファにプッシュします。クライアントはどうやってプレッシャーに耐えますか?
JDBC では、ストリーム結果セット処理を有効にしても、すべてのデータが Java メモリに一度に読み込まれるわけではありません。つまり、図 1 のデータは一度に読み取られません。Java バッファは 1 つのパッケージを読み取ります。 (このパッケージは Java の byte[] 配列として理解できます)。一度に多くのデータを読み取ることができ、データの整合性を確保するために下方への読み取りを続けるかどうかを確認します。ビジネス コードはバイトに基づいて行に解析され、ビジネス側で使用されます。
サーバーはバッファーへのデータのプッシュを開始し、両側のバッファーがいっぱいになると、データがクライアントのカーネル バッファーをいっぱいにします。データが受信者に送信されると、この時点でコンシューマのバッファもいっぱいになるため、送信者のスレッドはブロックされ、相手がデータの一部を消費するのを待ちます。相手がデータの一部を消費する場合は、そのデータの一部を受信者にプッシュすることができます。それ。この接続は、JDBC ストリーム データが消費される前に、バッファ データがいっぱいの場合、データを送信する MySQL のスレッドがブロックされ、バランスが確保されるようです (このために、Java のソケットを使用して試すことができます)以下の場合はこれに当てはまりますか)。
JDBC クライアントの場合、データは毎回、コミュニティ内の宅配ボックスから少し離れたローカル カーネル バッファーで取得されるため、当然のことながら、配送されるたびに RT よりもコストが高くなります。スーパーマーケットの場合ははるかに小さく、このプロセスは準備されたデータであるため、IO ブロック プロセスはありません (MySQL サーバーによって渡されるデータがデータを処理するコンシューマほど高速でない場合、通常はコンシューマのみが実行しません)。これは、テスト コードがデータを直接破棄した場合にのみ発生します)。現時点では、コンピューター ルーム間、地域間、国間を問わず、サーバーが応答を開始する限り、データは継続的に配信されます。 、そしてこのアクションは最初のタイプでもあり、その方法は経験する必要があるプロセスでもあります。
最初の方法と比較すると、JDBC を使用するとメモリ オーバーフローが発生しませんが、メモリ オーバーフローを起こさずに大きなテーブルを読み込んだ場合でも、応答に時間がかかります。 1. データの転送プロセス中に、対応するデータ行がロックされます (変更を防ぐため)。一方、MyISAM を使用すると、テーブル全体のロックが追加されます。ビジネスブロック。
[理論的には、その気になればさらに進めることができます]
理論的にはこの方法の方が優れていますが、完璧主義の観点からは、続けることができます。怠け者にとって、私たちが考えているのは、誰かがそれを私の家に持ってきて、たとえそれを私の口に入れたかということです。私の口を分解できたら素晴らしいでしょう。
技術的には、これは確かに理論上は可能ですが、JDBC がカーネルから Java にメモリをコピーするのに時間がかかるため、別の人がこれを行うと、私が家で他のことをしている間は機能しません。必要なときは家から直接来ますので時間の節約になります。すべての間違いは確かに保存されますが、問題は誰がそれを送信するかです。
これを行うには、プログラムにスレッドを追加し、カーネル データをアプリケーション メモリにコピーし、アプリケーションが直接使用できるようにデータ行に解析する必要がありますが、これは必ずしも完璧でしょうか?実際、途中で調整の問題が発生します。たとえば、家で料理をしたいときに調味料のパックが足りない場合、階下で自分で購入することもできますが、誰かに届けてもらう必要があります。この時点で他の料理はすべて調理されており、調味料が 1 袋だけ残っている場合は、調味料が自宅に届くのを待つしかありません。料理の次のステップ。したがって、理想的な状況では、メモリのコピー時間を大幅に節約できますが、調整ロックのオーバーヘッドがいくらか増加します。
では、カーネル バッファから直接データを読み取ることは可能でしょうか?
理論的には可能です。この問題を説明する前に、まずこのメモリ コピー以外に何が存在するかを理解しましょう。
JDBC はカーネル バッファからバイナリ形式でデータを読み取ります。取得の際、さらに特定の構造化データに解析されます。この時点では、ResultSet の特定の行の構造化データをビジネス パーティに返す必要があるためです。つまり、RowData によって生成されたデータのコピーが存在する必要があります。また、JDBC は特定のオブジェクト タイプ (バイト [] 配列など) を返します。一部のシナリオの実装では、返された結果 (バイト [1) のバイト [] の内容を変更する必要はありません。 ] = 0xFF) を使用して、ResultSet 自体の内容を変更することにより、別のメモリ コピーが作成され、文字列、ネットワーク出力なども作成されます。これらはビジネスレベルでは避けられないものですが、この小さなコピーに比べれば、それは単純です。そのため、プログラムのボトルネックがここにある場合を除き、全体的にはほとんど簡単ではないと考えて、これを実行しませんでした。
したがって、全体的な観点から見ると、メモリのコピーは避けられませんが、今回はシステムレベルの呼び出しにすぎず、技術的には、カーネルから直接実行できるオーバーヘッドが大きくなります。データの読み取り中ですが、この時点では、より多くのリモート データを転送できるように、バッファからデータをバイト単位で取り出す必要があります。そうしないと、バッファからメモリ コピーに戻ります。カーネルからアプリケーションへ。
相対的に言えば、サーバーは直接 IO を通じてデータの送信を直接最適化できます (ただし、この方法でのデータ プロトコルはデータ ストレージ形式と一致しており、これは明らかに理論上のものにすぎません)。プロトコルを変更し、カーネル状態を通じてデータを直接送信する場合、変換の目的を達成するには、OS レベルのファイル システム プロトコルを変更する必要があります。
以上がMySQL JDBC StreamResult の通信原理に関する簡単な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。