迷わないように注意してください。Java関連のテクノロジーと情報の更新を続けてください。!!
内容はグループの友達の貢献によるものです!ご支援ありがとうございます!
シャオZは最近ダブルミスを犯した
先ほど、小さなz社の本番トランザクションでエラーが報告されることがありましたが、調査の結果、Redisコマンドの実行がタイムアウトしたことが原因でした。
しかし、困惑しているのは、プロダクショントランザクションは単純なコマンドRedisセットのみを使用することです。これは合理的であり、非常にゆっくり実行することは不可能です。
それで、この問題の原因は何ですか?
この問題を見つけるために、Redisの最近の遅いログをチェックして分析し、最終的に、時間のかかるコマンドがキーXX *であることを発見しました
Xiao Zは、このコマンドで操作されるキーのプレフィックスを見て、これが彼が担当したアプリケーションであることに気付きました。しかし、Xiaozはそれをチェックアウトし、彼のコードはkeysコマンドを積極的に使用していませんでしたが、基礎となるフレームワークは間接的にそれを使用していたため、今日の問題がありました。
問題の原因
Xiaozが担当するアプリケーションは管理バックグラウンドアプリケーションです。権限管理はShiroフレームワークを使用します。複数のノードがあるため、分散セッションを使用する必要があるため、ここにセッション情報を格納するためにRedisが使用されます。
Shiroはセッションコンポーネントを格納するRedisを直接提供していなかったため、Xiaozはオープンソースコンポーネントshiro-redisであるGithubを使用する必要がありました。
Shiroフレームワークはセッションが有効かどうかを定期的に確認する必要があるため、Shiroの最下層はSessionDAO#getActiveSessionsを呼び出してすべてのセッション情報を取得します。
Shiro-redisはSessionDAOインターフェースを継承するだけで、最下層はkeysコマンドを使用してRedisに保存されているすべてのセッションキーを検索します。
public Set<byte[]> keys(byte[] pattern){
checkAndInit();
Set<byte[]> keys = null;
Jedis jedis = jedisPool.getResource();
try{
keys = jedis.keys(pattern);
}finally{
jedis.close();
}
return keys;
}
問題の原因を見つけます。解決策は比較的簡単です。githubで解決策を見つけ、shiro-redisを最新バージョンにアップグレードします。
このバージョンでは、shiro-redisはキーの代わりにscanコマンドを使用してこの問題を修正しています。
public Set<byte[]> keys(byte[] pattern) {
Set<byte[]> keys = null;
Jedis jedis = jedisPool.getResource();
try{
keys = new HashSet<byte[]>();
ScanParams params = new ScanParams();
params.count(count);
params.match(pattern);
byte[] cursor = ScanParams.SCAN_POINTER_START_BINARY;
ScanResult<byte[]> scanResult;
do{
scanResult = jedis.scan(cursor,params);
keys.addAll(scanResult.getResult());
cursor = scanResult.getCursorAsBytes();
}while(scanResult.getStringCursor().compareTo(ScanParams.SCAN_POINTER_START) > 0);
}finally{
jedis.close();
}
return keys;
}
問題は正常に解決されましたが、Xiao Zはまだ少し戸惑いました。
- keysコマンドが他のコマンドの実行を遅くするのはなぜですか?
- Keysコマンドのクエリが非常に遅いのはなぜですか?
- Scanコマンドに問題がないのはなぜですか?
Redisがコマンドを実行する原理
まず、最初の質問を見てみましょう。keysコマンドで他のコマンドの実行が遅くなるのはなぜですか。
この質問に答えるために、まずRedisクライアントでのコマンドの実行を見てみましょう。
クライアントの観点から見ると、コマンドの実行は3つのステップに分かれています。
- 送信コマンド
- 注文の実行
- 結果を返す
しかし、これはクライアント自体が考えるプロセスにすぎませんが、実際には、同時に、多くのクライアントがRedisにコマンドを送信している可能性があり、Redisは皆、シングルスレッドモデルを使用していることを知っています。
すべてのクライアント要求を同時に処理するために、Redisは内部的にキューメソッドを使用して実行のためにキューに入れます。
したがって、クライアントは実際にコマンドを実行するために4つのステップを必要とします。
- 送信コマンド
- コマンドキュー
- 注文の実行
- 結果を返す
Redisはコマンドを単一のスレッドで実行するため、タスクはキューから順番にしか実行できません。
このプロセスのコマンド実行速度が遅すぎる限り、キュー内の他のタスクは待機する必要があります。これは、Redisがブロックされ、応答が受信されていないかのように外部クライアントに見えます。
したがって、Redisを使用する場合は、長時間実行が必要な命令を実行しないでください。これにより、Redisがブロックされ、他の命令の実行に影響を与える可能性があります。
KEYSの原則
次に、2番目の質問への回答を開始します。なぜ、Keysコマンドのクエリが非常に遅いのですか?
Redisは、Java HashMapの最下層に似た最下層の辞書構造を使用します。
keysコマンドは、指定されたパターンに一致するすべてのRedisキーを返す必要があります。これを実現するには、Redisがディクショナリ内のht [0]ハッシュテーブルの基になる配列をトラバースする必要があります。この時間の複雑さは "O(N)"(N Redisのキーの数です)。
Redisのキーの数が少ない場合でも、実行速度は速くなります。Redisキーの数が徐々に増加し、数百万、数千万、さらには数億のレベルに達すると、実行速度が非常に遅くなります。
以下はXiaozがローカルで行った実験です。luaスクリプトを使用して10WキーをRedisに追加し、キーを使用してすべてのキーをクエリすると、このクエリはおそらく10秒以上ブロックします。
eval "for i=1,100000 do redis.call('set',i,i+1) end" 0
ここでXiaozはDockerを使用してRedisをデプロイしているため、パフォーマンスがわずかに低下する可能性があります。
SCANの原理
最後に、3番目の質問を見てみましょう。スキャンコマンドに問題がないのはなぜですか。
これは、scanコマンドが黒いテクノロジ「カーソルベースのイテレータ」を使用しているためです。
scanコマンドが呼び出されるたびに、Redisは新しいカーソルと特定の数のキーをユーザーに返します。残りのキーを引き続き取得する場合は、このカーソルをスキャンコマンドに渡して、前の反復処理を続行する必要があります。
簡単に言えば、scanコマンドはページングを使用してredisをクエリします。
以下は、scanコマンドの反復プロセスの例です。
scanコマンドは、カーソルを使用して巧妙に完全なクエリを複数回に分割し、クエリの複雑さを軽減します。
スキャンコマンドの時間の複雑さはキーのそれと同じですが、どちらも「O(N)」ですが、スキャンコマンドは少数のキーを返すだけでよいため、実行速度は非常に速くなります。
最後に、scanコマンドはキーの不足を解決しますが、他の欠陥ももたらします。
- 同じ要素が複数回返される場合があるため、アプリケーションで繰り返し要素を処理する機能を強化する必要があります。
- 反復処理中に要素がredisに追加された場合、または反復処理中に削除された場合、その要素は返されるか、返されない場合があります。
これらの欠陥は、開発時に考慮する必要があります。
スキャンに加えて、redisにはインクリメンタル反復のための他のいくつかのコマンドがあります。
- sscan:現在のデータベースのデータベースキーを反復するために使用され、smembersの可能なブロッキング問題を解決するために使用されます
- hscanコマンドを使用して、ハッシュキーのキーと値のペアを反復処理し、hgetallのブロッキングの問題を解決します。
- zscan:コマンドは、順序付けされたセット内の要素(要素メンバーと要素スコアを含む)を反復するために使用され、ブロッキングの問題を引き起こす可能性があるzrangeを生成するために使用されます。
総括する
Redisは単一のスレッドを使用して操作コマンドを実行します。すべてのクライアントがコマンドを送信し、Redisはそれらをキューに入れ、対応するコマンドを順番にキューから取り出します。
タスクの実行速度が遅すぎると、キュー内の他のタスクに影響を与えるため、外部クライアントから見ると、Redisからの応答を取得する際の遅延がブロックされているように見えます。
したがって、本番環境ではキー、smembers、hgetall、zrangeなどのブロッキングを引き起こす可能性のある命令を実行しないでください。それらを実際に実行する必要がある場合は、対応するスキャンコマンドを使用して段階的に走査することで、ブロッキングの問題を効果的に防止できます。
上記のJavaインタビュー資料を受け取る必要がある場合は、ここをクリックして無料で受け取ることができます。
上記のJavaインタビュー資料を受け取る必要がある場合は、ここをクリックして無料で受け取ることができます。