Redisキャッシュ更新戦略、アクティブ更新戦略、ペネトレーション、雪崩、ブレークダウン実戦
Redis キャッシュ更新戦略、アクティブ更新戦略、実際の侵入、雪崩、およびブレークダウン
2. Redis キャッシュの使用開始
データをクエリするときは、通常、データベースに直接クエリを実行し、それをフロントエンドに返します。実際のプロジェクトでは効率化を図るため、基礎データなどのホットデータをRedisキャッシュに置きます。
リクエストを開始するには、まず Redis キャッシュにアクセスし、キャッシュ ヒットがある場合はデータを直接返します。キャッシュがヒットしない場合は、データベースにクエリを実行し、データを Redis に保存してからデータを返します。
次に、キャッシュを実装するためにストアへのクエリをシミュレートします。
2.1 インターフェース queryShopById を変更する
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
String key ="cache:shop" + id;
//1.从redis中查询缓存
String shopjson = stringRedisTemplate.opsForValue().get(key);
//2、判断缓存是否存在
if (StrUtil.isNotBlank(shopjson)) {
//3、存在,返回数据
Shop shop = JSONUtil.toBean(shopjson, Shop.class);
return Result.ok(shop);
}
//4.不存在查询数据库
Shop shop = getById(id);
//5.不存在返回错误
if ((null == shop)) {
return Result.fail("店铺不存在");
}
//6.存在放入缓存中
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
//7.返回
return Result.ok(shop);
}
2.2 観測結果へのアクセス
http://localhost:8081/shop/1 に何度もアクセスした結果、データが正常に返されることがわかりました。
理想的なコンソールを観察してください。
Redis コンソールを観察します
3 つのキャッシュ更新戦略
-
記憶の陳腐化
Redis はメモリ不足の問題を解決するために使用されます。Redis はメモリ ストレージに基づいており、メモリには制限があり、多くの場合上限が設定されています。保存するデータが増えてメモリ不足になる可能性があるため、redis にはメモリ削除メカニズムがあり、メモリが不足すると、この戦略がトリガーされて一部のデータが削除され、新しいデータを保存するためのスペースが確保されます。このメカニズムはデフォルトで有効になっています。この種のデータの一貫性は比較的低く、削除されたデータのみが再度更新されるため、メモリが十分にある場合、キャッシュされたデータは長期間古いデータであることがよくあります。
-
タイムアウトカリング
キャッシュされたデータに有効期限を追加し、有効期限が切れたらデータを自動的に削除し、次のクエリ時にデータを更新します。ただし、有効期限内のデータの整合性は保証できないため、整合性は平均的ですが、より良いものになります。メモリ消去メカニズムよりも。
-
アクティブなアップデート
ビジネス ロジックを自分で作成すると、データが変更されるたびにキャッシュも変更されます。この種のデータの一貫性は比較的良好ですが、データが完全に一貫していることを完全に保証することはできません。コストは比較的高くなります。
プロアクティブなアップデート戦略
アクティブな更新戦略は 3 つあります。
-
Cache Aside Pattern
キャッシュの呼び出し元はデータベースを更新しながらキャッシュも更新するため、プログラミングが必要で業務が比較的複雑です。
-
Read/Write Through Pattern
キャッシュとデータベースはサービスに統合されており、サービスは一貫性を維持します。呼び出し元は、キャッシュの整合性の問題を気にせずにこのサービスを呼び出します。この方法の最大の問題は、そのようなサービスの維持が複雑であり、現在市場には完璧なソリューションが存在しないことです。
-
Write Behind Caching Pattern
呼び出し元はキャッシュを操作するだけで、他のスレッドはキャッシュ データをデータベースに非同期的に永続化して、結果的な整合性を確保します。非同期スレッドが最新の操作をデータベースに保持する前に Redis がクラッシュすると、データ損失が発生します。
その中で、キャッシュ アサイド パターンが最も一般的な更新戦略ですが、キャッシュとデータベースを運用する際には考慮すべき 3 つの問題があります。
2 つのソリューションを比較します。最初にキャッシュを削除してからデータベースを操作する方法と、最初にデータベースを操作してからキャッシュを削除する方法です。
最初にキャッシュを削除してからデータベースを操作すると、次の図に示す問題が発生する可能性があります。つまり、スレッド 1 がキャッシュを削除した後、データベースが更新されていません。スレッド 2 がクエリを実行し、redis が利用できないことが判明したため、スレッド 2 がクエリを実行します。これにより、後続のすべてのアクセスで Redis のダーティ データが取得されます。
この種のスレッド セーフでは、キャッシュがすぐに削除され、その後データが処理されて最終的に更新されるため、データベース データとキャッシュされたデータの間で不一致が発生する可能性が非常に高くなります。時間差が長いほど、他のスレッドが不一致になる可能性が高くなります。チャンスを活かすために。
最初にデータベースを操作してからキャッシュを削除します。特定のデータがキャッシュされていない場合、スレッド 1 はデータベースにアクセスしてデータを読み取ります。それを Redis に書き込む前に、スレッド 2 がデータベースを更新します。その後、スレッド 1 はデータを書き込んでいません。 Redis がキャッシュ削除操作を実行する場合、これは当然無効な削除であり、その後、スレッド 1 が古いデータを Redis に書き込み、これにより後続のすべてのリクエストがダーティ データを取得することになります。この場合、データの不整合が非常に大きくなります。高い
. 高いのはどうですか?これが起こるには 2 つの条件があります。
まず、2 つのスレッドを並行して実行する必要があります。
次に、スレッド 1 がクエリを実行するとき、たまたまキャッシュが無効であり、データベースにクエリを実行してキャッシュに書き込みます (時間差は非常に短い)。データベースを更新しに来ます。
この極めて短い時間内にデータベースの書き込み操作が完了する可能性は非常に低いため、データの不整合が発生する可能性は高くありません。
遅延二重削除
上記のどちらの状況でもデータの不整合が生じる可能性がありますが、整合性を確保するためのより良い方法はあるのでしょうか?
遅延二重削除には 4 つの一般的な手順があります。以下の疑似コードを参照してください。
def update_data(key, obj):
del_cache(key) # 删除 redis 缓存数据。
update_db(obj) # 更新数据库数据。
logic_sleep(_time) # 当前逻辑延时执行。
del_cache(key) # 删除 redis 缓存数据。
logic_sleep は、コルーチンのスリープ切り替えなど、現在のリクエスト ロジックの遅延実行、または次のステップの実行を遅らせるためにクロックに組み込まれた非同期ロジックです。これをスレッド/プロセスのスリープ切り替えだと勘違いする人も多いでしょう もちろんこれも可能です インパクト大きすぎませんか~
Redisキャッシュ整合性遅延二重削除コード実装方法
なぜ遅れたのでしょうか?
これは、redis を 2 回目に削除する前にデータベースの更新操作を完了できるようにするためです。3 番目のステップがない場合は、2 つの Redis 削除操作が完了した後、データベース内のデータが更新されていない可能性が高いと仮定します。この時点でデータへのアクセス要求がある場合、問題は発生します。冒頭で述べたような質問が表示されます。
なぜキャッシュを 2 回削除するのでしょうか?
2 回目の削除操作がなく、この時点でデータへのアクセス要求がある場合、それは以前に変更されていない Redis データである可能性があります。削除操作の実行後、redis は空になります。この時点でデータベース内のデータは更新済みのデータとなっており、データの整合性が確保されています。
セクション
- 遅延二重削除では、比較的簡単な方法を使用してmysql と redis データの最終的な整合性を実現しますが、これは強い整合性ではありません。
- この遅延の原因は、mysql と redis のマスター/スレーブ ノード間のデータ同期がリアルタイムではないためです。そのため、データの整合性を高めるには一定期間待機する必要があります。
- レイテンシーとは、現在のスレッドやプロセスのスリープ遅延ではなく、現在のリクエストの論理処理遅延を指します。
MySQL と Redis のデータ整合性は複雑なトピックであり、通常は、遅延二重削除、Redisの有効期限、ルーティング戦略による同じ種類のデータのシリアル処理、分散ロックなど、複数の戦略が同時に使用されます。
遅延二重削除(redis-mysql) のデータ整合性に関する考慮事項を
要約するには、Redisに接続するための遅延二重削除戦略を参照してください。
キャッシュ更新戦略の概要
事例演習
ストア、キャッシュ、データベース間の二重書き込みの整合性を実現します。
ID に基づいてストアをクエリする場合、キャッシュが見つからない場合は、データベースにクエリを実行し、データベースの結果をキャッシュに書き込み、タイムアウトを設定します。ID に基づいてストアを変更する場合は、最初にデータベースを変更してからキャッシュを削除し
ます。
キー コード 1 の変更: ShopServiceImpl の queryById メソッドを変更して、
Redis キャッシュを設定するときに有効期限を追加します。
public static final Long CACHE_SHOP_TTL = 30L;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 4.不存在,根据id查询数据库
Shop shop = getById(id);
// 5.不存在,返回错误
if (shop == null) {
// 返回错误信息
return Result.fail("店铺不存在!");
}
// 6.存在,写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7.返回
return Result.ok(shop);
}
キーコード2を変更する
コード分析: 以前の排除を通じて、二重書き込み問題を解決するために削除戦略を採用することを決定しました。データを変更した後、キャッシュ内のデータを削除しました。クエリを実行したときに、キャッシュにデータがないことがわかりました。 , mysql から削除します。データベースとキャッシュの不整合を避けるために最新のデータをロードしてください
@Override
@Transactional
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空");
}
// 1.更新数据库
updateById(shop);
// 2.删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
}
クアッドキャッシュの浸透
4.1 キャッシュペネトレーションの分析と解決策
キャッシュペネトレーション: キャッシュペネトレーションとは、クライアントによって要求されたデータがキャッシュまたはデータベースに存在しないため、キャッシュが有効にならず、これらのリクエストがデータベースにヒットすることを意味します。
一般的な解決策は 2 つあります。
- 空のオブジェクトをキャッシュする
- 利点: シンプルな実装と容易なメンテナンス
- 欠点:
- 追加のメモリ消費量
- 短期的な不整合が発生する可能性がある
- ブルームフィルタリング
- 利点: メモリ使用量が少なく、冗長キーが不要
- 欠点:
- 実装が複雑
- 判断ミスの可能性もある
空のオブジェクトをキャッシュするというアイデアの分析:クライアントが存在しないデータにアクセスするとき、最初に Redis をリクエストしますが、この時点では Redis にデータがありません。この時点でデータベースにアクセスされますが、データはありません。このデータはキャッシュに侵入します。データベースを直接見ると、データベースが実行できる同時実行性が Redis ほど高くないことがわかります。大量のリクエストがこの存在しないデータにアクセスするようになった場合、同時に、これらのリクエストはすべてデータベースにアクセスします。簡単な解決策は、データがデータベース内にある場合でも、存在しない場合は、このデータも Redis に保存します。このようにして、次回ユーザーがアクセスしたときに、この存在しないデータにアクセスすると、そのデータは Redis でも見つかり、キャッシュには入りません。
ブルーム フィルタリング:ブルーム フィルタは、この問題を解決するために実際にハッシュ思考を使用します。巨大なバイナリ配列を通じて、ハッシュのアイデアを使用して、クエリ対象の現在のデータが存在するかどうかを判断します。ブルーム フィルタがデータが存在すると判断した場合は、リリースされました。このリクエストは redis にアクセスします。この時点で redis 内のデータの有効期限が切れている場合でも、データはデータベースに存在する必要があります。データベース内のデータをクエリした後、redis にデータが配置されます。
ブルーム フィルターがこのデータが存在しないと判断した場合、直接返されます。
この方法の利点はメモリ容量を節約できることですが、ブルームフィルタがハッシュ思考を使用しているため、誤判断が発生し、ハッシュ思考を使用している限り、ハッシュの競合が発生する可能性があります。
4.2 キャッシュペネトレーションソリューションの実践
核となるアイデアは次のとおりです。
- 元のロジックでは、このデータが mysql に存在しないことが判明した場合は、直接 404 を返します。これにより、キャッシュ貫通の問題が発生します。
- 現在のロジックでは、データが存在しない場合は 404 を返さず、データを Redis に書き込み、値を空に設定します。再度クエリを開始したときに、ヒットした場合にこれを判断します。 value が null かどうか。null の場合は以前に書き込まれたデータであり、キャッシュ貫通データであることが証明されます。そうでない場合は、データが直接返されます。
ShopServiceImpl の queryById メソッドを変更して、
redis に null 値を書き込むようにします。null 値のキャッシュ時間は、通常の値よりも短くなります。
@Override
public Result queryById(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return Result.fail("店铺信息不存在!");
}
// 4.不存在,根据id查询数据库
Shop shop = getById(id);
// 5.不存在,返回错误
if (shop == null) {
// 将空值写入redis,空值的缓存时间应该小于正常值的时间
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return Result.fail("店铺不存在!");
}
// 6.存在,写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7.返回
return Result.ok(shop);
}
4.3 概要
キャッシュ侵入の原因は何ですか?
- ユーザーがリクエストしたデータはキャッシュにもデータベースにも存在しないため、このようなリクエストを継続的に開始するとデータベースに大きな負荷がかかります。
キャッシュ侵入に対する解決策は何ですか?
- null値をキャッシュする
- ブルームフィルタリング
- ID ルールの推測を避けるために ID の複雑さを強化する
- データの基本的なフォーマット検証を実行する
- ユーザー権限の検証を強化する
- ホットスポット パラメータの現在のフローを適切に制限する
5 つのキャッシュ雪崩
キャッシュなだれとは、多数のキャッシュ キーが同時に期限切れになるか、Redis サービスがダウンして、大量のリクエストがデータベースに到達し、大きな圧力がかかることを意味します。
解決:
- 異なるキーの TTL にランダムな値を追加します
- Redis クラスターを利用してサービスの可用性を向上させる
- キャッシュ ビジネスにダウングレード電流制限ポリシーを追加する
- ビジネスにマルチレベル キャッシュを追加する
6 つのキャッシュの内訳
キャッシュ破壊問題はホットキー問題とも呼ばれ、同時アクセスが多くキャッシュ再構築業務が複雑なキーが突然故障してしまう問題であり、無数のアクセス要求が瞬時にデータベースに多大な影響を及ぼします。
一般的な解決策は 2 つあります。
- ミューテックスロック
- 論理的な有効期限
6.1 論理分析
スレッド 1 がキャッシュにクエリを実行した後、データベースにクエリを実行し、データをキャッシュに再ロードする必要があるとします。この時点で、スレッド 1 がこのロジックを完了している限り、他のスレッドはキャッシュからデータをロードできますが、スレッド 1 が終了していない場合、後続のスレッド 2、スレッド 3、およびスレッド 4 が同時に現在のメソッドにアクセスするようになります。この場合、これらのスレッドはキャッシュのデータをクエリできなくなり、クエリにアクセスします。チェック後、データベースへのアクセスとデータベースコードの実行が同時に行われるため、データベースアクセスに大きな負荷がかかります。
6.2 解決策 1: ロックを使用して次のことを解決します。
なぜなら、ロックは相互排他性を達成できるからです。スレッドが来た場合、データベース アクセスへの過剰な負荷を避けるために、データベースにアクセスできるのは 1 人だけですが、この時点でクエリのパフォーマンスが並列からシリアルに変わるため、これはクエリのパフォーマンスにも影響します。この問題を解決するには、tryLock メソッドとダブルチェックを使用できます。
ここでスレッド 1 がアクセスし、そのクエリ キャッシュはヒットしませんが、この時点でロック リソースを取得すると、スレッド 1 が単独でロジックを実行するとします。実行中にロックが発生します。ロックに到達すると、スレッド 1 がロックを解放するまでスレッド 2 はスリープ状態になり、スレッド 2 がロックを取得してからロジックを実行します。このとき、キャッシュからデータを取得できます。
6.3 解決策 2、論理的有効期限の解決策
プログラム分析:
このキャッシュ破壊の問題が発生する主な理由は、キーの有効期限を設定しているためです。有効期限を設定しないと仮定すると、キャッシュ破壊の問題は発生しません。ただし、有効期限を設定しないと、データは常にメモリを占有するのでしょうか? 論理有効期限スキームを使用できます。
redis の値に有効期限を設定します。注: この有効期限は redis に直接影響しませんが、後でロジックを通じて処理します。スレッド 1 がキャッシュをクエリし、その値から現在のデータの有効期限が切れていると判断するとします。このとき、スレッド 1 はミューテックス ロックを取得し、他のスレッドはブロックされます。ロックを取得したスレッドは、実行するスレッドを開始します。前の データを再構築するロジックは、新しく開かれたスレッドがこのロジックを完了するまでロックを解放せず、スレッド 1 が直接戻ります。ここでスレッド 3 がアクセスしてきたとします。スレッド 2 がロックを保持しているため、スレッド 3 はロックを取得できません。 , スレッド 3 もデータを直接返します。新しく開かれたスレッド 2 がデータの再構築を完了した後にのみ、他のスレッドは正しいデータを返すことができます。
このソリューションの賢い点は、キャッシュを非同期に構築することにありますが、欠点は、キャッシュが構築される前にすべてのダーティ データが返されることです。
比較する
相互排他ロック方式:相互排他が保証されているため、データの一貫性があり、実装が簡単です。ロックを 1 つ追加するだけで済み、他に心配する必要がないため、追加のメモリ消費がありません。欠点は、ロックが存在する限り、デッドロックの問題が発生し、シリアル実行のパフォーマンスのみが確実に影響を受けます。
論理有効期限スキーム:スレッドの読み取りプロセス中に待機する必要がなく、パフォーマンスが良好です。追加のスレッドがデータを再構築するためにロックを保持しますが、データの再構築が完了する前に、他のスレッドは前のスレッドのみを返すことができます。データと実装立ち上がるのが面倒
6.4 ミューテックスロックを使用してキャッシュの故障の問題を解決する練習をする
核となるアイデア:
キャッシュからデータをクエリしなかった直後のデータベースの元のクエリと比較して、現在の解決策は、クエリ後にキャッシュからデータがクエリされなかった場合にミューテックス ロックを取得し、ミューテックス ロックを取得した後、ロックが取得されているかどうかを判断します。スレッドがロックを取得できない場合は、スリープ状態になり、しばらくしてから再試行します。ロックが取得されるまで、クエリを実行できます。ロックを取得した場合は、再度クエリを実行し、クエリ後にデータを Redis に書き込み、解放します
。データをロックし、返し、ミューテックス ロックを使用して、データベース操作のロジックを 1 つのスレッドだけが実行するようにして、キャッシュの破壊を防ぎます。
ロックを操作するコード:
中心となるアイデアは、redis の setnx メソッドを使用してロックを取得することです。このメソッドの意味は、redis にそのようなキーが存在しない場合、挿入は成功し、1 が返されます。stringRedisTemplate の場合、true が返されます。そのようなキーが存在する場合、挿入は失敗し、0 が返されます。stringRedisTemplate の場合、false が返されます。が返されます。, true または false を使用して、スレッドがキーの挿入に成功したかどうかを示すことができます。正常に挿入されたキーのスレッドが、ロックを取得したスレッドであると見なされます。
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
オペコード:
public Shop queryWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1、从redis中查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2、判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 存在,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
//判断命中的值是否是空值
if (shopJson != null) {
//返回一个错误信息
return null;
}
// 4.实现缓存重构
//4.1 获取互斥锁
String lockKey = "lock:shop:" + id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2 判断否获取成功
if(!isLock){
//4.3 失败,则休眠重试
Thread.sleep(50);
return queryWithMutex(id);
}
//4.4 成功,根据id查询数据库
shop = getById(id);
// 5.不存在,返回错误
if(shop == null){
//将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
//返回错误信息
return null;
}
//6.写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL,TimeUnit.MINUTES);
}catch (Exception e){
throw new RuntimeException(e);
}
finally {
//7.释放互斥锁
unlock(lockKey);
}
return shop;
}
jmeterテスト
同時
http://localhost:8081/shop/1
観察コンソールをシミュレートするための複数のアクセス。クエリ SQL は 1 回だけ実行されます。
6.5 論理有効期限を使用してキャッシュの故障の問題を解決する練習をする
要件: ID に基づいてストアをクエリするビジネスを変更し、論理有効期限に基づいてキャッシュの故障の問題を解決します。
アイデア分析: ユーザーが Redis のクエリを開始すると、ヒットがあるかどうかを判断します。ヒットがない場合は、データベースにクエリを実行せずに空のデータが直接返されます。ヒットすると、値が取得され、有効期限が切れているかどうかが判断されます。値の時間が満たされている場合は、有効期限が切れていない場合は、redis 内のデータを直接返します。有効期限が切れている場合は、独立したスレッドを開始した後、前のデータが直接返されます。独立したスレッドはデータを再構築し、ミューテックス ロックを解放します。再建が完了した後。
データをカプセル化する場合: Redis に保存されるデータの値には有効期限が必要となるため、元のエンティティ クラスを変更するか、新しいエンティティ クラスを作成する必要があります。
第一歩、
元のコードに影響を与えない 2 番目の解決策を採用します。
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
ステップ2。
このメソッドをShopServiceImplに追加し、単体テストを使用してキャッシュをウォームアップします。
@Override
public void saveShop2Redis(Long id, Long expireSeconds) {
Shop shop = getById(id);
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
テストクラスで
@Autowired
private IShopService shopService;
@Test
void test() {
shopService.saveShop2Redis(1L,10L);
}
ステップ 3: 正式なコード
ショップサービス実装
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {
String key = CACHE_SHOP_KEY + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未过期,直接返回店铺信息
return shop;
}
// 5.2.已过期,需要缓存重建
// 6.缓存重建
// 6.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判断是否获取锁成功
if (isLock){
CACHE_REBUILD_EXECUTOR.submit( ()->{
try{
//重建缓存
this.saveShop2Redis(id,20L);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return shop;
}
7 つのキャッシュ ツール パッケージ
次の要件を満たすために、StringRedisTemplate に基づいてキャッシュ ツール クラスをカプセル化します。
- 方法 1: Java オブジェクトを json にシリアル化し、文字列型のキーに格納すると、TTL 有効期限を設定できます
- 方法 2: 任意の Java オブジェクトを json にシリアル化し、文字列型のキーに格納し、バッファリングの論理有効期限を設定します。
故障問題あり
- 方法 3: 指定されたキーに基づいてキャッシュをクエリし、指定された型にデシリアライズし、キャッシュの null 値を使用してキャッシュ侵入問題を解決する
- 方法 4: 指定されたキーに基づいてキャッシュをクエリし、指定された型に逆シリアル化する キャッシュの故障の問題を解決するには、論理有効期限を使用する必要があります。
ロジックをカプセル化する
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
// 设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
// 写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
if (json != null) {
// 返回一个错误信息
return null;
}
// 4.不存在,根据id查询数据库
R r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
return r;
}
public <R, ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未过期,直接返回店铺信息
return r;
}
// 5.2.已过期,需要缓存重建
// 6.缓存重建
// 6.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判断是否获取锁成功
if (isLock){
// 6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
R newR = dbFallback.apply(id);
// 重建缓存
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return r;
}
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
return JSONUtil.toBean(shopJson, type);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return null;
}
// 4.实现缓存重建
// 4.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2.判断是否获取成功
if (!isLock) {
// 4.3.获取锁失败,休眠并重试
Thread.sleep(50);
return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
// 4.4.获取锁成功,根据id查询数据库
r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.释放锁
unlock(lockKey);
}
// 8.返回
return r;
}
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
}
ショップサービス実装内
@Resource
private CacheClient cacheClient;
@Override
public Result queryById(Long id) {
// 解决缓存穿透
Shop shop = cacheClient
.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 互斥锁解决缓存击穿
// Shop shop = cacheClient
// .queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 逻辑过期解决缓存击穿
// Shop shop = cacheClient
// .queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);
if (shop == null) {
return Result.fail("店铺不存在!");
}
// 7.返回
return Result.ok(shop);
}