ホームページ  >  記事  >  データベース  >  Redis とローカル キャッシュを使用した高同時実行技術の共有

Redis とローカル キャッシュを使用した高同時実行技術の共有

WBOY
WBOY転載
2022-11-02 17:35:482357ブラウズ

この記事では、Redis に関する関連知識を提供します。主に、キャッシュ タイプの紹介、さまざまな使用シナリオ、使用方法など、分散キャッシュとローカル キャッシュの使用スキルを紹介し、最後に実践的な事例を挙げますので、ぜひ参考にしてみてください。

推奨される学習: Redis ビデオ チュートリアル

ご存知のとおり、キャッシュの主な目的は、アクセスを高速化し、アクセスを軽減することです。データベースのプレッシャー。最も一般的に使用されるキャッシュは、redis などの分散キャッシュです。ほとんどの同時実行シナリオや、一部の中小企業のトラフィックがそれほど高くない状況に直面した場合、基本的には redis で問題を解決できます。ただし、トラフィックが多い場合は、guava の LoadingCache や Kuaishou のオープン ソース ReloadableCache などのローカル キャッシュを使用する必要がある場合があります。

3 つのキャッシュ使用シナリオ

このパートでは、guava の LoadingCache や Kuaishou のオープン ソース ReloadableCache などの Redis の使用シナリオと制限事項を紹介します。ビジネス シナリオでどのキャッシュを使用する必要があるか、またその理由を説明します。

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 を使用するだけでは明らかに十分ではありません。

ReloadableCache の使用シナリオと制限事項

LoadingCache では解決できない上記のリアルタイム問題については、Kuaishou がオープンソース化しているローカル キャッシュ フレームワークである ReloadableCache の使用を検討できます。同時に複数のマシンをサポートしていることです キャッシュを更新します ホームページの情報を変更すると、リクエストがマシン A に到達するとします。このとき、ReloadableCache が再ロードされ、通知が送信されます。 その他同じ zk ノードをリッスンしているマシンは、通知を受信した後にキャッシュを更新します。このキャッシュを使用するための一般的な要件は、全量のデータをローカル キャッシュにロードすることです。そのため、データ量が大きすぎると確実に gc を圧迫するため、この場合は使用できません。 Xiao Ai のホームページにはステータスがあり、通常オンライン ステータスは 2 つだけなので、ReloadableCache を使用してオンライン ステータスのホームページのみを読み込むことができます。

概要

3 種類のキャッシュについては基本的にここで紹介しましたが、概要は次のとおりです:

  • ユーザー次元などの非ホットスポット データ アクセスの場合データ、直接 redis を使用してください;
  • ホット データ アクセスの場合、トラフィックがそれほど多くない場合は、何も考えずに redis を使用してください;
  • ホット データの場合、特定の範囲内でダーティ データが許可されている場合一定の期間、LoadingCache を使用するだけで十分です。
  • ホット データの場合、一貫性要件が高く、データ量が大きくない場合は、ReloadableCache を使用します。

ヒント

どのような種類のローカル キャッシュにも、故障の問題を解決するために仮想マシン レベルのロックが備わっていますが、予期せぬ形で事故が発生する可能性は常にあります。念のため、2 レベルのキャッシュ、つまりローカル キャッシュを使用できます。 Redis データベース。

キャッシュの使用法についての簡単な紹介

ここでは Redis の使用法については説明しませんが、多くの人が私よりも API の使用法に精通していると思います

LoadingCache の使い方

これは guava online で提供されていますが、注意点が 2 点あります。

V get(K key, Callabled1336e9e686742fb22c1860557381905loader)
    ; を使用するか、ビルドを使用する場合は
  • build(CacheLoader2ffe6bce707a67f84d4df961dbeb183aloader) を使用してください。 、 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
  • reloadableCache の使用法

サードパーティ依存関係のインポート
<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 つは本当に永遠の問題であり、トラフィックが大きい場合は実際に考慮する必要があります。

キャッシュの内訳

簡単に言うと、キャッシュが失敗し、多数のリクエストが同時にデータベースにヒットします。キャッシュの故障の問題については、上で多くの解決策が示されています。

#たとえば、ローカル キャッシュを使用します

ローカル キャッシュはロードミス方式を使用します
  • サードパーティ サービスを使用してキャッシュをロードします
  • 1.2 と先ほども言いましたが、主に 3 を見てください。たとえば、企業が Redis を使用するつもりでもローカル キャッシュを使用できない場合は、データ量が多すぎてリアルタイム要件が比較的高いことが考えられます。次に、キャッシュに障害が発生した場合、少数のリクエストのみがデータベースにヒットするようにする方法を見つける必要があります。分散ロックの使用を考えるのは自然であり、理論的には実現可能ですが、実際には隠れた危険性があります。私たちは、多くの人が redis lua を使用して分散ロックを実装し、その間にローテーション トレーニングを実行していると考えています。そのような多数のリクエストとデータにより、redis は隠れた危険となり、多くのビジネスを占有してしまいます。スレッドは実際には、単にそれだけで複雑さを増大させます。分散ロックの導入 私たちの原則は、可能な限り分散ロックを使用しないことです。
  • それでは、分散ロックに似ているが、より信頼性の高い rpc サービスを設計できるでしょうか? get メソッドを呼び出すと、この rpc サービスは同じキーが同じノードにヒットすることを確認し、synchronized を使用してロックし、データのロードを完了します。 Kuaishou は、cacheSetter と呼ばれるフレームワークを提供します。以下に簡略版を示します。自分で作成することで簡単に実装できます。
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进行处理
   	// 设置缓存
    }
}

キャッシュ侵入

簡単に言えば、要求されたデータがデータベースに存在しないため、無効なリクエストがデータベースに侵入することになります。 。 解決策も非常に簡単で、db からデータを取得するメソッド (getByKey(K key)) でデフォルト値を指定する必要があります。

たとえば、上限が 1W の賞金プールがあるとします。ユーザーがタスクを完了したら、お金を送り、redis を使用して記録し、テーブルにログインします。ユーザーは次のことを確認できます。タスクページ上で賞金プールの残高をリアルタイムで確認できます タスク開始時点では、賞金プールの金額が変化していないことがわかります redisとdbには発行額の記録がありません毎回 DB をチェックする必要があるため、DB からデータが見つからない場合は値をキャッシュする必要があります。0 を指定するとキャッシュされます。

キャッシュ雪崩

これは、大量のキャッシュ障害がデータベースに発生したことを意味します。もちろん、それらはすべてビジネス キャッシュである必要があります。最終的には、次の問題があります。コードの書き方。キャッシュ無効化の有効期限を分割して、中央で失敗させないようにすることができます。

推奨される学習: Redis ビデオ チュートリアル

以上がRedis とローカル キャッシュを使用した高同時実行技術の共有の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjb51.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。