この記事では、Redis に関する関連知識を提供します。主に、キャッシュ タイプの紹介、さまざまな使用シナリオ、使用方法など、分散キャッシュとローカル キャッシュの使用スキルを紹介し、最後に実践的な事例を挙げますので、ぜひ参考にしてみてください。
推奨される学習: Redis ビデオ チュートリアル
ご存知のとおり、キャッシュの主な目的は、アクセスを高速化し、アクセスを軽減することです。データベースのプレッシャー。最も一般的に使用されるキャッシュは、redis などの分散キャッシュです。ほとんどの同時実行シナリオや、一部の中小企業のトラフィックがそれほど高くない状況に直面した場合、基本的には redis で問題を解決できます。ただし、トラフィックが多い場合は、guava の LoadingCache や Kuaishou のオープン ソース ReloadableCache などのローカル キャッシュを使用する必要がある場合があります。
このパートでは、guava の LoadingCache や Kuaishou のオープン ソース ReloadableCache などの Redis の使用シナリオと制限事項を紹介します。ビジネス シナリオでどのキャッシュを使用する必要があるか、またその理由を説明します。
Redis をいつ使用するかについて大まかに話すと、ユーザーのアクセス数が多すぎる場所で自然に使用され、それによってアクセスとアクセスが高速化されます。データベースの負担を軽減します。細分化すると、単一ノードの問題と非単一ノードの問題に分けることができます。
ページへのユーザーのアクセス数が多いにもかかわらず、ユーザーが同じリソースにアクセスしていない場合。たとえば、ユーザーの詳細ページはアクセス数が比較的多いですが、ユーザーごとにデータが異なります。この場合、分散キャッシュしか使用できないことは明らかです。Redis を使用する場合、キーはユーザー固有のキーになります。キー、値はユーザー情報です。
redis によるキャッシュの故障。
ただし、注意すべき点は、有効期限を設定する必要があり、同じ時点で有効期限が切れるように設定することはできないことです。たとえば、ユーザーがアクティビティ ページを持っている場合、そのアクティビティ ページでは、アクティビティ中にユーザーの賞のデータが表示されます。不注意な人は、ユーザー データの有効期限をアクティビティの終了時点に設定してしまう可能性があります。
# (ホット) ポイントの問題
単一ノードの問題は、同じキーが Redis の同じノードに存在するため、Redis の単一ノードの同時実行性の問題を指します。 Redis クラスターなので、キーが次の場合、訪問数が多すぎる場合、この Redis ノードには同時実行のリスクがあり、このキーはホット キーと呼ばれます。 すべてのユーザーが同じリソースにアクセスする場合、たとえば、Xiao Ai アプリのホームページが (最初は) すべてのユーザーに同じコンテンツを表示し、サーバーが同じ大きな JSON を h5 に返す場合、明らかに次のようにする必要があります。キャッシュに使用されます。まずはredisの利用が可能か検討しますが、redisにはシングルポイントの問題があるため、トラフィックが大きすぎるとユーザーリクエストがすべてredisの同じノードに到達してしまい、そのノードが耐えられるかを評価する必要があります。こんなに大きな流れ。私たちのルールは、単一ノードの QPS が 1,000 レベルに達した場合、単一点問題を解決する必要があるというものです (たとえ Redis が 10 万レベルの QPS に耐えられると主張しているとしても)。最も一般的な方法はローカル キャッシュを使用することです。 。明らかに、Xiaoai アプリのホームページのトラフィックは 100 未満であるため、redis を使用しても問題ありません。 LoadingCache の使用シナリオと制限事項上記のホット キーの問題に対して、最も直接的なアプローチは、最もよく知られている guava の LoadingCache などのローカル キャッシュを使用することです。ローカル キャッシュを使用するホームページを更新してもローカル キャッシュは更新されないため、キャッシュは一定量のダーティ データを受け入れることができる必要があります。特定の有効期限ポリシーに従ってキャッシュをリロードするだけですが、一度ホームページがバックグラウンドにプッシュされると、再度変更されることはないため、このシナリオはまったく問題ありません。変更されても問題ありません。書き込み有効期限を 30 分に設定し、30 分後にキャッシュをリロードすることもできます。このような短期間でダーティ データを受け入れることができます。LoadingCache によるキャッシュの故障
ローカル キャッシュはマシンに強く関連していますが、コード レベルは 30 分で期限切れになるように書かれています。起動時間が異なると、キャッシュの読み込み時間や有効期限も異なるため、キャッシュの有効期限が同時に切れると、マシン上のすべてのリクエストがデータベースをリクエストすることはなくなります。ただし、キャッシュの侵入は 1 台のマシンでも発生するため、それぞれ 1,000 qps のマシンが 10 台ある場合、1 つのキャッシュが期限切れになると、これらの 1,000 リクエストが同時にデータベースにヒットする可能性があります。この種の問題は、実際には解決するのが簡単ですが、無視されやすいため、LoadingCache を設定する際には、cache.getIfPresent()== null を直接判定してから、 db; 前者は仮想マシンを追加します レイヤ ロックにより、データベースに送信されるリクエストは 1 つだけになるため、この問題は完全に解決されます。 ただし、一定期間にわたる頻繁なアクティビティなど、高いリアルタイム要件がある場合は、アクティビティ ページをほぼリアルタイム、つまり操作後に更新できるようにしたいと考えています。バックグラウンドでアクティビティ情報を設定するには、設定されたアクティビティ情報を C 側でほぼリアルタイムで表示する必要がありますが、LoadingCache を使用するだけでは明らかに十分ではありません。LoadingCache では解決できない上記のリアルタイム問題については、Kuaishou がオープンソース化しているローカル キャッシュ フレームワークである ReloadableCache の使用を検討できます。同時に複数のマシンをサポートしていることです キャッシュを更新します ホームページの情報を変更すると、リクエストがマシン A に到達するとします。このとき、ReloadableCache が再ロードされ、通知が送信されます。 その他同じ zk ノードをリッスンしているマシンは、通知を受信した後にキャッシュを更新します。このキャッシュを使用するための一般的な要件は、全量のデータをローカル キャッシュにロードすることです。そのため、データ量が大きすぎると確実に gc を圧迫するため、この場合は使用できません。 Xiao Ai のホームページにはステータスがあり、通常オンライン ステータスは 2 つだけなので、ReloadableCache を使用してオンライン ステータスのホームページのみを読み込むことができます。
3 種類のキャッシュについては基本的にここで紹介しましたが、概要は次のとおりです:
どのような種類のローカル キャッシュにも、故障の問題を解決するために仮想マシン レベルのロックが備わっていますが、予期せぬ形で事故が発生する可能性は常にあります。念のため、2 レベルのキャッシュ、つまりローカル キャッシュを使用できます。 Redis データベース。
ここでは Redis の使用法については説明しませんが、多くの人が私よりも API の使用法に精通していると思います
これは guava online で提供されていますが、注意点が 2 点あります。
V get(K key, Callabled1336e9e686742fb22c1860557381905loader) を使用してください。 、 get( ) を使用できます。さらに、getIfPresent==null の場合はデータベースをチェックする代わりに、load-miss を使用することをお勧めします。これにより、キャッシュが破損する可能性があります。
スレッドセーフであるため、load-miss を使用してください。キャッシュが失敗した場合は、複数のスレッドが get を呼び出す場合、1 つのスレッドだけがデータベースにクエリを実行し、他のスレッドは待機する必要があります。これは、スレッドセーフであることを意味します。
LoadingCache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(1000L) .expireAfterAccess(Duration.ofHours(1L)) // 多久不访问就过期 .expireAfterWrite(Duration.ofHours(1L)) // 多久这个key没修改就过期 .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { // 数据装载方式,一般就是loadDB return key + " world"; } }); String value = cache.get("hello"); // 返回hello world
<dependency> <groupId>com.github.phantomthief</groupId> <artifactId>zknotify-cache</artifactId> <version>0.1.22</version> </dependency>
public interface ReloadableCache<T> extends Supplier<T> { /** * 获取缓存数据 */ @Override T get(); /** * 通知全局缓存更新 * 注意:如果本地缓存没有初始化,本方法并不会初始化本地缓存并重新加载 * * 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()} */ void reload(); /** * 更新本地缓存的本地副本 * 注意:如果本地缓存没有初始化,本方法并不会初始化并刷新本地的缓存 * * 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()} */ void reloadLocal(); }決まり文句のキャッシュの故障/侵入/雪崩の問題これら 3 つは本当に永遠の問題であり、トラフィックが大きい場合は実際に考慮する必要があります。
キャッシュの内訳
import com.google.common.collect.Lists; import org.apache.commons.collections4.CollectionUtils; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; /** * @Description 分布式加载缓存的rpc服务,如果部署了多台机器那么调用端最好使用id做一致性hash保证相同id的请求打到同一台机器。 **/ public abstract class AbstractCacheSetterService implements CacheSetterService { private final ConcurrentMap<String, CountDownLatch> loadCache = new ConcurrentHashMap<>(); private final Object lock = new Object(); @Override public void load(Collection<String> needLoadIds) { if (CollectionUtils.isEmpty(needLoadIds)) { return; } CountDownLatch latch; Collection<CountDownLatch> loadingLatchList; synchronized (lock) { loadingLatchList = excludeLoadingIds(needLoadIds); needLoadIds = Collections.unmodifiableCollection(needLoadIds); latch = saveLatch(needLoadIds); } System.out.println("needLoadIds:" + needLoadIds); try { if (CollectionUtils.isNotEmpty(needLoadIds)) { loadCache(needLoadIds); } } finally { release(needLoadIds, latch); block(loadingLatchList); } } /** * 加锁 * @param loadingLatchList 需要加锁的id对应的CountDownLatch */ protected void block(Collection<CountDownLatch> loadingLatchList) { if (CollectionUtils.isEmpty(loadingLatchList)) { return; } System.out.println("block:" + loadingLatchList); loadingLatchList.forEach(l -> { try { l.await(); } catch (InterruptedException e) { e.printStackTrace(); } }); } /** * 释放锁 * @param needLoadIds 需要释放锁的id集合 * @param latch 通过该CountDownLatch来释放锁 */ private void release(Collection<String> needLoadIds, CountDownLatch latch) { if (CollectionUtils.isEmpty(needLoadIds)) { return; } synchronized (lock) { needLoadIds.forEach(id -> loadCache.remove(id)); } if (latch != null) { latch.countDown(); } } /** * 加载缓存,比如根据id从db查询数据,然后设置到redis中 * @param needLoadIds 加载缓存的id集合 */ protected abstract void loadCache(Collection<String> needLoadIds); /** * 对需要加载缓存的id绑定CountDownLatch,后续相同的id请求来了从map中找到CountDownLatch,并且await,直到该线程加载完了缓存 * @param needLoadIds 能够正在去加载缓存的id集合 * @return 公用的CountDownLatch */ protected CountDownLatch saveLatch(Collection<String> needLoadIds) { if (CollectionUtils.isEmpty(needLoadIds)) { return null; } CountDownLatch latch = new CountDownLatch(1); needLoadIds.forEach(loadId -> loadCache.put(loadId, latch)); System.out.println("loadCache:" + loadCache); return latch; } /** * 哪些id正在加载数据,此时持有相同id的线程需要等待 * @param ids 需要加载缓存的id集合 * @return 正在加载的id所对应的CountDownLatch集合 */ private Collection<CountDownLatch> excludeLoadingIds(Collection<String> ids) { List<CountDownLatch> loadingLatchList = Lists.newArrayList(); Iterator<String> iterator = ids.iterator(); while (iterator.hasNext()) { String id = iterator.next(); CountDownLatch latch = loadCache.get(id); if (latch != null) { loadingLatchList.add(latch); iterator.remove(); } } System.out.println("loadingLatchList:" + loadingLatchList); return loadingLatchList; } }
ビジネス実装
import java.util.Collection; public class BizCacheSetterRpcService extends AbstractCacheSetterService { @Override protected void loadCache(Collection<String> needLoadIds) { // 读取db进行处理 // 设置缓存 } }キャッシュ侵入
たとえば、上限が 1W の賞金プールがあるとします。ユーザーがタスクを完了したら、お金を送り、redis を使用して記録し、テーブルにログインします。ユーザーは次のことを確認できます。タスクページ上で賞金プールの残高をリアルタイムで確認できます タスク開始時点では、賞金プールの金額が変化していないことがわかります redisとdbには発行額の記録がありません毎回 DB をチェックする必要があるため、DB からデータが見つからない場合は値をキャッシュする必要があります。0 を指定するとキャッシュされます。
これは、大量のキャッシュ障害がデータベースに発生したことを意味します。もちろん、それらはすべてビジネス キャッシュである必要があります。最終的には、次の問題があります。コードの書き方。キャッシュ無効化の有効期限を分割して、中央で失敗させないようにすることができます。
推奨される学習: Redis ビデオ チュートリアル
以上がRedis とローカル キャッシュを使用した高同時実行技術の共有の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。