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

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

Simplemente implementamos varias características de redis. Java implementa redis a mano desde cero (1) ¿Cómo lograr un caché de tamaño fijo? En China se ha implementado una estrategia de eliminación de primero en entrar, primero en salir.

Sin embargo, en la práctica laboral real, generalmente se recomienda utilizar la estrategia de eliminación LRU / LFU.

Conceptos básicos de LRU

Qué es

El nombre completo del algoritmo LRU es el algoritmo de uso menos reciente, que se usa ampliamente en los mecanismos de almacenamiento en caché.

Cuando el espacio utilizado por la caché alcanza el límite superior, es necesario eliminar una parte de los datos existentes para mantener la disponibilidad de la caché, y la selección de los datos eliminados se completa mediante el algoritmo LRU.

La idea básica del algoritmo LRU es la localidad temporal basada en el principio de localidad:

Si se accede a un elemento de información, es probable que se vuelva a acceder en un futuro próximo.

Otras lecturas

Explicación detallada del código fuente de Apache Commons LRUMAP

Redis utilizado como LRU MAP

Redis manuscrito de Java desde cero (7) explicación detallada e implementación de la estrategia de exclusión de redis LRU

Ideas de implementación simples

Basado en matriz

Solución: adjunte un atributo adicional a cada pieza de datos (marca de tiempo) y actualice la marca de tiempo de los datos a la hora actual cada vez que se acceda a los datos.

Cuando el espacio de datos está lleno, se escanea toda la matriz y se eliminan los datos con la marca de tiempo más pequeña.

Insuficiencia: mantener la marca de tiempo requiere espacio adicional y escanear toda la matriz al eliminar datos.

Esta vez, la complejidad es una lástima y la complejidad del espacio no es buena.

Basado en una lista doblemente enlazada de extensión limitada

Solución: al acceder a un dato, cuando los datos no están en la lista vinculada, inserte los datos en el encabezado de la lista vinculada, y si está en la lista vinculada, mueva los datos al encabezado de la lista vinculada. Cuando el espacio de datos está lleno, se eliminan los datos al final de la lista vinculada.

Insuficiencia: al insertar o recuperar datos, es necesario escanear toda la lista vinculada.

Esta es la forma en que lo implementamos en la sección anterior.Las deficiencias aún son obvias.Cada vez que confirmamos si un elemento existe, la consulta lleva O (n) tiempo de complejidad.

Basado en lista doblemente enlazada y tabla hash

Solución: para mejorar los defectos anteriores que necesitan escanear la lista enlazada, coopere con la tabla hash para mapear los datos y los nodos en la lista enlazada, y reducir la complejidad temporal de la operación de inserción y lectura de O (N) a O (1)

Desventajas: Esto hace que las ideas de optimización que mencionamos en el apartado anterior, pero aún existen desventajas, es decir, se duplica la complejidad del espacio.

Elección de la estructura de datos

(1) Implementación basada en matrices

No se recomienda elegir array o ArrayList aquí, porque la complejidad del tiempo de lectura es O (1), pero la actualización es relativamente lenta, aunque jdk usa System.arrayCopy.

(2) Implementación basada en lista vinculada

Si elegimos una lista vinculada, la clave y el subíndice correspondiente no se pueden almacenar simplemente en el HashMap.

Debido a que el recorrido de la lista enlazada es en realidad O (n), la lista doblemente enlazada teóricamente se puede optimizar a la mitad, pero este no es el efecto O (1) que queremos.

(3) Basado en una lista bidireccional

Mantenemos la lista doblemente enlazada sin cambios.

Ponemos la información del nodo de la lista doblemente enlazada para el valor correspondiente a la clave en el Mapa.

El método de realización se convierte en la realización de una lista doblemente enlazada.

Código

  • Definición de nodo
/**
 * 双向链表节点
 * @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
}
  • Implementación de código central

Mantenemos la interfaz original sin cambios y la implementación es la siguiente:

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

}

No es difícil de implementar, es una simple lista bidireccional.

Es solo que cuando obtenemos nodos, usamos el mapa para reducir la complejidad del tiempo a O (1).

prueba

Verifiquemos nuestra implementación:

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());
  • Iniciar sesión
[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]

Debido a que visitamos A una vez, B se ha convertido en el elemento menos visitado.

Basado en LinkedHashMap

De hecho, LinkedHashMap en sí mismo es una estructura de datos combinada para list y hashMap, podemos usar LinkedHashMap directamente en jdk para lograrlo.

Realización directa

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

El LinkedHashMap predeterminado no elimina los datos, por lo que reescribimos su método removeEldestEntry (). Cuando el número de datos alcanza el límite superior preestablecido, los datos se eliminan. Establecer accessOrder en verdadero significa ordenar en el orden de acceso.

La cantidad de código para toda la implementación no es grande, principalmente utilizando las características de LinkedHashMap.

Transformación simple

Simplemente modificamos este método para adaptarlo a la interfaz que definimos.

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

prueba

  • Código
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());
  • Iniciar sesión
[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]

resumen

El problema del recorrido de la matriz O (n) mencionado en la sección anterior se ha resuelto básicamente en esta sección.

Pero, de hecho, este algoritmo todavía tiene ciertos problemas. Por ejemplo, cuando las operaciones por lotes ocasionales, los datos calientes serán extraídos de la caché por los datos no activos. En la siguiente sección, aprenderemos cómo mejorar aún más el algoritmo LRU.

El artículo habla principalmente sobre las ideas, y la parte de realización no se publica todo debido a limitaciones de espacio.

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

Si cree que este artículo es útil para usted, puede dar me gusta, comentar, marcar y seguir una ola ~

Tu aliento es mi mayor motivación ~

Aprendizaje profundo

Supongo que te gusta

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