Java handwritten redis from scratch (9) LRU 캐시 제거 알고리즘이 캐시 오염을 방지하는 방법

머리말

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 제거 알고리즘 성능 최적화

처음 두 섹션에서는 LRU 알고리즘을 구현하고 성능을 최적화했습니다.

LRU 알고리즘의 마지막 섹션 인이 섹션은 주로 캐시 오염 문제를 해결합니다.

LRU 기본 사항

뭐야

LRU 알고리즘의 전체 이름은 캐싱 메커니즘에서 널리 사용되는 최소 최근 사용 알고리즘입니다.

캐시가 사용하는 공간이 상한에 도달하면 캐시의 가용성을 유지하기 위해 기존 데이터의 일부를 제거해야하며, 제거 된 데이터의 선택은 LRU 알고리즘을 통해 완료됩니다.

LRU 알고리즘의 기본 아이디어는 지역성 원칙에 기반한 시간 지역성입니다.

정보 항목에 액세스중인 경우 가까운 장래에 다시 액세스 할 가능성이 있습니다.

추가 읽기

Apache Commons LRUMAP 소스 코드 상세 설명

LRU MAP로 사용되는 Redis

Java 손으로 쓴 redis 처음부터 (7) redis LRU 제외 전략의 자세한 설명 및 구현

순진한 LRU 알고리즘의 단점

핫 데이터가있는 경우 LRU는 매우 효율적이지만 가끔씩 주기적으로 배치 작업을 수행하면 LRU 적중률이 급격히 떨어지고 캐시 오염이 더욱 심각해집니다.

확장 알고리즘

1. LRU-K

LRU-K의 K는 최근 사용 횟수를 나타내므로 LRU는 LRU-1로 간주 할 수 있습니다.

LRU-K의 주요 목적은 LRU 알고리즘의 "캐시 오염"문제를 해결하는 것이며, 핵심 아이디어는 "최근 1 회 사용"기준을 "최근 K 회 사용"으로 확장하는 것입니다.

LRU와 비교하여 LRU-K는 액세스중인 모든 캐시 된 데이터의 기록을 기록하기 위해 하나 이상의 대기열을 유지해야합니다. 데이터 액세스 횟수가 K 회에 도달 할 때만 데이터가 캐시에 저장됩니다.

데이터를 제거해야하는 경우 LRU-K는 현재 시간에서 K 번째 액세스 시간이 가장 큰 데이터를 제거합니다.

데이터에 처음 접근하면 이력 접근 목록에 추가되며, 접근 이력 목록에서 데이터가 K 회에 도달하지 않을 경우 특정 규칙 (FIFO, LRU)에 따라 삭제됩니다.

액세스 히스토리 큐의 데이터 액세스 횟수가 K 회에 도달하면 히스토리 큐에서 데이터 인덱스가 삭제되고 데이터가 캐시 큐로 이동되고 데이터가 캐시되고 캐시 큐가 다시 시간별로 정렬됩니다.

캐시 데이터 큐에 다시 액세스 한 후 다시 정렬됩니다. 데이터를 제거해야하는 경우 캐시 큐의 끝에있는 데이터가 제거됩니다. 즉, "마지막 K 회 액세스가 가장 긴 데이터 제거"입니다.

LRU-K는 LRU의 장점을 가지고있는 동시에 LRU의 단점을 피할 수 있습니다. 실제 응용에서 LRU-2는 가장 포괄적 인 선택입니다.

LRU-K는 액세스되었지만 캐시에 저장되지 않은 개체도 기록해야하므로 메모리 소비는 LRU보다 많습니다.

2. 두 개의 대기열

두 개의 큐 (다음은 2Q를 대신 사용) 알고리즘은 LRU-2와 유사합니다. 차이점은 2Q가 LRU-2 알고리즘 (캐시 데이터가 아님)의 액세스 히스토리 큐를 FIFO 버퍼 큐로 변경한다는 것입니다. 즉, 2Q 알고리즘에는 두 개가 있습니다. 두 개의 버퍼 큐가 있습니다. 하나는 FIFO 큐이고 다른 하나는 LRU 큐입니다.

데이터에 처음 액세스하면 2Q 알고리즘은 FIFO 대기열의 데이터를 버퍼링합니다. 데이터에 두 번째 액세스하면 FIFO 대기열에서 LRU 대기열로 데이터를 이동합니다. 두 대기열은 자체 방법에 따라 데이터를 제거합니다.

새로 액세스 한 데이터는 FIFO 대기열에 삽입되며, 데이터가 FIFO 대기열에서 다시 액세스되지 않으면 결국 FIFO 규칙에 따라 제거됩니다.

FIFO 대기열에서 데이터에 다시 액세스하면 데이터는 LRU 대기열의 헤드로 이동하고, LRU 대기열에서 데이터에 다시 액세스하면 데이터는 LRU 대기열의 헤드로 이동하고 LRU 대기열은 끝에서 데이터를 제거합니다.

3. 많은 대기열 (MQ)

MQ 알고리즘은 액세스 빈도에 따라 데이터를 여러 큐로 분할합니다. 큐마다 액세스 우선 순위가 다릅니다. 핵심 아이디어는 액세스 시간이 많은 데이터의 우선 순위지정하는 것입니다 .

자세한 알고리즘 구조 다이어그램은 다음과 같습니다. Q0, Q1 ... Qk는 서로 다른 우선 순위 큐를 나타내고 Q-history는 캐시에서 데이터를 제거하는 큐를 나타내지 만 데이터의 인덱스와 참조 수를 기록합니다.

새로 삽입 된 데이터는 Q0에 넣고 각 큐는 LRU에 따라 관리되며, 데이터 액세스 횟수가 일정 수에 도달하여 우선 순위를 높여야 할 경우 현재 큐에서 데이터를 삭제하고 상위 큐의 헤드에 추가합니다. 우선 순위가 높은 데이터가 절대 제거되지 않도록하려면 지정된 시간 내에 데이터에 액세스하지 않으면 우선 순위를 낮추고 현재 큐에서 데이터를 삭제 한 다음 하위 레벨 큐의 헤드에 추가해야합니다. 데이터를 제거해야하는 경우, 최하위 큐부터 LRU에 따라 제거되며, 각 큐가 데이터를 제거하면 캐시에서 데이터가 삭제되고 Q-history 헤더에 데이터 인덱스가 추가됩니다.

데이터가 Q- 히스토리에서 재검토되면 우선 순위가 재 계산되어 대상 대기열의 헤드로 이동됩니다.

Q-history는 LRU에 따라 데이터 인덱스를 제거합니다.

MQ는 여러 큐를 유지해야하며 각 데이터의 액세스 시간을 유지해야하는데 이는 LRU보다 더 복잡합니다.

LRU 알고리즘 비교

대비 점 비교
타율 LRU-2> MQ (2)> 2Q> LRU
복잡성 LRU-2> MQ (2)> 2Q> LRU
비용 LRU-2> MQ (2)> 2Q> LRU

개인적인 이해

사실 위의 알고리즘은 생각이 비슷합니다.

핵심 목적 : 일괄 작업으로 인한 핫 데이터 무효화 및 캐시 오염 문제를 해결합니다.

구현 : 한 번만 액세스 한 데이터를 저장하는 대기열을 추가 한 다음 횟수에 따라 LRU에 넣습니다.

한 번만 액세스되는 큐는 FIFO 큐 또는 LRU 일 수 있습니다. 2Q 및 LRU-2의 두 가지 구현을 구현해 보겠습니다.

2Q

실현 아이디어

실제로 이전 FIFO + LRU의 조합입니다.

암호

기본 속성

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

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

    /**
     * 队列大小限制
     *
     * 降低 O(n) 的消耗,避免耗时过长。
     * @since 0.0.13
     */
    private static final int LIMIT_QUEUE_SIZE = 1024;

    /**
     * 第一次访问的队列
     * @since 0.0.13
     */
    private Queue<K> firstQueue;

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

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

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

    public CacheEvictLru2Q() {
        this.firstQueue = new LinkedList<>();
        this.lruIndexMap = new HashMap<>();
        this.head = new DoubleListNode<>();
        this.tail = new DoubleListNode<>();

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

}

데이터 제거

데이터 제거 논리 :

캐시 크기가 최대 한도에 도달하면 실행합니다.

(1) firstQueue에서 데이터 제거 우선 순위 지정

(2) firstQueue의 데이터가 비어 있으면 lruMap의 데이터 정보가 제거됩니다.

다음은 가정입니다. 여러 번 액세스 한 데이터가 한 번만 액세스 한 데이터보다 더 중요하다고 생각합니다.

@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()) {
        K evictKey = null;
        //1. firstQueue 不为空,优先移除队列中元素
        if(!firstQueue.isEmpty()) {
            evictKey = firstQueue.remove();
        } else {
            // 获取尾巴节点的前一个元素
            DoubleListNode<K,V> tailPre = this.tail.pre();
            if(tailPre == this.head) {
                log.error("当前列表为空,无法进行删除");
                throw new CacheRuntimeException("不可删除头结点!");
            }
            evictKey = tailPre.key();
        }
        // 执行移除操作
        V evictValue = cache.remove(evictKey);
        result = new CacheEntry<>(evictKey, evictValue);
    }
    return result;
}

데이터 삭제

데이터가 삭제되면 호출됩니다.

이 논리는 하나 이상의 FIFO 대기열이 제거된다는 점을 제외하면 이전과 유사합니다.

/**
 * 移除元素
 *
 * 1. 获取 map 中的元素
 * 2. 不存在直接返回,存在执行以下步骤:
 * 2.1 删除双向链表中的元素
 * 2.2 删除 map 中的元素
 *
 * @param key 元素
 * @since 0.0.13
 */
@Override
public void removeKey(final K key) {
    DoubleListNode<K,V> node = lruIndexMap.get(key);
    //1. LRU 删除逻辑
    if(ObjectUtil.isNotNull(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.lruIndexMap.remove(node.key());
    } else {
        //2. FIFO 删除逻辑(O(n) 时间复杂度)
        firstQueue.remove(key);
    }
}

데이터 업데이트

데이터에 액세스하면 데이터의 우선 순위를 높입니다.

(1) lruMap에 있다면 먼저 제거한 다음 헤드에 넣습니다.

(2) lruMap이 아니라 FIFO 대기열에있는 경우 FIFO 대기열에서 제거되고 LRU 맵에 추가됩니다.

(3) 없으면 FIFO 대기열에 추가하십시오.

/**
 * 放入元素
 * 1. 如果 lruIndexMap 已经存在,则处理 lru 队列,先删除,再插入。
 * 2. 如果 firstQueue 中已经存在,则处理 first 队列,先删除 firstQueue,然后插入 Lru。
 * 1 和 2 是不同的场景,但是代码实际上是一样的,删除逻辑中做了二种场景的兼容。
 *
 * 3. 如果不在1、2中,说明是新元素,直接插入到 firstQueue 的开始即可。
 *
 * @param key 元素
 * @since 0.0.13
 */
@Override
public void updateKey(final K key) {
    //1.1 是否在 LRU MAP 中
    //1.2 是否在 firstQueue 中
    DoubleListNode<K,V> node = lruIndexMap.get(key);
    if(ObjectUtil.isNotNull(node)
        || firstQueue.contains(key)) {
        //1.3 删除信息
        this.removeKey(key);
        //1.4 加入到 LRU 中
        this.addToLruMapHead(key);
        return;
    }
    //2. 直接加入到 firstQueue 队尾
    //        if(firstQueue.size() >= LIMIT_QUEUE_SIZE) {
//            // 避免第一次访问的列表一直增长,移除队头的元素
//            firstQueue.remove();
//        }
    firstQueue.add(key);
}

순회 시간 복잡성이 O (n)이므로 최대 크기가 1024로 제한되기 때문에 firstQueue의 지속적인 성장을 제한하는 최적화 지점을 생각합니다.

초과하는 경우 먼저 FIFO의 요소를 제거하십시오.

그러나 FIFO 만 제거하고 캐시를 제거하지 않으면 둘 사이에 일관되지 않은 활동이 발생합니다.

동시에 제거되지만 캐시의 크기가 아직 만족스럽지 않은 경우 사용자의 기대치를 초과 할 수 있으므로 최적화 포인트로 활용하여 일시적으로 주석 처리 할 수 ​​있습니다.

테스트

암호

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

LRU-2 구현

기술

FIFO의 단점은 매우 분명하며 순회에 O (n) 시간 복잡성이 필요합니다.

그리고 적중률은 여전히 ​​LRU-2보다 약간 나쁩니다.

작업 준비

여기에 LRU 맵이 여러 번 나타 났으며 편의를 위해 LRU 맵을 데이터 구조로 간단히 캡슐화합니다.

간단한 버전을 구현하기 위해 이중 연결 목록 + HashMap을 사용합니다.

마디

노드 노드는 이전과 동일합니다.

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 getter & setter
}

상호 작용

우리는 필요에 따라 가장 중요한 3 가지 방법을 임시로 정의합니다.

/**
 * LRU map 接口
 * @author binbin.hou
 * @since 0.0.13
 */
public interface ILruMap<K,V> {

    /**
     * 移除最老的元素
     * @return 移除的明细
     * @since 0.0.13
     */
    ICacheEntry<K, V> removeEldest();

    /**
     * 更新 key 的信息
     * @param key key
     * @since 0.0.13
     */
    void updateKey(final K key);

    /**
     * 移除对应的 key 信息
     * @param key key
     * @since 0.0.13
     */
    void removeKey(final K key);

    /**
     * 是否为空
     * @return 是否
     * @since 0.0.13
     */
    boolean isEmpty();

    /**
     * 是否包含元素
     * @param key 元素
     * @return 结果
     * @since 0.0.13
     */
    boolean contains(final K key);
}

이루다

우리는 DoubleLinkedList + HashMap 구현을 기반으로합니다.

이전 섹션에서 구현을 정렬하기 만하면됩니다.

import com.github.houbb.cache.api.ICacheEntry;
import com.github.houbb.cache.core.exception.CacheRuntimeException;
import com.github.houbb.cache.core.model.CacheEntry;
import com.github.houbb.cache.core.model.DoubleListNode;
import com.github.houbb.cache.core.support.struct.lru.ILruMap;
import com.github.houbb.heaven.util.lang.ObjectUtil;
import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory;

import java.util.HashMap;
import java.util.Map;

/**
 * 基于双向列表的实现
 * @author binbin.hou
 * @since 0.0.13
 */
public class LruMapDoubleList<K,V> implements ILruMap<K,V> {

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

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

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

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

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

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

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

        K evictKey = tailPre.key();
        V evictValue = tailPre.value();

        return CacheEntry.of(evictKey, evictValue);
    }

    /**
     * 放入元素
     *
     * (1)删除已经存在的
     * (2)新元素放到元素头部
     *
     * @param key 元素
     * @since 0.0.12
     */
    @Override
    public void updateKey(final K key) {
        //1. 执行删除
        this.removeKey(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.13
     */
    @Override
    public void removeKey(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);
    }

    @Override
    public boolean isEmpty() {
        return indexMap.isEmpty();
    }

    @Override
    public boolean contains(K key) {
        return indexMap.containsKey(key);
    }
}

실현 아이디어

LRU 구현은 변경되지 않습니다. FIFO를 LRU 맵으로 직접 대체 할 수 있습니다.

이해의 편의를 위해 FIFO를 firstLruMap으로 대응하며 사용자가 한 번만 액세스 한 요소를 저장하는 데 사용됩니다.

2 회 이상 액세스 한 요소를 원래 LRU에 저장하십시오.

다른 논리는 2Q와 일치합니다.

이루다

기본 속성

액세스 된 정보를 개별적으로 저장할 2 개의 LRU를 정의합니다.

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

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

    /**
     * 第一次访问的 lru
     * @since 0.0.13
     */
    private final ILruMap<K,V> firstLruMap;

    /**
     * 2次及其以上的 lru
     * @since 0.0.13
     */
    private final ILruMap<K,V> moreLruMap;

    public CacheEvictLru2() {
        this.firstLruMap = new LruMapDoubleList<>();
        this.moreLruMap = new LruMapDoubleList<>();
    }

}

제거 달성

lru 2Q 모드와 유사하게 firstLruMap에서 데이터 정보를 제거하는 데 우선 순위를 둡니다.

@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()) {
        ICacheEntry<K,V>  evictEntry = null;
        //1. firstLruMap 不为空,优先移除队列中元素
        if(!firstLruMap.isEmpty()) {
            evictEntry = firstLruMap.removeEldest();
            log.debug("从 firstLruMap 中淘汰数据:{}", evictEntry);
        } else {
            //2. 否则从 moreLruMap 中淘汰数据
            evictEntry = moreLruMap.removeEldest();
            log.debug("从 moreLruMap 中淘汰数据:{}", evictEntry);
        }
        // 执行缓存移除操作
        final K evictKey = evictEntry.key();
        V evictValue = cache.remove(evictKey);
        result = new CacheEntry<>(evictKey, evictValue);
    }
    return result;
}

지우다

/**
 * 移除元素
 *
 * 1. 多次 lru 中存在,删除
 * 2. 初次 lru 中存在,删除
 *
 * @param key 元素
 * @since 0.0.13
 */
@Override
public void removeKey(final K key) {
    //1. 多次LRU 删除逻辑
    if(moreLruMap.contains(key)) {
        moreLruMap.removeKey(key);
        log.debug("key: {} 从 moreLruMap 中移除", key);
    } else {
        firstLruMap.removeKey(key);
        log.debug("key: {} 从 firstLruMap 中移除", key);
    }
}

최신 정보

/**
 * 更新信息
 * 1. 如果 moreLruMap 已经存在,则处理 more 队列,先删除,再插入。
 * 2. 如果 firstLruMap 中已经存在,则处理 first 队列,先删除 firstLruMap,然后插入 Lru。
 * 1 和 2 是不同的场景,但是代码实际上是一样的,删除逻辑中做了二种场景的兼容。
 *
 * 3. 如果不在1、2中,说明是新元素,直接插入到 firstLruMap 的开始即可。
 *
 * @param key 元素
 * @since 0.0.13
 */
@Override
public void updateKey(final K key) {
    //1. 元素已经在多次访问,或者第一次访问的 lru 中
    if(moreLruMap.contains(key)
        || firstLruMap.contains(key)) {
        //1.1 删除信息
        this.removeKey(key);
        //1.2 加入到多次 LRU 中
        moreLruMap.updateKey(key);
        log.debug("key: {} 多次访问,加入到 moreLruMap 中", key);
    } else {
        // 2. 加入到第一次访问 LRU 中
        firstLruMap.updateKey(key);
        log.debug("key: {} 为第一次访问,加入到 firstLruMap 中", key);
    }
}

사실 LRU-2를 사용하는 코드 로직이 더 명확 해졌습니다. 주로 lruMap을 독립적 인 데이터 구조로 추출했기 때문입니다.

테스트

암호

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .size(3)
        .evict(CacheEvicts.<String, String>lru2Q())
        .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 14:39:04.966] [main] [c.g.h.c.c.s.e.CacheEvictLru2.updateKey] - key: A 为第一次访问,加入到 firstLruMap 中
[DEBUG] [2020-10-03 14:39:04.967] [main] [c.g.h.c.c.s.e.CacheEvictLru2.updateKey] - key: B 为第一次访问,加入到 firstLruMap 中
[DEBUG] [2020-10-03 14:39:04.968] [main] [c.g.h.c.c.s.e.CacheEvictLru2.updateKey] - key: C 为第一次访问,加入到 firstLruMap 中
[DEBUG] [2020-10-03 14:39:04.970] [main] [c.g.h.c.c.s.e.CacheEvictLru2.removeKey] - key: A 从 firstLruMap 中移除
[DEBUG] [2020-10-03 14:39:04.970] [main] [c.g.h.c.c.s.e.CacheEvictLru2.updateKey] - key: A 多次访问,加入到 moreLruMap 中
[DEBUG] [2020-10-03 14:39:04.972] [main] [c.g.h.c.c.s.e.CacheEvictLru2.doEvict] - 从 firstLruMap 中淘汰数据:EvictEntry{key=B, value=null}
[DEBUG] [2020-10-03 14:39:04.974] [main] [c.g.h.c.c.s.l.r.CacheRemoveListener.listen] - Remove key: B, value: world, type: evict
[DEBUG] [2020-10-03 14:39:04.974] [main] [c.g.h.c.c.s.e.CacheEvictLru2.updateKey] - key: D 为第一次访问,加入到 firstLruMap 中
[D, A, C]

요약

LRU 알고리즘의 개선을 위해 주로 두 가지 사항을 언급했습니다.

(1) O (N) 최적화에서 O (1)까지 성능 향상

(2) 캐시 오염 방지를위한 일괄 작업 개선

실제로 LRU 외에도 다른 제거 전략이 있습니다.

다음 문제를 고려해야합니다.

데이터 A에 10 번 액세스하고 데이터 B에 2 번 액세스했습니다. 그렇다면 두 사람의 핫 데이터는 누구입니까?

A가 핫 데이터라고 생각한다면 실제로 LFU의 제거 알고리즘을 기반으로 한 또 다른 제거 알고리즘 이 있습니다. 방문이 많을수록 핫 데이터가 더 많다고 생각하십시오 .

다음 섹션에서 함께 LFU 제거 알고리즘의 구현에 대해 알아 보겠습니다.

오픈 소스 주소 :https://github.com/houbb/cache

이 글이 도움이되었다고 생각 되시면 좋아요, 댓글, 모으고, 웨이브를 따라주세요. 여러분의 격려가 저의 가장 큰 동기입니다 ~

현재 우리는 두 가지 최적화를 통해 배치로 인한 성능 문제와 캐시 오염 문제를 해결했습니다.

당신이 얻은 게 뭔지 모르겠어요? 또는 더 많은 아이디어가 있으시면 메시지 영역에서 저와 함께 토론하고 여러분의 생각을 만날 수 있기를 기대합니다.

딥 러닝

추천

출처blog.51cto.com/9250070/2540073