本記事では、大手企業の面接ではほぼ必須となるRedisの分散ロックについて主に解説します。以下では、注文取得をシミュレートするシナリオと組み合わせて使用します。この記事では、redis 環境の構築は行わず、個人用のテスト環境を素早く構築します。ここでは docker を使用することをお勧めします。この記事のコンテンツノードは次のとおりです。 ジェディス NX ジェネレーション ロック - ロックを解除する方法
- 注文獲得アクションをシミュレートする(10万人が注文を獲得)
- ジェディス NX ジェネレーション ロック
Java で Redis を操作する場合、最善の方法は jedis を使用することです。まず、pom に依存関係を導入します。 - <依存関係>
- <グループID> redis.clients</グループID>
- <artifactId>ジェディス</artifactId>
- </依存関係>
分散ロックを生成するときは、次の点に注意する必要があります。 - ロックを作成するための戦略: Redis の通常のキーは通常、上書きが許可されます。ユーザー A がキーを設定すると、B も同じキーの設定に成功します。ロック シナリオの場合、どのユーザーがキーを正常に設定したかを知ることは不可能です。ここでは、jedis の setnx メソッドがこの問題を解決します。単純な原理は、ユーザー A が最初にキーを正常に設定し、次にユーザー B がキーを設定すると失敗が返され、特定の時点でロックを取得できるのは 1 人のユーザーのみであるという要件が満たされるというものです。
- ロックの有効期限:ラッシュ購入のシナリオでは、有効期限の概念がない場合、ユーザー A がロックを生成したが、後続のプロセスがブロックされてロックを解除できない場合、他のユーザーはこの時点でロックを取得できず、ラッシュ購入アクティビティを完了できません。もちろん、通常の状況ではブロックは発生せず、ユーザー A のプロセスは正常にロックを解除します。有効期限はセキュリティを強化するためのものです。
setnx 操作のコードは次のとおりです。 - パブリックブールsetnx(文字列キー、文字列値) {
- ジェディスジェディス= null ;
- 試す {
- jedis = jedisPool .getResource();
- if ( jedis == null ) {
- false を返します。
- }
- jedis.set(key, val, "NX", "PX", 1000 * 60) を返します。
- equalsIgnoreCase("ok");
- } catch (例外例) {
- ついに
- if (jedis != null) {
- jedis.close();
- }
- }
- false を返します。
- }
ここで注目すべき点は、jedis の set メソッドであり、そのパラメータの説明は次のとおりです。 - NX: 鍵は存在しますか?そうなる場合、セットは失敗します。
- PX: キーの有効期限の単位はミリ秒に設定されています (例: 単位は秒)
setnx が失敗した場合は、カプセル化して false を返します。次に、get API を介して setnx メソッドを呼び出します。 - @GetMapping("/setnx/{キー}/{値}")
- public boolean setnx(@PathVariable String key, @PathVariable String val) {
- jedisCom.setnx(key, val) を返します。
- }
次のテスト URL にアクセスしてください。通常、1 回目は true が返され、2 回目は false が返されます。 2 番目のリクエストが行われたときに redis キーがすでに存在するため、設定は成功しません。 上の図から、1 つのセットのみが成功し、キーに有効な時間があり、その時点で分散ロックの条件が満たされていることがわかります。 ロックを解除する方法 上記はロックを作成するためのもので、ロックにも有効時間がありますが、この有効時間に完全に依存することはできません。たとえば、有効時間は 1 分に設定されます。ユーザー A はロックを取得した後、特別な状況に遭遇することなく通常どおりにラッシュ オーダーを生成します。この時点で、他のユーザーは正常に注文できるはずです。ただし、ロックは 1 分後にのみ自動的に解除されるため、この 1 分間は他のユーザーは通常どおり注文を行うことができません (ロックはまだユーザー A に属しているため)。したがって、操作後にユーザー A が積極的にロックを解除する必要があります。 - パブリック int delnx(文字列キー、文字列値) {
- ジェディスジェディス= null ;
- 試す {
- jedis = jedisPool .getResource();
- if ( jedis == null ) {
- 0を返します。
- }
- //redis.call('get','orderkey')=='1111' の場合は redis.call('del','orderkey') を返し、それ以外の場合は 0 を返します。終了
- StringBuilder sbScript =新しいStringBuilder();
- sbScript.append("if redis.call('get','").append(key).append("')").append("=='").append(val).append("'").
- append(" then ") を実行します。
- append(" return redis.call('del','").append(key).append("')").
- 追加(" else ")。
- append(" return 0").
- 追加("終了");
- Integer.valueOf(jedis.eval(sbScript.toString()).toString()) を返します。
- } catch (例外例) {
- ついに
- if (jedis != null) {
- jedis.close();
- }
- }
- 0を返します。
- }
ここでも、jedis メソッドを使用して Lua スクリプトを直接実行します。val に基づいてスクリプトが存在するかどうかを判断し、存在する場合は del を実行します。 実際、Jedis の get メソッドを通じて val を取得し、値を比較して現在ロックを保持しているのがユーザーであるかどうかを確認し、そうであれば最後にそれを削除すると、効果は実際には同等になると思います。スクリプトが eval を通じて直接実行されるため、Redis をもう一度操作する必要がなくなり、アトミック操作の間隔が短縮されます。 (異なる意見がある場合は、メッセージを残して話し合いましょう)テスト用の get メソッド API も作成します。 - @GetMapping("/delnx/{キー}/{値}")
- public int delnx(@PathVariable String key, @PathVariable String val) {
- jedisCom.delnx(キー、val) を返します。
- }
delnx を使用する場合、et の値と delnx の値はロックを保持する操作要求であるかどうかを判断するために使用されるため、ロックの作成に使用される値を渡す必要があることに注意してください。値が同じ場合のみ del が許可されます。 注文獲得アクションをシミュレートする(10万人が注文を獲得) 上記の分散ロックの大まかな基礎に基づいて、100,000 人が注文を競うシナリオをシミュレートします。実際のところ、これは単なる同時操作要求です。環境が限られているため、この方法でしかテストできません。次のコードに示すように、100,000 人のユーザーを初期化し、在庫、製品、およびその他の情報を初期化します。 - //総在庫数
- プライベートlong nKuCuen = 0 ;
- //プロダクトキー名
- プライベート文字列shangpingKey = "computer_key" ;
- //ロックタイムアウトを秒単位で取得する
- プライベートintタイムアウト= 30 * 1000;
- @GetMapping("/強丹")
- パブリックリスト<文字列> qiangdan() {
- //商品を手にしたユーザー
- リスト<文字列> shopUsers =新しいArrayList < > ();
- //多数のユーザーを構築する
- リスト<文字列> users =新しいArrayList < > ();
- IntStream.range(0, 100000).parallel().forEach(b - > {
- users.add("Shenniu-" + b);
- });
- // インベントリを初期化する
- 10 ;
- //つかむ動作をシミュレートする
- users.parallelStream().forEach(b - > {
- 文字列shopUser = qiang (b);
- StringUtils.isEmpty(shopUser) の場合 {
- shopUsers を追加します。
- }
- });
- shopUsers を返します。
- }
上記の 100,000 人の異なるユーザーを対象に、商品の在庫を 10 個だけに設定し、並列ストリームを通じてラッシュ購入をシミュレートします。急ぎ購入は以下のように実施されます。 - /**
- * 注文獲得アクションをシミュレートする
- *
- * @パラメータ b
- * @戻る
- */
- プライベート文字列 qiang(文字列 b) {
- //ユーザーが時間を取得し始める
- 長いstartTime = System .currentTimeMillis();
- //ロックが取得されていない場合は、30秒以内にロックの取得を続行します
- ((開始時間 + タイムアウト) > = System.currentTimeMillis()) の間 {
- //製品が残っているかどうか
- nKuCuen < = 0の場合{
- 壊す;
- }
- jedisCom.setnx(shangpingKey, b) の場合 {
- //ユーザーbがロックを取得する
- logger.info("ユーザー {} がロックを取得しました...", b);
- 試す {
- //製品が残っているかどうか
- nKuCuen < = 0の場合{
- 壊す;
- }
- // 時間のかかる注文生成操作をシミュレートして簡単に表示: Shenniu-50 複数取得ロック記録
- 試す {
- 時間単位.SECONDS.スリープ(1);
- } キャッチ (InterruptedException e) {
- e.printStackTrace();
- }
- // 購入成功、商品減少、ユーザー記録
- nKuCuen - = 1 ;
- // 注文の取得に成功して終了
- logger.info("ユーザー {} は注文を正常に取得して飛び出しました...残りの在庫: {}", b, nKuCuen);
- return b + "注文を正常に取得しました。残りの在庫:" + nKuCuen;
- ついに
- logger.info("ユーザー {} がロックを解除しました...", b);
- //ロックを解除する
- jedisCom.delnx(shangpingKey, b);
- }
- } それ以外 {
- // ユーザー B はロックを取得せず、タイムアウト範囲内でロックを要求し続けたため、処理は必要ありません
- // if (b.equals("神牛-50") || b.equals("神牛-69")) {
- // logger.info("ユーザー {} がロックの取得を待機しています...", b);
- // }
- }
- }
- 戻る "";
- }
ここで実装されているロジックは次のとおりです。 - parallelStream(): 並列ストリームは複数ユーザーの突発購入をシミュレートします
- (startTime + timeout) >= System.currentTimeMillis(): ロックの取得に失敗したユーザーを特定し、タイムアウト秒以内にロックの取得を続行します。
- ロックを取得する前と取得後に、在庫が十分かどうかを確認します。
- jedisCom.setnx(shangpingKey, b): ユーザーはスナップアップロックを取得します
- ロックを取得して注文が正常に行われた後、ロックは解除されます: jedisCom.delnx(shangpingKey, b)
記録されたログの結果を見てみましょう。 最後に、製品を正常に購入したユーザーが返されます。 |