16. Redis アプリケーションの問題解決
16.1 キャッシュ侵入
16.1.1 問題の説明
キーに対応するデータがデータ ソースに存在しません。このキーに対する要求をキャッシュから取得できないたびに、データ ソースに要求が押し付けられ、データ ソースに負荷がかかる可能性があります。たとえば、存在しないユーザー ID を使用してユーザー情報を取得し、キャッシュもデータベースも取得しない場合、ハッカーがこの脆弱性を悪用して攻撃すると、データベースが圧倒される可能性があります。
16.1.2 解決策
キャッシュに存在してはならず、クエリできないデータ。これは、キャッシュが失われると受動的に書き込まれるためであり、フォールト トレランスのために、データがストレージ レイヤーから見つからない場合は、キャッシュに書き込まれません。この存在しないデータが発生します。すべてのリクエストはストレージ層に移動してクエリを実行する必要があり、キャッシュの意味が失われます。
解決:
null 値のキャッシュ:クエリによって返されたデータが空の場合 (データが存在しないかどうかにかかわらず)、空の結果 (null) をキャッシュし、null の結果の有効期限は非常に短く設定されます。 5分以上
アクセス可能なリスト (ホワイト リスト) を設定します。
ビットマップ タイプを使用して、アクセス可能なリストを定義します。リスト ID は、ビットマップのオフセットとして使用されます。各訪問は、ビットマップ内の ID と比較されます。アクセス ID がビットマップ内にない場合、アクセス ID は傍受され、アクセスが行われます。禁じられている。
ブルーム フィルターの採用: (ブルーム フィルター (ブルーム フィルター) は、1970 年にブルームによって提案されました。実際には、非常に長いバイナリ ベクトル (ビットマップ) と一連のランダム マッピング関数 (ハッシュ関数) です。
ブルーム フィルターを使用して、要素がセット内にあるかどうかを取得できます。その利点は、スペース効率とクエリ時間が一般的なアルゴリズムよりもはるかに優れていることです。欠点は、一定の誤認率と削除の困難性があることです。)
可能なすべてのデータを十分な大きさのビットマップにハッシュすると、存在してはならないデータがこのビットマップによってインターセプトされるため、基盤となるストレージ システムへのクエリ プレッシャーが回避されます。
リアルタイム監視: Redisのヒット率が急激に低下し始めていることが判明した場合は、アクセス対象やアクセスデータを確認し、運用保守担当者と連携してブラックリストを設定し、サービスを制限する必要があります
16.2 キャッシュの内訳
16.2.1 問題の説明
16.2.2 ソリューション
キーは、非常に「ホット」なデータである、高い同時実行性で特定の時点でアクセスされる場合があります。現時点では、問題を考慮する必要があります。キャッシュが「壊れる」という問題です。
問題を解く:
(1) 事前設定された人気のあるデータ: redis のピーク アクセスの前に、いくつかの人気のあるデータを事前に redis に保存し、これらの人気のあるデータ キーの期間を増やします。
(2) リアルタイム調整:現場で人気のデータを監視し、鍵の有効期限をリアルタイムで調整
(3) ロックを使用する:
- つまり、キャッシュが無効な場合(取り出した値が空であると判断した場合)、すぐにdbをロードしないということです。
- 最初に、成功した操作の戻り値 (Redis の SETNX など) を使用してキャッシュ ツールのいくつかの操作を使用して、ミューテックス キーを設定します。
- 操作が正常に終了したら、load db 操作を実行し、キャッシュをリセットして、最後にミューテックス キーを削除します。
- 操作が失敗すると、db をロードしているスレッドが存在することが証明され、現在のスレッドは get cache メソッド全体を再試行する前に一定期間スリープします。
16.3 キャッシュなだれ
16.3.1 問題の説明
キーに対応するデータは存在しますが、redis で有効期限が切れます. この時点で多数の同時リクエストがある場合、これらのリクエストは通常、バックエンド DB からデータをロードし、キャッシュが見つかったときにキャッシュに戻します.バックエンド DB がクラッシュしています。
キャッシュ アバランチとキャッシュ ブレークダウンの違いは、ここでは多くのキー キャッシュが対象であるのに対し、前者は特定のキーの通常のアクセスであるということです。
キャッシュ無効化の瞬間
16.3.2 ソリューション
キャッシュが無効な場合の基盤となるシステムに対するなだれ効果の影響はひどいものです。
解決:
マルチレベル キャッシュ アーキテクチャを構築します。
nginx キャッシュ + redis キャッシュ + その他のキャッシュ (ehcache など)
ロックまたはキューを使用します。
ロックまたはキューを使用して、一度に多数のスレッドがデータベースの読み取りと書き込みを行うことがないようにし、障害が発生したときに基盤となるストレージ システムに多数の同時要求が発生するのを回避します。高い同時実行には適していません
有効期限フラグを設定してキャッシュを更新します。
キャッシュされたデータの有効期限が切れるかどうかを記録します (前払い金額を設定します)。期限が切れると、別のスレッドに通知して、バックグラウンドで実際のキー キャッシュを更新します。
キャッシュの有効期限を広げる:
たとえば、1 ~ 5 分のランダムなど、元の有効期限に基づいてランダムな値を追加できます。これにより、各キャッシュの有効期限の繰り返し率が減少し、集合的な障害イベントをトリガーすることが難しくなります。 .
16.4 分散ロック
16.4.1 問題の説明
ビジネス開発のニーズにより、元の単一マシン展開システムが分散クラスター システムに進化した後、分散システムはマルチスレッド、マルチプロセス、および異なるマシンに分散されるため、これにより同時実行制御がロックされます。元のスタンドアロン展開の状況 この戦略は失敗し、純粋な Java API はロックを分散する機能を提供できません。この問題を解決するには、共有リソースへのアクセスを制御するクロス JVM 相互排他メカニズムが必要であり、分散ロックによって解決される問題です。
分散ロックの主流の実装:
1. データベースベースの分散ロックを実現
2.キャッシュベース(Redisなど)
3.Zookeeperに基づく
各分散ロック ソリューションには、それぞれ長所と短所があります。
1. パフォーマンス: Redis が最高
2.信頼性:飼育係が最高
ここでは、redis に基づいて分散ロックを実装します。
16.4.2 解決策: redis を使用して分散ロックを実装する
redis: コマンド
# set sku:1:info 「OK」 NX PX 10000
EX second : 鍵の有効期限を秒秒に設定します。SET キー値の EX 秒は、SETEX キーの秒値と同じです。
PX millisecond : キーの有効期限をミリ秒ミリ秒に設定します。SET キー値 PX ミリ秒は、PSETEX キー ミリ秒値に相当します。
NX : キーが存在しない場合にのみキーを設定します。SET キー値 NX は SETNX キー値と同等です。
XX : キーがすでに存在する場合にのみキーを設定します。
1. 複数のクライアントが同時にロックを取得する (setnx)
2.取得成功、ビジネスロジックを実行{dbからデータを取得、キャッシュに入れる}、実行完了後にロック解除(del)
3. 他のクライアントは再試行を待ちます
16.4.3 コードの記述
戻り値: 0 かどうかを設定
@GetMapping("testLock")
public void testLock(){
//1获取锁,setne
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
//2获取锁成功、查询num的值
if(lock){
Object value = redisTemplate.opsForValue().get("num");
//2.1判断num为空return
if(StringUtils.isEmpty(value)){
return;
}
//2.2有值就转成成int
int num = Integer.parseInt(value+"");
//2.3把redis的num加1
redisTemplate.opsForValue().set("num", ++num);
//2.4释放锁,del
redisTemplate.delete("lock");
}else{
//3获取锁失败、每隔0.1秒再获取
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
再起動してクラスターを提供し、ゲートウェイのストレス テストに合格します。
ab -n 10 00 -c 100 http://192.168.140.1:8080/test/testLock
redis で num の値を表示します。
基本的に実現。
問題: setnx がロックを取得したばかりで、ビジネス ロジックが異常であるため、ロックが解放されない
解決策: 有効期限を設定し、ロックを自動的に解放します。
16.4.4 ロックの有効期限を設定するための最適化
有効期限を設定するには、次の 2 つの方法があります。
1. まず、expire で有効期限を設定することを考える (アトミック性の欠如: setnx と expire の間で例外が発生した場合、ロックを解除できない)
2.設定時に有効期限を指定する(推奨)
有効期限の設定:
ストレステストも問題ありません。セルフテスト
問題: 他のサーバーからのロックが解除される場合があります。
シナリオ: ビジネス ロジックの実行時間が 7 秒の場合。実行プロセスは次のとおりです。
- index1 ビジネス ロジックは実行されておらず、ロックは 3 秒後に自動的に解放されます。
- index2 がロックを取得し、ビジネス ロジックを実行すると、ロックは 3 秒後に自動的に解放されます。
- index3 はロックを取得し、ビジネス ロジックを実行します
- index1 ビジネス ロジックの実行が完了したら、del を呼び出してロックを解放しますが、この時点で index3 のロックが解放され、実行からわずか 1 秒で他の人によって index3 ビジネスが解放されます。
結局、ノーロックの状態に等しい。
解決策: setnx がロックを取得するときに、指定された一意の値 (例: uuid) を設定し、この値を取得してから解放し、それが独自のロックであるかどうかを判断します。
16.4.5 偶発的な削除を防ぐために最適化された UUID
16.4.6 最適化された LUA スクリプトは、削除の原子性を保証します
@GetMapping("testLockLua")
public void testLockLua() {
//1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
String uuid = UUID.randomUUID().toString();
//2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
String skuId = "25"; // 访问skuId 为25号的商品 100008348542
String locKey = "lock:" + skuId; // 锁住的是每个商品的数据
// 3 获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
// 第一种: lock 与过期时间中间不写任何的代码。
// redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
// 如果true
if (lock) {
// 执行的业务逻辑开始
// 获取缓存中的num 数据
Object value = redisTemplate.opsForValue().get("num");
// 如果是空直接返回
if (StringUtils.isEmpty(value)) {
return;
}
// 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
int num = Integer.parseInt(value + "");
// 使num 每次+1 放入缓存
redisTemplate.opsForValue().set("num", String.valueOf(++num));
/*使用lua脚本来锁*/
// 定义lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 使用redis执行lua执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 设置一下返回值类型 为Long
// 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
// 那么返回字符串与0 会有发生错误。
redisScript.setResultType(Long.class);
// 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
} else {
// 其他线程等待
try {
// 睡眠
Thread.sleep(1000);
// 睡醒了之后,调用方法。
testLockLua();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Lua スクリプトの詳細な説明:
プロジェクトで正しく使用されている:
- キーを定義します。キーは SKU ごとに定義する必要があります。つまり、各 SKU にはロックがあります。
String locKey = "lock:" +skuId; //各商品のデータをロック
Boolean lock = redisTemplate .opsForValue().setIfAbsent(locKey, uuid, 3 ,TimeUnit. SECONDS );
16.4.7 まとめ
1. ロック
// 1.redis中获取锁,set k1 v1 px 20000 nx String uuid = UUID. randomUUID ().toString(); ブールロック = this . redisTemplate .opsForValue() .setIfAbsent( "lock" , uuid, 2 , TimeUnit. SECONDS );
2. lua を使用してロックを解除します
// 2.ロックを解除 del
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end " ;
// Luaスクリプトによって返されるデータ型を設定DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); // Luaスクリプトの戻り値の型をLongに設定redisScript.setResultType(Long.class ); redisScript.setScriptText( script) ; redisTemplate .execute(redisScript, Arrays. asList ( "lock" ),uuid);
3. 再試行
糸。スリープ( 500 );
testLock();
分散ロックを利用できるようにするために、少なくともロックの実装が次の 4 つの条件を満たしていることを確認する必要があります。
- 相互排除。一度に 1 つのクライアントだけがロックを保持できます。
- デッドロックは発生しません。クライアントがロックを保持している間にクラッシュし、アクティブにロックを解除しない場合でも、他のクライアントが後でロックできるようにすることができます。
- トラブルはそれを終了する必要があります。ロックとロック解除は同じクライアントである必要があり、クライアント自体は他のユーザーが追加したロックを解除できません。
- ロックとロック解除はアトミックでなければなりません。