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

序文

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

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

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

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

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

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

redisのいくつかの機能を実装しただけです。Javaはredisを最初から手動で実装します(1)固定サイズのキャッシュを実現するにはどうすればよいですか?ファーストイン、ファーストアウトの排除戦略が中国で実施されました。

ただし、実際の作業では、通常、LRU / LFU除去戦略を使用することをお勧めします。

LRUの基本

それは何ですか

LRUアルゴリズムの正式名称は、Least Recent Useアルゴリズムであり、キャッシングメカニズムで広く使用されています。

キャッシュが使用するスペースが上限に達すると、キャッシュの可用性を維持するために既存のデータの一部を削除する必要があり、削除されたデータの選択はLRUアルゴリズムによって完了します。

LRUアルゴリズムの基本的な考え方は、局所性の原則に基づく時間の局所性です:

情報項目にアクセスしている場合、近い将来再びアクセスされる可能性があります。

参考文献

Apache CommonsLRUMAPソースコードの詳細な説明

LRUMAPとして使用されるRedis

Java手書きredisを最初から(7)redisLRU除外戦略の詳細な説明と実装

簡単な実装のアイデア

配列に基づく

解決策:各データに追加の属性(タイムスタンプ)を添付し、データにアクセスするたびにデータのタイムスタンプを現在の時刻に更新します。

データスペースがいっぱいになると、アレイ全体がスキャンされ、タイムスタンプが最小のデータが削除されます。

不十分:タイムスタンプを維持するには追加のスペースが必要であり、データを削除するときにアレイ全体をスキャンする必要があります。

今回は複雑すぎて、スペースの複雑さも良くありません。

限られた長さの二重にリンクされたリストに基づく

解決策:データにアクセスするときに、データがリンクリストにない場合は、データをリンクリストの先頭に挿入し、リンクリストにある場合は、データをリンクリストの先頭に移動します。データスペースがいっぱいになると、リンクリストの最後にあるデータが削除されます。

不十分:データを挿入またはフェッチする場合、リンクされたリスト全体をスキャンする必要があります。

これは前のセクションで実装した方法です。欠点はまだ明らかです。要素が存在するかどうかを確認するたびに、クエリを実行するのにO(n)時間の複雑さが必要です。

二重にリンクされたリストとハッシュテーブルに基づく

解決策:リンクリストをスキャンする必要がある上記の欠陥を改善するには、ハッシュテーブルと連携して、リンクリスト内のデータとノードをマッピングし、挿入操作と読み取り操作の時間の複雑さをO(N)からO(1)に減らします。

短所:これにより、前のセクションで説明した最適化のアイデアが得られますが、それでも短所があります。つまり、スペースの複雑さが2倍になります。

データ構造の選択

(1)アレイベースの実装

読み取りの時間の複雑さはO(1)であるため、ここでarrayまたはArrayListを選択することはお勧めしませんが、jdkはSystem.arrayCopyを使用しますが、更新は比較的低速です。

(2)リンクリストに基づく実装

リンクリストを選択した場合、キーと対応する添え字を単純にHashMapに保存することはできません。

リンクリストのトラバーサルは実際にはO(n)であるため、二重リンクリストは理論的には半分に最適化できますが、これは必要なO(1)効果ではありません。

(3)双方向リストに基づく

二重にリンクされたリストは変更されません。

二重にリンクされたリストのノード情報を、キーに対応する値としてマップに配置します。

実現方法は、二重にリンクされたリストの実現になります。

コード

  • ノード定義
/**
 * 双向链表节点
 * @author binbin.hou
 * @since 0.0.12
 * @param <K> key
 * @param <V> value
 */
public class DoubleListNode<K,V> {

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

    /**
     * 值
     * @since 0.0.12
     */
    private V value;

    /**
     * 前一个节点
     * @since 0.0.12
     */
    private DoubleListNode<K,V> pre;

    /**
     * 后一个节点
     * @since 0.0.12
     */
    private DoubleListNode<K,V> next;

    //fluent get & set
}
  • コアコードの実装

元のインターフェイスは変更せず、実装は次のとおりです。

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

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

    /**
     * 头结点
     * @since 0.0.12
     */
    private DoubleListNode<K,V> head;

    /**
     * 尾巴结点
     * @since 0.0.12
     */
    private DoubleListNode<K,V> tail;

    /**
     * map 信息
     *
     * key: 元素信息
     * value: 元素在 list 中对应的节点信息
     * @since 0.0.12
     */
    private Map<K, DoubleListNode<K,V>> indexMap;

    public CacheEvictLruDoubleListMap() {
        this.indexMap = new HashMap<>();
        this.head = new DoubleListNode<>();
        this.tail = new DoubleListNode<>();

        this.head.next(this.tail);
        this.tail.pre(this.head);
    }

    @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()) {
            // 获取尾巴节点的前一个元素
            DoubleListNode<K,V> tailPre = this.tail.pre();
            if(tailPre == this.head) {
                log.error("当前列表为空,无法进行删除");
                throw new CacheRuntimeException("不可删除头结点!");
            }

            K evictKey = tailPre.key();
            V evictValue = cache.remove(evictKey);
            result = new CacheEntry<>(evictKey, evictValue);
        }

        return result;
    }

    /**
     * 放入元素
     *
     * (1)删除已经存在的
     * (2)新元素放到元素头部
     *
     * @param key 元素
     * @since 0.0.12
     */
    @Override
    public void update(final K key) {
        //1. 执行删除
        this.remove(key);

        //2. 新元素插入到头部
        //head<->next
        //变成:head<->new<->next
        DoubleListNode<K,V> newNode = new DoubleListNode<>();
        newNode.key(key);

        DoubleListNode<K,V> next = this.head.next();
        this.head.next(newNode);
        newNode.pre(this.head);
        next.pre(newNode);
        newNode.next(next);

        //2.2 插入到 map 中
        indexMap.put(key, newNode);
    }

    /**
     * 移除元素
     *
     * 1. 获取 map 中的元素
     * 2. 不存在直接返回,存在执行以下步骤:
     * 2.1 删除双向链表中的元素
     * 2.2 删除 map 中的元素
     *
     * @param key 元素
     * @since 0.0.12
     */
    @Override
    public void remove(final K key) {
        DoubleListNode<K,V> node = indexMap.get(key);

        if(ObjectUtil.isNull(node)) {
            return;
        }

        // 删除 list node
        // A<->B<->C
        // 删除 B,需要变成: A<->C
        DoubleListNode<K,V> pre = node.pre();
        DoubleListNode<K,V> next = node.next();

        pre.next(next);
        next.pre(pre);

        // 删除 map 中对应信息
        this.indexMap.remove(key);
    }

}

実装は難しくありません。単純な双方向のリストです。

ノードを取得するときは、マップを使用して時間の複雑さをO(1)に減らします。

テスト

実装を確認しましょう:

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .size(3)
        .evict(CacheEvicts.<String, String>lruDoubleListMap())
        .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 09:37:41.007] [main] [c.g.h.c.c.s.l.r.CacheRemoveListener.listen] - Remove key: B, value: world, type: evict
[D, A, C]

一度Aを訪れたので、Bは最も訪問されていない要素になりました。

LinkedHashMapに基づく

実際、LinkedHashMap自体はlistとhashMapを組み合わせたデータ構造であり、jdkでLinkedHashMapを直接使用して実現できます。

直接実現

public class LRUCache extends LinkedHashMap {

    private int capacity;

    public LRUCache(int capacity) {
        // 注意这里将LinkedHashMap的accessOrder设为true
        super(16, 0.75f, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return super.size() >= capacity;
    }
}

デフォルトのLinkedHashMapはデータを削除しないため、removeEldestEntry()メソッドを書き直します。データ数が事前設定された上限に達すると、データは削除されます。accessOrderをtrueに設定すると、アクセス順に並べ替えることができます。

主にLinkedHashMapの特性を使用して、実装全体のコードの量は多くありません。

単純な変換

このメソッドを変更して、定義したインターフェイスに適合させました。

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .size(3)
        .evict(CacheEvicts.<String, String>lruLinkedHashMap())
        .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());

テスト

  • コード
ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .size(3)
        .evict(CacheEvicts.<String, String>lruLinkedHashMap())
        .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 10:20:57.842] [main] [c.g.h.c.c.s.l.r.CacheRemoveListener.listen] - Remove key: B, value: world, type: evict
[D, A, C]

概要

前のセクションで説明したアレイO(n)トラバーサルの問題は、このセクションで基本的に解決されています。

しかし実際には、このアルゴリズムにはまだ特定の問題があります。たとえば、時折バッチ操作を行うと、ホットデータが非ホットデータによってキャッシュから絞り出されます。次のセクションでは、LRUアルゴリズムをさらに改善する方法を学習します。

記事は主にアイデアについて述べていますが、スペースの制限のため、実現部分はすべて掲載されていません。

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

この記事がお役に立てば幸いです。いいね、コメント、ブックマーク、そして波をフォローしてください〜

あなたの励ましが私の最大の動機です〜

深い学習

おすすめ

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