java手書きredisゼロから(10)キャッシュ除去アルゴリズムLFU最小使用頻度

序文

Javaは最初からredisを手動で実装します(1)固定サイズのキャッシュを実現するにはどうすればよいですか?

Javaはredisを最初から手作業で実装します(3)redisの有効期限の原則

Javaは最初から手動でredisを実装します(3)メモリデータを失うことなく再起動する方法は?

Javaは手動でredisを最初から実装します(4つ)リスナーを追加します

redis(5)有効期限戦略を最初から実装する別の方法

Javaは最初から手動でredisを実装します(6)AOF永続性の原則の詳細と実装

Javaは最初からredisを手動で実装します(7)LRUキャッシュ除去戦略の詳細な説明

手書きのredisを最初から(8)単純なLRU除去アルゴリズムのパフォーマンスの最適化

このセクションでは、もう1つの一般的に使用されるキャッシュ除去アルゴリズムであるLFUの最小使用頻度アルゴリズムについて学習しましょう。

LFUの基本

概念

LFU(Least Frequently Used)は最近最も使用頻度が低いものです。名前を見るだけで、アクセス頻度に基づくアルゴリズムであることがわかります。

LRUは時間ベースであり、時間内に最もアクセス頻度の低いデータを排除し、アルゴリズムのパフォーマンスの観点からリストの一番上に配置します。LFUは、頻度において最もアクセス頻度の低いデータを排除します。

頻度に基づいているため、各データアクセスの回数を保存する必要があります。

ストレージスペースに関しては、LRUよりもカウントを保持するためのスペースが多くなります。

本旨

最近の期間にデータがほとんど使用されていない場合、将来使用される可能性は低くなります。

実現アイデア

O(N)削除

最も使用されていないデータを排除するために、私の最初の本能は直接1つでありHashMap<String, Interger>、Stringはキー情報に対応し、Integerは回数に対応します。

訪問するたびに+1に移動します。設定と読み取りの時間の複雑さはO(1)ですが、削除はより面倒で、すべての比較をトラバースする必要があり、時間の複雑さはO(n)です。

O(ログ)削除

別の実装アイデアは、スモールトップヒープ+ハッシュマップを使用することです。スモールトップヒープの挿入および削除操作は、O(logn)時間の複雑さを実現できるため、最初の実装方法よりも効率が高くなります。TreeMapなど。

O(1)削除

さらに最適化できますか?

実際、O(1)アルゴリズムがあります。このペーパーを参照してください。

LFUキャッシュエビクションスキームを実装するためのO(1)アルゴリズム

個人的な考えについて簡単に話します。

O(1)操作を実現したいのであれば、ハッシュ操作なしではいけません。O(N)削除では、O(1)put / getを実現します。

ただし、最小回数を見つけるのに時間がかかるため、削除のパフォーマンスは低下します。

private Map<K, Node> map; // key和数据的映射
private Map<Integer, LinkedHashSet<Node>> freqMap; // 数据频率和对应数据组成的链表

class Node {
    K key;
    V value;
    int frequency = 1;
}

基本的に、ダブルハッシュを使用することでこの問題を解決できます。

マップには、キーとノード間のマッピング関係が格納されます。プット/ゲットは間違いなくO(1)です。

キーによってマップされたノードには、対応する周波数情報があります。同じ周波数がfreqMapを介して関連付けられ、対応するリンクリストを周波数ごとにすばやく取得できます。

削除も非常に簡単になりました。基本的に、削除の最低頻度は1であると判断できます。1... n以下の場合、最小頻度でリンクリストの最初の要素を選択して削除を開始します。

リンクリスト自体の優先度については、FIFOまたはその他の任意の方法に基づくことができます。

紙のコアコンテンツの紹介

他の丘からの石は、学ぶことができます。

コードを実装する前に、このO(1)ペーパーを読んでみましょう。

前書き

この記事の構成は次のとおりです。

他のキャッシュエビクションアルゴリズムよりも優れていることが証明できるLFUユースケースの説明

LFUキャッシュの実装は、辞書操作をサポートする必要があります。これらは、戦略ランタイムの複雑さを決定する操作です

最も有名なLFUアルゴリズムとその実行時の複雑さの説明

提案されたLFUアルゴリズムの説明。各操作の実行時の複雑さはO(1)です。

LFUの使用

HTTPプロトコル用のキャッシングWebプロキシアプリケーションについて考えてみます。

プロキシは通常、インターネットとユーザーまたはユーザーのグループの間に配置されます。

これにより、すべてのユーザーがインターネットにアクセスし、共有可能なすべてのリソースの共有を実現して、最高のネットワーク使用率と応答速度を実現できます。

このようなキャッシングエージェントは、限られた量のストレージまたはメモリにキャッシュできるデータの量を最大化するように努める必要があります。

一般に、静的リソース(画像、CSSスタイルシート、javascriptコードなど)は、新しいバージョンに置き換える前に、長期間簡単にキャッシュできます。

これらの静的リソースまたはプログラマーが「アセット」と呼ぶものは、ほぼすべてのページに含まれているため、ほとんどすべての要求に必要になるため、それらをキャッシュすることが最も有益です。

さらに、ネットワークエージェントは1秒あたり数千の要求を処理する必要があるため、そのために必要なオーバーヘッドを最小限に抑える必要があります。

このため、頻繁に使用されないリソースのみを削除する必要があります。

したがって、頻繁に使用されるリソースは、使用頻度の低いリソースに保持する必要があります。前者は、一定期間にわたって有用であることが証明されているためです。

もちろん、逆に言えば、将来的にはリソースがあまり使われないかもしれないということですが、ほとんどの場合そうではないことがわかりました。

たとえば、頻繁に使用されるページの静的リソースは、ページの各ユーザーから常に要求されます。

したがって、メモリが不足している場合、これらのキャッシングエージェントは、LFUキャッシュ置換戦略を使用して、キャッシュ内で最も使用されていないアイテムを排出できます。

ここではLRUも適用可能な戦略ですが、要求モードによって要求されたすべてのアイテムがキャッシュに入らず、これらのアイテムがラウンドロビン方式で要求された場合、LRUは失敗します。

ps:データの周期的な要求により、LRUはこのシナリオに適応しなくなります。

LRUの場合、アイテムは引き続きキャッシュに出入りし、ユーザーがキャッシュへのアクセスを要求することはありません。

ただし、同じ条件下では、LFUアルゴリズムのパフォーマンスが向上し、ほとんどのキャッシュアイテムがキャッシュヒットを引き起こします。

LFUアルゴリズムの病理学的挙動は不可能ではありません。

ここではLFUの事例を紹介していませんが、LFUが適用可能な戦略である場合、以前に公開された方法よりも優れた実装方法があることを証明しようとしています。

LFUキャッシュでサポートされる辞書操作

キャッシュエビクションアルゴリズムについて話すとき、主にキャッシュされたデータに対して3つの異なる操作を実行する必要があります。

  1. キャッシュにアイテムを設定(または挿入)する

  2. キャッシュ内のアイテムを取得(または検索)すると同時に、使用数を増やします(LFUの場合)

  3. 最も使用されていない(または削除アルゴリズムの戦略として)キャッシュから削除(または削除)する

LFUアルゴリズムの現在最も有名な複雑さ

これを書いている時点で、LFUキャッシュエビクション戦略の上記の各操作の最も有名なランタイムは次のとおりです。

挿入:O(log n)

検索:O(log n)

削除:O(log n)

これらの複雑さの値は、二項ヒープの実装と標準の衝突のないハッシュテーブルから直接取得されます。

最小ヒープデータ構造とハッシュグラフを使用すると、LFUキャッシング戦略を簡単かつ効果的に実装できます。

最小ヒープは(アイテムの)使用回数に基づいて作成され、ハッシュテーブルは要素のキーによってインデックスが付けられます。

衝突のないハッシュテーブルでのすべての操作の順序はO(1)であるため、LFUキャッシュの実行時間は、最小のヒープでの操作の実行時間によって制御されます。

要素がキャッシュに挿入されると、使用回数1で入力されます。最小ヒープを挿入するオーバーヘッドはO(log n)であるため、LFUキャッシュに挿入するのにO(log n)時間がかかります。

要素を検索するとき、要素は実際の要素へのキーをハッシュするハッシュ関数を介して見つけることができます。同時に、使用カウント(最大のヒープ内のカウント)が1つ増加します。これにより、最小のヒープが再編成され、要素がルートから移動します。

要素はどの段階でもlog(n)レベルまで下がることができるため、この操作にも時間O(log n)がかかります。

要素が削除されるように選択され、最終的にヒープから削除されると、ヒープデータ構造が大幅に再編成される可能性があります。

使用回数が最も少ない要素は、最小のヒープのルートにあります。

最小のヒープのルートを削除するには、ルートノードをヒープ内の最後のリーフノードに置き換え、ノードを正しい位置にバブリングする必要があります。

この操作の実行時の複雑さもO(log n)です。

提案されたLFUアルゴリズム

LFUキャッシュで実行できる各辞書操作(挿入、ルックアップ、および削除)について、提案されたLFUアルゴリズムの実行時の複雑さはO(1)です。

これは、2つのリンクされたリストを維持することによって実現されます。1つはアクセス頻度用で、もう1つは同じアクセス頻度を持つすべての要素用です。

ハッシュテーブルは、キーで要素にアクセスするために使用されます(わかりやすくするために下の図には示されていません)。

二重リンクリストは、同じアクセス頻度を持つノードのグループを表すノードをリンクするために使用されます(下の図では長方形のブロックとして示されています)。

この二重リンクリストを周波数リストと呼びます。同じアクセス頻度を持つノードのグループは、実際にはそのようなノードの双方向リンクリストです(下の図では円形ノードとして示されています)。

この双方向リンクリスト(特定の頻度でローカル)をノードリストと呼びます。

ノードリストの各ノードには、その親ノードへのポインタがあります。

周波数リスト(わかりやすくするために図には示されていません)。したがって、ノードxとノード1へのポインタがあり、ノードzとaにはノード2へのポインタがあります。

写真の説明を入力してください

以下の疑似コードは、LFUキャッシュを初期化する方法を示しています。

キーによって要素を見つけるために使用されるハッシュテーブルは、キー変数によって表されます。

実装を簡素化するために、リンクリストの代わりにSETを使用して、同じアクセス頻度で要素を格納します。

変数項目は標準のSETデータ構造であり、同じアクセス頻度を持つそのような要素のキーが含まれています。

その挿入、検索、および削除の実行時の複雑さはO(1)です。

写真の説明を入力してください

偽のコード

以下はいくつかの疑似コードです、私たちは国内です。

そのコアアイデアを理解するだけで、以下の実際のコードに行きましょう。

感じる

このO(1)アルゴリズムのコアは実際にはそれほど多くないため、リートコードに配置すると中程度の難易度の問題と見なす必要があります。

しかし、この論文が2010年に提案されたのは不思議であり、以前はO(logn)が限界だったと推定されています。

javaコードの実装

基本属性

public class CacheEvictLfu<K,V> extends AbstractCacheEvict<K,V> {

    private static final Log log = LogFactory.getLog(CacheEvictLfu.class);

    /**
     * key 映射信息
     * @since 0.0.14
     */
    private final Map<K, FreqNode<K,V>> keyMap;

    /**
     * 频率 map
     * @since 0.0.14
     */
    private final Map<Integer, LinkedHashSet<FreqNode<K,V>>> freqMap;

    /**
     *
     * 最小频率
     * @since 0.0.14
     */
    private int minFreq;

    public CacheEvictLfu() {
        this.keyMap = new HashMap<>();
        this.freqMap = new HashMap<>();
        this.minFreq = 1;
    }

}

ノード定義

  • FreqNode.java
public class FreqNode<K,V> {

    /**
     * 键
     * @since 0.0.14
     */
    private K key;

    /**
     * 值
     * @since 0.0.14
     */
    private V value = null;

    /**
     * 频率
     * @since 0.0.14
     */
    private int frequency = 1;

    public FreqNode(K key) {
        this.key = key;
    }

    //fluent getter & setter
    // toString() equals() hashCode()
}

要素を削除します

/**
 * 移除元素
 *
 * 1. 从 freqMap 中移除
 * 2. 从 keyMap 中移除
 * 3. 更新 minFreq 信息
 *
 * @param key 元素
 * @since 0.0.14
 */
@Override
public void removeKey(final K key) {
    FreqNode<K,V> freqNode = this.keyMap.remove(key);
    //1. 根据 key 获取频率
    int freq = freqNode.frequency();
    LinkedHashSet<FreqNode<K,V>> set = this.freqMap.get(freq);
    //2. 移除频率中对应的节点
    set.remove(freqNode);
    log.debug("freq={} 移除元素节点:{}", freq, freqNode);
    //3. 更新 minFreq
    if(CollectionUtil.isEmpty(set) && minFreq == freq) {
        minFreq--;
        log.debug("minFreq 降低为:{}", minFreq);
    }
}

要素を更新

/**
 * 更新元素,更新 minFreq 信息
 * @param key 元素
 * @since 0.0.14
 */
@Override
public void updateKey(final K key) {
    FreqNode<K,V> freqNode = keyMap.get(key);
    //1. 已经存在
    if(ObjectUtil.isNotNull(freqNode)) {
        //1.1 移除原始的节点信息
        int frequency = freqNode.frequency();
        LinkedHashSet<FreqNode<K,V>> oldSet = freqMap.get(frequency);
        oldSet.remove(freqNode);
        //1.2 更新最小数据频率
        if (minFreq == frequency && oldSet.isEmpty()) {
            minFreq++;
            log.debug("minFreq 增加为:{}", minFreq);
        }
        //1.3 更新频率信息
        frequency++;
        freqNode.frequency(frequency);
        //1.4 放入新的集合
        this.addToFreqMap(frequency, freqNode);
    } else {
        //2. 不存在
        //2.1 构建新的元素
        FreqNode<K,V> newNode = new FreqNode<>(key);
        //2.2 固定放入到频率为1的列表中
        this.addToFreqMap(1, newNode);
        //2.3 更新 minFreq 信息
        this.minFreq = 1;
        //2.4 添加到 keyMap
        this.keyMap.put(key, newNode);
    }
}

/**
 * 加入到频率 MAP
 * @param frequency 频率
 * @param freqNode 节点
 */
private void addToFreqMap(final int frequency, FreqNode<K,V> freqNode) {
    LinkedHashSet<FreqNode<K,V>> set = freqMap.get(frequency);
    if (set == null) {
        set = new LinkedHashSet<>();
    }
    set.add(freqNode);
    freqMap.put(frequency, set);
    log.debug("freq={} 添加元素节点:{}", frequency, freqNode);
}

データの削除

@Override
protected ICacheEntry<K, V> doEvict(ICacheEvictContext<K, V> context) {
    ICacheEntry<K, V> result = null;
    final ICache<K,V> cache = context.cache();
    // 超过限制,移除频次最低的元素
    if(cache.size() >= context.size()) {
        FreqNode<K,V> evictNode = this.getMinFreqNode();
        K evictKey = evictNode.key();
        V evictValue = cache.remove(evictKey);
        log.debug("淘汰最小频率信息, key: {}, value: {}, freq: {}",
                evictKey, evictValue, evictNode.frequency());
        result = new CacheEntry<>(evictKey, evictValue);
    }
    return result;
}

/**
 * 获取最小频率的节点
 *
 * @return 结果
 * @since 0.0.14
 */
private FreqNode<K, V> getMinFreqNode() {
    LinkedHashSet<FreqNode<K,V>> set = freqMap.get(minFreq);
    if(CollectionUtil.isNotEmpty(set)) {
        return set.iterator().next();
    }
    throw new CacheRuntimeException("未发现最小频率的 Key");
}

テスト

コード

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .size(3)
        .evict(CacheEvicts.<String, String>lfu())
        .build();
cache.put("A", "hello");
cache.put("B", "world");
cache.put("C", "FIFO");
// 访问一次A
cache.get("A");
cache.put("D", "LRU");

Assert.assertEquals(3, cache.size());
System.out.println(cache.keySet());

ログ

[DEBUG] [2020-10-03 21:23:43.722] [main] [c.g.h.c.c.s.e.CacheEvictLfu.addToFreqMap] - freq=1 添加元素节点:FreqNode{key=A, value=null, frequency=1}
[DEBUG] [2020-10-03 21:23:43.723] [main] [c.g.h.c.c.s.e.CacheEvictLfu.addToFreqMap] - freq=1 添加元素节点:FreqNode{key=B, value=null, frequency=1}
[DEBUG] [2020-10-03 21:23:43.725] [main] [c.g.h.c.c.s.e.CacheEvictLfu.addToFreqMap] - freq=1 添加元素节点:FreqNode{key=C, value=null, frequency=1}
[DEBUG] [2020-10-03 21:23:43.727] [main] [c.g.h.c.c.s.e.CacheEvictLfu.addToFreqMap] - freq=2 添加元素节点:FreqNode{key=A, value=null, frequency=2}
[DEBUG] [2020-10-03 21:23:43.728] [main] [c.g.h.c.c.s.e.CacheEvictLfu.doEvict] - 淘汰最小频率信息, key: B, value: world, freq: 1
[DEBUG] [2020-10-03 21:23:43.731] [main] [c.g.h.c.c.s.l.r.CacheRemoveListener.listen] - Remove key: B, value: world, type: evict
[DEBUG] [2020-10-03 21:23:43.732] [main] [c.g.h.c.c.s.e.CacheEvictLfu.addToFreqMap] - freq=1 添加元素节点:FreqNode{key=D, value=null, frequency=1}
[D, A, C]

LFUとLRU

違い

LFUはアクセス頻度に基づくモードであり、LRUはアクセス時間に基づくモードです。

利点

データアクセスが通常の分布に準拠している場合、LFUアルゴリズムのキャッシュヒット率はLRUアルゴリズムのキャッシュヒット率よりも高くなります。

不利益

  • LFUの複雑さはLRUの複雑さよりも高くなっています。

  • データアクセスの頻度を維持し、各アクセスを更新する必要があります。

  • 初期のデータは後のデータよりもキャッシュしやすいため、後のデータをキャッシュすることは困難です。

  • キャッシュに新しく追加されたデータは、キャッシュの最後の「ジッター」など、簡単に削除できます。

概要

ただし、実際には、LFUのアプリケーションシナリオはそれほど広範囲ではありません。

実際のデータは歪んでおり、ホットデータが標準であるため、LRUのパフォーマンスは一般にLFUのパフォーマンスよりも優れています。

オープンソースアドレス:https://github.com/houbb/cache

この記事が役に立ったと思ったら、いいね、コメント、集めて、波をたどってください。あなたの励ましが私の最大の動機です〜

現在、LRUとLFUのアルゴリズムを優れたパフォーマンスで実装していますが、オペレーティングシステムは実際にはこれら2つのアルゴリズムを使用しています。次のセクションでは、オペレーティングシステムが好むクロック除去アルゴリズムについて学習します。

あなたが何を得たのか分かりませんか?または、他にもアイデアがある場合は、メッセージエリアで私と話し合うことを歓迎し、あなたの考えに会うことを楽しみにしています。

深い学習

おすすめ

転載: blog.51cto.com/9250070/2540213
おすすめ