java manuscrito redis desde cero (diez) algoritmo de eliminación de caché LFU menor frecuencia de uso

Prefacio

Java implementa redis a mano desde cero (1) ¿Cómo lograr un caché de tamaño fijo?

Java implementa redis a mano desde cero (tres) principio de expiración de redis

Java implementa redis a mano desde cero (3) ¿Cómo reiniciar sin perder datos de memoria?

Java implementa redis desde cero a mano (cuatro) agregar oyente

Otra forma de implementar la estrategia de vencimiento de redis (5) desde cero

Java implementa redis a mano desde cero (6) Principio de persistencia AOF detallado e implementación

Java implementa redis a mano desde cero (7) Explicación detallada de la estrategia de eliminación de caché LRU

Redistribución de escritura a mano desde cero (ocho) optimización del rendimiento del algoritmo de eliminación de LRU simple

En esta sección, aprendamos sobre otro algoritmo de eliminación de caché de uso común, el algoritmo de frecuencia menos utilizado de LFU.

Conceptos básicos de LFU

concepto

LFU (Menos de uso frecuente) es el menos utilizado recientemente, basta con mirar el nombre y sabrás que es un algoritmo basado en la frecuencia de acceso.

LRU se basa en el tiempo y eliminará los datos a los que se accede con menos frecuencia en el tiempo y los colocará en la parte superior de la lista en términos de rendimiento del algoritmo; LFU elimina los datos a los que se accede con menos frecuencia en frecuencia.

Dado que se basa en la frecuencia, es necesario almacenar el número de veces que cada acceso a los datos.

En términos de espacio de almacenamiento, habrá más espacio para almacenar recuentos que LRU.

idea principal

Si un dato se ha utilizado con poca frecuencia en el período reciente, es poco probable que se utilice en el futuro.

Idea de realización

Eliminación O (N)

Para eliminar los datos menos utilizados, mi primer instinto es directamente uno HashMap<String, Interger>, String corresponde a la información clave y Integer corresponde al número de veces.

Vaya a +1 cada vez que visite, la complejidad del tiempo de configuración y lectura es O (1); pero eliminar es más problemático, debe recorrer todas las comparaciones y la complejidad del tiempo es O (n);

Eliminación de O (logn)

Otra idea de implementación es utilizar el pequeño montón superior + hashmap, las operaciones de inserción y eliminación del pequeño montón superior pueden lograr una complejidad de tiempo O (logn), por lo que la eficiencia es más eficiente que el primer método de implementación. Como TreeMap.

O (1) eliminación

¿Se puede optimizar aún más?

De hecho, existen algoritmos O (1), consulte este artículo:

Un algoritmo O (1) para implementar el esquema de desalojo de caché LFU

Hable brevemente sobre pensamientos personales:

Si queremos lograr la operación O (1), no debemos prescindir de la operación Hash. En nuestra eliminación O (N), nos damos cuenta de O (1) put / get.

Pero el rendimiento de eliminación es deficiente, porque se necesita tiempo para encontrar la menor cantidad de veces.

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

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

Básicamente, podemos resolver este problema usando doble Hash.

El mapa almacena la relación de mapeo entre claves y nodos. Put / get son definitivamente O (1).

El nodo mapeado por la clave tiene la información de frecuencia correspondiente; la misma frecuencia se asociará a través de freqMap, y la lista vinculada correspondiente se puede obtener rápidamente por frecuencia.

Eliminar también se ha vuelto muy simple Básicamente, se puede determinar que la frecuencia más baja de eliminación es 1. Si no es como máximo 1 ... n, la frecuencia mínima selecciona el primer elemento de la lista vinculada y comienza a eliminar.

En cuanto a la prioridad de la propia lista vinculada, puede basarse en FIFO u otros métodos que desee.

Introducción al contenido básico del artículo

Piedras de otras colinas, pueden aprender.

Antes de implementar el código, leamos este artículo de O (1).

Introducción

La estructura de este artículo es la siguiente.

Una descripción del caso de uso de LFU, que puede resultar superior a otros algoritmos de desalojo de caché

La implementación de la caché LFU debe admitir operaciones de diccionario. Estas son las operaciones que determinan la complejidad del tiempo de ejecución de la estrategia.

Descripción del algoritmo LFU más famoso y su complejidad en tiempo de ejecución

Descripción del algoritmo LFU propuesto; la complejidad en tiempo de ejecución de cada operación es O (1)

Usos de LFU

Considere una aplicación de proxy web de almacenamiento en caché para el protocolo HTTP.

El proxy generalmente se encuentra entre Internet y el usuario o un grupo de usuarios.

Garantiza que todos los usuarios puedan acceder a Internet y darse cuenta de que se comparten todos los recursos compartibles para lograr la mejor utilización de la red y velocidad de respuesta.

Dicho agente de almacenamiento en caché debería intentar maximizar la cantidad de datos que puede almacenar en caché en la cantidad limitada de almacenamiento o memoria a su disposición.

Generalmente, los recursos estáticos (como imágenes, hojas de estilo CSS y código javascript) se pueden almacenar en caché fácilmente durante mucho tiempo antes de reemplazarlos con versiones más nuevas.

Estos recursos estáticos o lo que los programadores llaman "activos" están contenidos en casi todas las páginas, por lo que almacenarlos en caché es lo más beneficioso porque serán necesarios para casi todas las solicitudes.

Además, dado que los agentes de red deben manejar miles de solicitudes por segundo, la sobrecarga requerida para hacerlo debe mantenerse al mínimo.

Por esta razón, debe expulsar solo aquellos recursos que no se utilizan con frecuencia.

Por lo tanto, debe mantener los recursos de uso frecuente en recursos de uso menos frecuente, porque el primero ha demostrado ser útil durante un período de tiempo.

Por supuesto, hay un dicho que dice lo contrario, dice que puede que no se utilicen muchos recursos en el futuro, pero hemos descubierto que este no es el caso en la mayoría de los casos.

Por ejemplo, cada usuario de la página siempre solicita los recursos estáticos de las páginas de uso frecuente.

Por lo tanto, cuando la memoria es insuficiente, estos agentes de almacenamiento en caché pueden utilizar la estrategia de reemplazo de caché LFU para expulsar los elementos menos utilizados en su caché.

LRU también puede ser una estrategia aplicable aquí, pero cuando el modo de solicitud hace que todos los elementos solicitados no ingresen a la memoria caché y estos elementos se solicitan en forma rotatoria, LRU fallará.

ps: La solicitud cíclica de datos hará que LRU simplemente no se adapte a este escenario.

En el caso de LRU, los elementos seguirán entrando y saliendo del caché, y ningún usuario solicitará acceder al caché.

Sin embargo, en las mismas condiciones, el rendimiento del algoritmo LFU será mejor y la mayoría de los elementos de la caché provocarán aciertos.

El comportamiento patológico del algoritmo LFU no es imposible.

No presentamos el caso de LFU aquí, pero intentamos demostrar que si LFU es una estrategia aplicable, existe un método de implementación mejor que el método publicado anteriormente.

Operaciones de diccionario compatibles con la caché LFU

Cuando hablamos del algoritmo de expulsión de caché, principalmente necesitamos realizar 3 operaciones diferentes en los datos almacenados en caché.

  1. Establecer (o insertar) elementos en la caché

  2. Recupere (o busque) elementos en la caché; al mismo tiempo, aumente su recuento de uso (para LFU)

  3. Desalojar (o eliminar) de la caché los menos utilizados (o como estrategia para el algoritmo de desalojo)

La complejidad actual más famosa del algoritmo LFU

En el momento de escribir este artículo, los tiempos de ejecución más famosos para cada una de las operaciones anteriores para la estrategia de desalojo de caché de LFU son los siguientes:

Insertar: O (log n)

Búsqueda: O (log n)

Eliminar: O (log n)

Estos valores de complejidad se obtienen directamente de la implementación del montón binomial y de la tabla hash estándar sin colisiones.

El uso de la estructura de datos de pila mínima y el gráfico hash puede implementar de manera fácil y efectiva la estrategia de almacenamiento en caché LFU.

El montón mínimo se crea en función del recuento de uso (del elemento) y la tabla hash se indexa por la clave del elemento.

El orden de todas las operaciones en la tabla hash sin colisiones es O (1), por lo que el tiempo de ejecución de la caché LFU está controlado por el tiempo de ejecución de la operación en el montón más pequeño.

Cuando un elemento se inserta en la caché, ingresará con un recuento de uso de 1. Dado que la sobrecarga de insertar el montón mínimo es O (log n), se necesita O (log n) para insertarlo en la caché LFU.

Al buscar un elemento, el elemento se puede encontrar a través de una función hash, que aplica un hash a la clave del elemento real. Al mismo tiempo, el recuento de uso (el recuento en el montón más grande) aumenta en uno, lo que provoca la reorganización del montón más pequeño y el elemento se aleja de la raíz.

Dado que el elemento puede descender al nivel log (n) en cualquier etapa, esta operación también requiere tiempo O (log n).

Cuando se selecciona un elemento para ser desalojado y, finalmente, eliminado del montón, puede provocar una reorganización importante de la estructura de datos del montón.

El elemento con el recuento de uso más bajo está en la raíz del montón más pequeño.

Eliminar la raíz del montón más pequeño implica reemplazar el nodo raíz con el último nodo hoja del montón y hacer burbujear el nodo en la posición correcta.

La complejidad en tiempo de ejecución de esta operación también es O (log n).

Algoritmo LFU propuesto

Para cada operación de diccionario (inserción, búsqueda y eliminación) que se puede realizar en la caché de LFU, la complejidad en tiempo de ejecución del algoritmo LFU propuesto es O (1).

Esto se logra manteniendo dos listas enlazadas. Uno es para la frecuencia de acceso y el otro es para todos los elementos con la misma frecuencia de acceso.

Las tablas hash se utilizan para acceder a elementos por clave (no se muestran en la figura siguiente para mayor claridad).

Se utiliza una lista de doble enlace para vincular nodos que representan un grupo de nodos con la misma frecuencia de acceso (mostrados como bloques rectangulares en la figura siguiente).

Llamamos a esta lista de doble enlace una lista de frecuencias. El grupo de nodos con la misma frecuencia de acceso es en realidad una lista enlazada bidireccional de dichos nodos (que se muestran como nodos circulares en la figura siguiente).

Llamamos a esta lista enlazada bidireccional (local a una frecuencia específica) una lista de nodos.

Cada nodo de la lista de nodos tiene un puntero a su nodo principal.

Lista de frecuencias (no se muestra en la figura para mayor claridad). Entonces, el nodo xy tendrá un puntero al nodo 1, los nodos zya tendrán un puntero al nodo 2, y así sucesivamente ...

Ingrese la descripción de la imagen

El pseudocódigo a continuación muestra cómo inicializar la caché LFU.

La tabla hash utilizada para ubicar elementos por clave está representada por variables clave.

Para simplificar la implementación, usamos SET en lugar de la lista vinculada para almacenar elementos con la misma frecuencia de acceso.

El elemento variable es una estructura de datos SET estándar, que contiene las claves de dichos elementos con la misma frecuencia de acceso.

Su complejidad en tiempo de ejecución de inserción, búsqueda y eliminación es O (1).

Ingrese la descripción de la imagen

Código falso

Los siguientes son algunos pseudo-códigos, somos domésticos.

Solo entienda su idea central, vayamos al código real a continuación.

Sensación

El núcleo de este algoritmo O (1) en realidad no es mucho, y debería considerarse como un problema de dificultad media cuando se coloca en leetcode.

Sin embargo, es extraño que este documento se propuso en 2010, y se estima que O (logn) era el límite antes?

implementación de código java

Atributos basicos

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;
    }

}

Definición de nodo

  • 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()
}

Quitar elemento

/**
 * 移除元素
 *
 * 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);
    }
}

Elemento de actualización

/**
 * 更新元素,更新 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);
}

Eliminación de datos

@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");
}

prueba

Código

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());

Iniciar sesión

[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 frente a LRU

la diferencia

LFU es un modo basado en la frecuencia de acceso, mientras que LRU es un modo basado en el tiempo de acceso.

Ventaja

Cuando el acceso a los datos se ajusta a una distribución normal, la tasa de aciertos de caché del algoritmo LFU es mayor que la del algoritmo LRU.

Desventaja

  • La complejidad de LFU es mayor que la de LRU.

  • Es necesario mantener la frecuencia de acceso a los datos y actualizar cada acceso.

  • Los datos iniciales son más fáciles de almacenar en caché que los datos posteriores, lo que dificulta el almacenamiento en caché de los datos posteriores.

  • Los datos recién agregados a la caché se eliminan fácilmente, como "fluctuaciones" al final de la caché.

resumen

Sin embargo, en la práctica real, los escenarios de aplicación de LFU no son tan extensos.

Debido a que los datos reales están sesgados y los datos calientes son la norma, el rendimiento de LRU es generalmente mejor que el de LFU.

Dirección de fuente abierta:https://github.com/houbb/cache

Si cree que este artículo es útil para usted, por favor haga clic en Me gusta, comente, recopile y siga una ola. Su aliento es mi mayor motivación ~

En la actualidad, hemos implementado los algoritmos LRU y LFU con excelente rendimiento, pero el sistema operativo realmente utiliza estos dos algoritmos, en la siguiente sección aprenderemos sobre el algoritmo de eliminación de reloj favorecido por el sistema operativo.

No sé lo que has ganado O si tiene más ideas, bienvenido a discutir conmigo en el área de mensajes y esperamos conocer sus pensamientos.

Aprendizaje profundo

Supongo que te gusta

Origin blog.51cto.com/9250070/2540213
Recomendado
Clasificación