Redis - 12. アプリケーションの問題解決

1. キャッシュの侵入

1.1. 問題の説明

Redisキャッシュをシステムに導入すると、リクエストが来た後、まずredisキャッシュからクエリを実行し、キャッシュがあれば直接返し、キャッシュがなければ直接クエリを返します。データベース。一部のキーはデータベースに存在しないデータに対応しています。このキーに対するリクエストがキャッシュから取得できないたびに、そのリクエストはデータベースに押しつけられ、データベースに負荷がかかる可能性があります。

たとえば、存在しないユーザー ID を使用してユーザー情報を取得する場合、キャッシュやデータベースの有無に関係なく、ハッカーがこのような攻撃を大量に使用すると、データベースが圧倒される可能性があります。

1.2. 解決策

1.2.1. null 値のキャッシュ

クエリによって返されたデータが空の場合 (データベースが存在するかどうかに関係なく)、結果 (null) をキャッシュし、最長 5 分の短い有効期限を設定します。

1.2.2. アクセス可能なリスト(ホワイトリスト)の設定

Redis のビットマップ タイプを使用して、アクセス可能なリストを定義します。リスト ID は、ビットマップのオフセットとして使用されます。サンプル テキストがビットマップ内の ID と比較されるたびに、アクセスされた ID がビットマップにない場合は、傍受され、アクセスが許可されません。

1.2.3、ブルームフィルターを使用する

ブルーム フィルター (ブルーム フィルター) は、1970 年にブルームによって提案されました。実際には、非常に長いバイナリ ベクトル (ビットマップ) と一連のランダム マッピング関数 (ハッシュ関数) です。

ブルームフィルターは、要素がコレクションに含まれているかどうかを検出するために使用できますが、スペース効率とクエリ範囲が一般的なアルゴリズムをはるかに超えているという利点がありますが、一定の誤認識率と削除が困難であるという欠点があります。

考えられるすべてのデータを十分な大きさのビットマップにハッシュすると、存在してはいけないデータがこのビットマップによってインターセプトされるため、基盤となるストレージ システムに対するクエリの負荷が回避されます。

1.2.4、リアルタイム監視

Redis のヒット率が急激に低下し始めていることが判明した場合は、アクセス対象やアクセスされたデータを確認し、運用保守担当者と連携し、提供するサービスを制限するブラックリストを設定する必要があります。 :IPブラックリスト)

2. キャッシュの内訳

2.1. 問題の説明

redisのホットキー(アクセス量の多いキー)の有効期限が切れる この時、同時に大量のリクエストが来て、キャッシュにヒットしないことが分かり、これらのリクエストはすべてキャッシュ上でヒットします。 db への負荷が瞬間的に増加し、db が破壊される可能性があります。この状態はキャッシュ破壊と呼ばれます。

キャッシュが壊れる現象

  • データベースアクセス圧力の瞬間的な増加
  • Redis には多数のキーの有効期限がありません
  • Redisは正常に実行されています

2.2. 解決策

キーはある時点で高い同時実行性でアクセスされる可能性があり、非常に「ホット」なデータです。この時点では、キャッシュが「壊れている」という問題を考慮する必要があります。一般的な解決策は次のとおりです。続く

2.2.1. 人気のあるデータを事前に設定し、有効期限を適切に調整する

redis のピークの前に、事前に人気のあるデータを redis に保存し、これらの人気のあるデータをキャッシュで監視し、有効期限をリアルタイムで調整します。

2.2.2. ロックの使用

データがキャッシュ内で利用できない場合、データベース内ですぐにクエリを実行するのではなく、分散ロック (redis の setnx など) を取得し、ロックを取得してデータベースにデータをロードします。ロックがスリープ状態になる 一定期間後にデータをフェッチするメソッド全体を再試行します。

3. キャッシュなだれ

3.1. 問題の説明

キーに対応するデータは存在しますが、非常に短期間に大量のキーが期限切れになります。このとき、大量のリクエストが同時に来てキャッシュにデータが存在しない場合、大量のリクエストが発生します。データをロードするためにデータベースに落ち、データベースがクラッシュしてサービスがクラッシュします。

キャッシュなだれとキャッシュ ブレークダウンの違いは、前者は多数のキーが集中的に失効するのに対し、後者は特定のホット キーが失効することです。

3.2. 解決策

キャッシュが無効な場合、基盤となるシステムに対する雪崩効果の影響は非常に深刻であり、一般的な解決策は次のとおりです。

3.2.1. マルチレベルキャッシュの構築

nginx キャッシュ + redis キャッシュ + その他のキャッシュ (ehcache など)

3.2.2. ロックまたはキューの使用

ロックまたはキューを使用して、一度に多数のスレッドがデータベースの読み取りと書き込みを行わないようにします。これにより、基盤となるストレージ システムに障害が発生したときに、大量の同時リクエストがそのシステムに降りかかるのを避けることができますが、これは適切ではありません。同時実行性が高い状況向け。

3.2.3. キャッシュの有効期限を監視し、事前に更新する

キャッシュを監視し、キャッシュの有効期限が近づいていることを通知し、事前にキャッシュを更新します。

3.2.4. キャッシュ無効化時間の配布

たとえば、元の有効期限に基づいてランダムな値 (1 ~ 5 分など) を追加すると、キャッシュ有効期限の繰り返し率が減少し、集合的な失敗イベントがトリガーされにくくなります。

4. 分散ロック

4.1. 問題の説明

ビジネス開発のニーズに伴い、元の単一マシン展開システムが分散クラスタ システムに進化した後、分散システムはマルチスレッド、マルチプロセスであり、異なるマシンに分散されているため、同時実行制御がロックされます。元のスタンドアロン デプロイメントの状況 この戦略は失敗し、純粋な Java API は分散ロックの機能を提供できません。この問題を解決するには、共有リソースへのアクセスを制御する JVM 間の相互排他メカニズムが必要です。これが分散ロックの問題です。ロックを解決する必要があります。

4.2. 分散ロックの主流の実装

  1. データベースに基づいた分散ロックを実現
  2. キャッシュ(redisなど)に基づく
  3. 動物園の飼育員に基づいて

 各分散ロック ソリューションには独自の長所と短所があります。

  1. パフォーマンス: Redis が最高
  2. 信頼性: 飼育員が最も高い

ここでは、redis に基づいて分散ロックを実装します。

4.3. 解決策: Redis を使用して分散ロックを実装する

分散ロックを実装するには、次のコマンドを使用する必要があります。

set key value NX PX 有效期(毫秒)

このコマンドは、キーが存在しない場合、その値を value に設定し、同時にその有効期間を設定することを意味します。

set sku:1:info "ok" NX PX 10000

sku:1:info が存在しない場合、設定値は問題なく、有効期間は 10,000 ミリ秒であることを示します。

4.3.1. ロックのプロセス

処理の流れは以下の図の通りです set key value NX PX validity period (ms) コマンドを実行し、実行が成功したことを示す ok を返し、ロックの取得に成功します 複数のクライアントがこのコマンドを同時に実行すると、redis 1 つだけが正常に実行できることを保証できます。

4.3.2. 有効期限を設定する必要があるのはなぜですか?

クライアントがロックを取得した後、システムダウンなどのシステム障害によりロックが解除できなくなり、他のクライアントがロックを使用できなくなるため、ロックの有効期間を指定する必要があります。

4.3.3. 有効期間が短すぎる場合はどうすればよいですか?

例えば、有効期限を10秒に設定しているが、ビジネス側にとっては10秒では足りないため、クライアント側で寿命延長機能を実装する必要があり、この問題を解決できます。

4.3.4. ロックの誤削除の問題を解決する

ロックを誤って削除する場合があります。いわゆる誤って削除とは、他の人が保持しているロックを削除することです。

例えば、スレッド A がロックを取得する際、有効期限は 10 秒に設定されていますが、業務実行中にプログラム A が突然 10 秒以上スタックしてしまうと、スレッドなど他のスレッドによってロックが取得される可能性があります。 B. その後、Aはフリーズから復帰し、業務を継続します 業務実行後、ロックを解除する操作を実行します このとき、Aはdelコマンドを実行します このとき、ロックは解除されますが誤って削除され、その結果、B が保持しているロックが解放され、他のスレッドが再びロックを取得することになります。これは非常に深刻です。

どうやって解決すればいいでしょうか?

ロックを取得する前に、グローバルに一意の ID を生成し、この ID をキーに対応する値にスローします。ロックを解放する前に、Redis から ID を取得し、ローカルの ID と比較して、それが自分の ID であるかどうかを確認します。はい、del を実行してロックを解除します。

4.3.5. 誤って削除する可能性がまだあります (アトミック操作の問題)

前述の通り、delの前にまずredisからidを読み込んでローカルidと比較し、一致していればdeleteを実行します。

step1:判断 redis.get("key").id==本地id 是否相当,如果是则执行step2
step2:del key;

このとき、例えば手順2の実行時にシステムカード所有者が実行されると、カード所有者は10秒待ってからredisがそれを受信することになりますが、この間に他のスレッドによってロックが取得され、誤ってロックが取得されてしまう可能性があります。この時点で削除操作が発生します。

この問題の根本原因は、判断と削除の 2 つのステップが Redis のアトミック操作によって引き起こされるものではないことです。これを解決するにはどうすればよいですか?
解決するにはLuaスクリプトを使用する必要があります。

4.3.6、究極の解決策: ロックを解除する Lua スクリプト

複雑なまたは複数ステップの Redis 操作をスクリプトとして作成し、それを Redis に送信して一度に実行することで、Redis への繰り返し接続の数が減り、パフォーマンスが向上します。

Lua スクリプトは Redis トランザクションに似ており、特定の原子性を持ち、他のコマンドによってキューに入れられることはなく、一部の Redis トランザクション操作を完了できます。

ただし、redis の LUA スクリプト機能は redis2.6 以降でのみ使用できるので注意してください。

コードは以下のように表示されます。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @className LockTest
 * @date 2022/6/21
 **/
@RestController
public class LockTest {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @RequestMapping(value = "/lock", produces = MediaType.TEXT_PLAIN_VALUE)
    public String lock() {
        String lockKey = "k1";
        String uuid = UUID.randomUUID().toString();
        // 1.获取锁,有效期10秒
        if (this.redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 10, TimeUnit.SECONDS)) {
            // 2.执行业务
            // todo 业务

            //3.使用Lua脚本释放锁(可防止误删)
            String script = "if redis.call('get',KEYS[1])==ARGV[1] then returnredis.call('del', KEYS[1]) else return 0 end ";
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(script);
            redisScript.setResultType(Long.class);
            Long result = redisTemplate.execute(redisScript, Arrays.asList(lockKey), uuid);
            System.out.println(result);
            return "获取锁成功!";
        } else {
            return "加锁失败!";
        }
    }

}

4.3.7 分散ロックの概要

分散ロックを確実に利用できるようにするには、分散ロックの実装が次の 4 つの条件を同時に満たしていることを確認する必要があります。

  • 相互排他。いつでも 1 つのクライアントだけがロックを保持できます。
  • 明確なデッドロックが発生します。クライアントがロックを保持している間にクラッシュしてロックを解放しなかった場合でも、後で他のクライアントがロックできるようにすることもできます。
  • ロックを解除するには送信者を送信する必要があります。ロックとロック解除には同じクライアントを使用する必要があります。クライアントは他の人のロックを解除することはできません。
  • ロックとロック解除はアトミックである必要があります

おすすめ

転載: blog.csdn.net/qq_34272760/article/details/125398488