Redis manuscrita de Java desde cero (7) Explicación detallada de la estrategia de eliminación de caché LRU

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.

Eliminado

Conceptos básicos de LRU

Ampliar el aprendizaje

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

Redis utilizado como LRU MAP

Que es LRU

LRU se compone de las primeras letras de Menos recientemente utilizado, que significa el menos utilizado recientemente, y generalmente se utiliza en el algoritmo de eliminación de objetos.

También es un algoritmo de eliminación relativamente común.

La idea central es que si se ha accedido a los datos recientemente, la posibilidad de que se acceda a ellos en el futuro también es mayor .

Continuidad

En informática, hay un principio rector: el principio de continuidad.

Continuidad horaria: Para las visitas informativas, si han sido visitadas recientemente, la posibilidad de ser visitadas nuevamente es muy alta. La caché se basa en este concepto para eliminar datos.

Continuidad espacial: para acceder a la información del disco, es muy posible acceder a información espacial continua. Por lo tanto, habrá una búsqueda previa de páginas para mejorar el rendimiento.

Pasos de implementación

  1. Los nuevos datos se insertan en el encabezado de la lista vinculada;

  2. Siempre que la caché llegue (es decir, se acceda a los datos almacenados en caché), mueva los datos al encabezado de la lista vinculada;

  3. Cuando la lista vinculada está llena, los datos al final de la lista vinculada se descartan.

De hecho, es relativamente simple, en comparación con la cola FIFO, podemos introducir una lista enlazada.

Un poco de pensamiento

Consideremos las 3 oraciones anteriores una por una para ver si hay puntos o pozos dignos de optimización.

¿Cómo juzgar si son nuevos datos?

(1) Inserte nuevos datos en el encabezado de la lista vinculada;

Estamos usando una lista vinculada.

La forma más fácil de determinar si existen nuevos datos es atravesar. Para listas enlazadas, esta es una complejidad de tiempo O (n).

De hecho, el rendimiento sigue siendo relativamente bajo.

Por supuesto, también puede considerar el espacio para el tiempo, como presentar un conjunto, pero esto duplicará la presión sobre el espacio.

¿Qué es un acierto de caché?

(2) Siempre que la caché llegue (es decir, se acceda a los datos almacenados en caché), mueva los datos al encabezado de la lista vinculada;

En el caso de put (clave, valor), es un elemento nuevo. Si este elemento ya existe, puede eliminarlo primero y luego agregarlo, consultando el procesamiento anterior.

En el caso de get (key), para acceder al elemento, el elemento existente se elimina y el nuevo elemento se coloca en el encabezado.

remove (clave) Elimina un elemento, elimina directamente el elemento existente.

keySet () valueSet () entrySet () Estos son accesos indiscriminados y no ajustamos la cola.

Eliminar

(3) Cuando la lista vinculada esté llena, descarte los datos al final de la lista vinculada.

Solo hay un escenario cuando la lista enlazada está llena, y es cuando se agregan elementos, es decir, cuando se ejecuta put (clave, valor).

Simplemente elimine la clave correspondiente directamente.

implementación de código java

Definición de interfaz

Es consistente con la interfaz FIFO y el lugar de la llamada tampoco cambia.

Para la implementación posterior de LRU / LFU, se agregan dos nuevos métodos, eliminar / actualizar.

public interface ICacheEvict<K, V> {

    /**
     * 驱除策略
     *
     * @param context 上下文
     * @since 0.0.2
     * @return 是否执行驱除
     */
    boolean evict(final ICacheEvictContext<K, V> context);

    /**
     * 更新 key 信息
     * @param key key
     * @since 0.0.11
     */
    void update(final K key);

    /**
     * 删除 key 信息
     * @param key key
     * @since 0.0.11
     */
    void remove(final K key);

}

Implementación de LRU

Realice directamente basado en LinkedList:

/**
 * 丢弃策略-LRU 最近最少使用
 * @author binbin.hou
 * @since 0.0.11
 */
public class CacheEvictLRU<K,V> implements ICacheEvict<K,V> {

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

    /**
     * list 信息
     * @since 0.0.11
     */
    private final List<K> list = new LinkedList<>();

    @Override
    public boolean evict(ICacheEvictContext<K, V> context) {
        boolean result = false;
        final ICache<K,V> cache = context.cache();
        // 超过限制,移除队尾的元素
        if(cache.size() >= context.size()) {
            K evictKey = list.get(list.size()-1);
            // 移除对应的元素
            cache.remove(evictKey);
            result = true;
        }
        return result;
    }

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

    /**
     * 移除元素
     * @param key 元素
     * @since 0.0.11
     */
    @Override
    public void remove(final K key) {
        this.list.remove(key);
    }

}

La implementación es relativamente simple y hay tres métodos más que FIFO:

update (): Hacemos una pequeña simplificación y pensamos que mientras sea de acceso, se elimina y luego se inserta en el encabezado de la cola.

remove (): Eliminar es eliminar directamente.

Estos tres métodos se utilizan para actualizar el uso reciente.

¿Cuándo se llamará?

Atributos de anotación

Para garantizar el proceso central, lo implementamos en función de anotaciones.

Agregar atributos:

/**
 * 是否执行驱除更新
 *
 * 主要用于 LRU/LFU 等驱除策略
 * @return 是否
 * @since 0.0.11
 */
boolean evict() default false;

Uso de anotaciones

¿Qué métodos se deben utilizar?

@Override
@CacheInterceptor(refresh = true, evict = true)
public boolean containsKey(Object key) {
    return map.containsKey(key);
}

@Override
@CacheInterceptor(evict = true)
@SuppressWarnings("unchecked")
public V get(Object key) {
    //1. 刷新所有过期信息
    K genericKey = (K) key;
    this.expire.refreshExpire(Collections.singletonList(genericKey));
    return map.get(key);
}

@Override
@CacheInterceptor(aof = true, evict = true)
public V put(K key, V value) {
    //...
}

@Override
@CacheInterceptor(aof = true, evict = true)
public V remove(Object key) {
    return map.remove(key);
}

Implementación del interceptor de exclusión de anotaciones

Orden de ejecución: actualice después del método, de lo contrario, la clave de cada operación actual se colocará primero.

/**
 * 驱除策略拦截器
 * 
 * @author binbin.hou
 * @since 0.0.11
 */
public class CacheInterceptorEvict<K,V> implements ICacheInterceptor<K, V> {

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

    @Override
    public void before(ICacheInterceptorContext<K,V> context) {
    }

    @Override
    @SuppressWarnings("all")
    public void after(ICacheInterceptorContext<K,V> context) {
        ICacheEvict<K,V> evict = context.cache().evict();

        Method method = context.method();
        final K key = (K) context.params()[0];
        if("remove".equals(method.getName())) {
            evict.remove(key);
        } else {
            evict.update(key);
        }
    }

}

Solo hacemos un juicio especial sobre el método de eliminación, y todos los demás métodos utilizan la actualización para actualizar la información.

El parámetro toma directamente el primer parámetro.

prueba

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .size(3)
        .evict(CacheEvicts.<String, String>lru())
        .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());
  • Información de registro
[D, A, C]

También puede ver que B se ha eliminado del registro removeListener:

[DEBUG] [2020-10-02 21:33:44.578] [main] [c.g.h.c.c.s.l.r.CacheRemoveListener.listen] - Remove key: B, value: world, type: evict

resumen

La estrategia de eliminación de redis LRU en realidad no es una LRU real.

LRU tiene un gran problema, es decir, cada vez que O (n) busca, esto sigue siendo muy lento cuando el número de claves es particularmente grande.

Si redis está diseñado así, definitivamente será lento.

El individuo puede apreciar ese espacio para el tiempo, como agregar unas Map&lt;String, Integer&gt;claves almacenadas en el índice y la lista, O (1) para encontrar la velocidad, pero la complejidad espacial se duplica.

Pero el sacrificio merece la pena. Este tipo de optimización unificada de seguimiento tiene en cuenta varios puntos de optimización, de modo que la situación general se puede coordinar y también es conveniente para ajustes unificados posteriores.

En la siguiente sección, implementaremos la siguiente versión mejorada de LRU juntos.

Lo que hace Redis es lograr lo último en cosas aparentemente simples, algo que vale la pena aprender para cada software de código abierto.

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/2539883
Recomendado
Clasificación