Redis manuscrito de Java desde cero (6) explicación detallada e implementación del principio de persistencia AOF de redis

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

Simplemente implementamos varias características de redis anteriormente. Java implementa redis manualmente desde cero (3) ¿Cómo reiniciar sin perder datos de memoria? El modo RDB similar a redis se implementa en.

redis aof basics

Persistencia de Redis AOF detallada

Alguna comprensión personal de AOF

¿Por qué elegir AOF?

El rendimiento del modo AOF es particularmente bueno , ¿qué tan bueno es?

Los estudiantes que han utilizado Kafka deben saber que Kafka también utiliza la función de escritura secuencial.

Escriba y agregue contenido de archivo de forma secuencial, evitando problemas de escritura de E / S de archivos aleatorios, y el rendimiento es básicamente comparable al de la memoria.

AOF tiene un mejor rendimiento en tiempo real , que es relativo al modo RDB.

Originalmente usamos el modo RDB para conservar todo el contenido almacenado en caché. Esta es una acción que requiere relativamente tiempo, generalmente cada pocos minutos.

El modo AOF es principalmente para instrucciones para modificar el contenido, y luego todas las instrucciones se agregan al archivo en orden. En este caso, el rendimiento en tiempo real será mucho mejor y puede elevarse al segundo nivel o incluso al segundo nivel.

Rendimiento AOF

El modo AOF puede persistir para cada operación, pero esto provocará una disminución significativa en el rendimiento.

La forma más común de mejorar el rendimiento es el procesamiento por lotes . Esto es similar en Kafka. Por ejemplo, podemos persistir una vez en 1 y poner todas las operaciones en 1 en el búfer.

Este es en realidad un problema de compensación, el arte de equilibrar el rendimiento y el rendimiento en tiempo real.

En los negocios reales, el error de 1 es generalmente aceptable, por lo que también es un método más reconocido en la industria.

Asincrónico + multiproceso de AOF

Todas las operaciones en Kafka se implementan de forma asincrónica + devoluciones de llamada.

De hecho, el subproceso múltiple asíncrono + puede mejorar el rendimiento de la operación.

Por supuesto, antes de redis 6, en realidad era de un solo subproceso. Entonces, ¿por qué el rendimiento sigue siendo tan bueno?

De hecho, los subprocesos múltiples también tienen un precio, es decir, el cambio de contexto de subprocesos requiere mucho tiempo y, para mantener los problemas de seguridad de concurrencia, también debe bloquearse, lo que reduce el rendimiento.

Así que aquí tenemos que considerar la cuestión de si el beneficio de lo asincrónico es proporcional al tiempo empleado.

Colocación de pedidos AOF

Nuestros modos AOF y RDB, en el análisis final, se basan en el sistema de archivos del sistema operativo para la persistencia.

Para los desarrolladores, se puede lograr llamando a una API, pero es posible que la acción real de mantener el pedido no se complete en un solo paso.

Para mejorar el rendimiento, el sistema de archivos también utiliza un enfoque similar a un búfer. De repente hubo un poco de matrioska rusa.

Pero un buen diseño siempre es similar. Por ejemplo, la caché tiene L1 / L2 y así sucesivamente del diseño de la CPU. Las ideas son las mismas.

Muchas de las tecnologías de código abierto de Alibaba se optimizarán aún más para la ubicación del sistema operativo. Haremos un estudio en profundidad más adelante.

Los defectos de AOF

A la carretera principal le falta uno, no hay una fórmula mágica.

AOF es muy bueno, y hay una falla en comparación con RDB, esa es la instrucción

implementación de java

interfaz

La interfaz es consistente con la de rdb

/**
 * 持久化缓存接口
 * @author binbin.hou
 * @since 0.0.7
 * @param <K> key
 * @param <V> value
 */
public interface ICachePersist<K, V> {

    /**
     * 持久化缓存信息
     * @param cache 缓存
     * @since 0.0.7
     */
    void persist(final ICache<K, V> cache);

}

Definición de anotación

Para ser coherentes con las estadísticas, la actualización y otras características que consumen mucho tiempo, para el tipo de operación, las acciones se agregan al archivo (anexar al archivo), también especificamos en función de los atributos de anotación, en lugar de escribir de forma fija en el código, lo que es conveniente para una expansión y ajuste posteriores.

/**
 * 缓存拦截器
 * @author binbin.hou
 * @since 0.0.5
 */
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheInterceptor {

    /**
     * 操作是否需要 append to file,默认为 false
     * 主要针对 cache 内容有变更的操作,不包括查询操作。
     * 包括删除,添加,过期等操作。
     * @return 是否
     * @since 0.0.10
     */
    boolean aof() default false;

}

Hemos @CacheInterceptorañadido la anotación de propiedad aof que especifica si se abre el modo de funcionamiento aof.

Método para especificar el modo aof

Especificamos este atributo de anotación en el método que cambiará los datos:

Operación caducada

De forma similar al interceptor de transacciones de Spring, usamos la clase de proxy para llamar a expireAt.

El método de expiración no necesita agregar una intercepción.

/**
 * 设置过期时间
 * @param key         key
 * @param timeInMills 毫秒时间之后过期
 * @return this
 */
@Override
@CacheInterceptor
public ICache<K, V> expire(K key, long timeInMills) {
    long expireTime = System.currentTimeMillis() + timeInMills;
    // 使用代理调用
    Cache<K,V> cachePoxy = (Cache<K, V>) CacheProxy.getProxy(this);
    return cachePoxy.expireAt(key, expireTime);
}

/**
 * 指定过期信息
 * @param key key
 * @param timeInMills 时间戳
 * @return this
 */
@Override
@CacheInterceptor(aof = true)
public ICache<K, V> expireAt(K key, long timeInMills) {
    this.expire.expire(key, timeInMills);
    return this;
}

Cambiar operación

@Override
@CacheInterceptor(aof = true)
public V put(K key, V value) {
    //1.1 尝试驱除
    CacheEvictContext<K,V> context = new CacheEvictContext<>();
    context.key(key).size(sizeLimit).cache(this);
    boolean evictResult = evict.evict(context);
    if(evictResult) {
        // 执行淘汰监听器
        ICacheRemoveListenerContext<K,V> removeListenerContext = CacheRemoveListenerContext.<K,V>newInstance().key(key).value(value).type(CacheRemoveType.EVICT.code());
        for(ICacheRemoveListener<K,V> listener : this.removeListeners) {
            listener.listen(removeListenerContext);
        }
    }
    //2. 判断驱除后的信息
    if(isSizeLimit()) {
        throw new CacheRuntimeException("当前队列已满,数据添加失败!");
    }
    //3. 执行添加
    return map.put(key, value);
}

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

@Override
@CacheInterceptor(aof = true)
public void putAll(Map<? extends K, ? extends V> m) {
    map.putAll(m);
}

@Override
@CacheInterceptor(refresh = true, aof = true)
public void clear() {
    map.clear();
}

Implementación de interceptación persistente AOF

Definición de objeto persistente

/**
 * AOF 持久化明细
 * @author binbin.hou
 * @since 0.0.10
 */
public class PersistAofEntry {

    /**
     * 参数信息
     * @since 0.0.10
     */
    private Object[] params;

    /**
     * 方法名称
     * @since 0.0.10
     */
    private String methodName;

    //getter & setter &toString
}

Aquí solo necesitamos el nombre del método y el objeto de parámetro.

Puede ser más sencillo por el momento.

Interceptor persistente

Definimos interceptores, cuando el caché se define como clases persistentes CachePersistAof, la información se pondrá en funcionamiento en la lista de búfer CachePersistAof.

public class CacheInterceptorAof<K,V> implements ICacheInterceptor<K, V> {

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

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

    @Override
    public void after(ICacheInterceptorContext<K,V> context) {
        // 持久化类
        ICache<K,V> cache = context.cache();
        ICachePersist<K,V> persist = cache.persist();

        if(persist instanceof CachePersistAof) {
            CachePersistAof<K,V> cachePersistAof = (CachePersistAof<K,V>) persist;

            String methodName = context.method().getName();
            PersistAofEntry aofEntry = PersistAofEntry.newInstance();
            aofEntry.setMethodName(methodName);
            aofEntry.setParams(context.params());

            String json = JSON.toJSONString(aofEntry);

            // 直接持久化
            log.debug("AOF 开始追加文件内容:{}", json);
            cachePersistAof.append(json);
            log.debug("AOF 完成追加文件内容:{}", json);
        }
    }

}

Llamada de interceptor

Cuando el atributo de anotación de AOF sea verdadero, simplemente llame al interceptor anterior.

Para evitar el desperdicio aquí, la llamada solo se realiza cuando la clase persistente está en modo AOF.

//3. AOF 追加
final ICachePersist cachePersist = cache.persist();
if(cacheInterceptor.aof() && (cachePersist instanceof CachePersistAof)) {
    if(before) {
        persistInterceptors.before(interceptorContext);
    } else {
        persistInterceptors.after(interceptorContext);
    }
}

Implementación de persistencia AOF

El modo AOF aquí y las clases de persistencia RDB anteriores son solo modos diferentes, de hecho, los dos son la misma interfaz.

interfaz

Aquí definimos uniformemente el tiempo de diferentes clases de persistencia para facilitar la activación de diferentes intervalos de tiempo para diferentes tareas de RDB y AOF.

public interface ICachePersist<K, V> {

    /**
     * 持久化缓存信息
     * @param cache 缓存
     * @since 0.0.7
     */
    void persist(final ICache<K, V> cache);

    /**
     * 延迟时间
     * @return 延迟
     * @since 0.0.10
     */
    long delay();

    /**
     * 时间间隔
     * @return 间隔
     * @since 0.0.10
     */
    long period();

    /**
     * 时间单位
     * @return 时间单位
     * @since 0.0.10
     */
    TimeUnit timeUnit();
}

Implementación de la clase de persistencia

Implemente una lista de búfer para que cada interceptor se agregue directamente de forma secuencial.

La implementación de la persistencia también es relativamente simple: después de agregar al archivo, simplemente borre la lista de búfer directamente.

/**
 * 缓存持久化-AOF 持久化模式
 * @author binbin.hou
 * @since 0.0.10
 */
public class CachePersistAof<K,V> extends CachePersistAdaptor<K,V> {

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

    /**
     * 缓存列表
     * @since 0.0.10
     */
    private final List<String> bufferList = new ArrayList<>();

    /**
     * 数据持久化路径
     * @since 0.0.10
     */
    private final String dbPath;

    public CachePersistAof(String dbPath) {
        this.dbPath = dbPath;
    }

    /**
     * 持久化
     * key长度 key+value
     * 第一个空格,获取 key 的长度,然后截取
     * @param cache 缓存
     */
    @Override
    public void persist(ICache<K, V> cache) {
        log.info("开始 AOF 持久化到文件");
        // 1. 创建文件
        if(!FileUtil.exists(dbPath)) {
            FileUtil.createFile(dbPath);
        }
        // 2. 持久化追加到文件中
        FileUtil.append(dbPath, bufferList);

        // 3. 清空 buffer 列表
        bufferList.clear();
        log.info("完成 AOF 持久化到文件");
    }

    @Override
    public long delay() {
        return 1;
    }

    @Override
    public long period() {
        return 1;
    }

    @Override
    public TimeUnit timeUnit() {
        return TimeUnit.SECONDS;
    }

    /**
     * 添加文件内容到 buffer 列表中
     * @param json json 信息
     * @since 0.0.10
     */
    public void append(final String json) {
        if(StringUtil.isNotEmpty(json)) {
            bufferList.add(json);
        }
    }

}

Prueba de persistencia

Código de prueba

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .persist(CachePersists.<String, String>aof("1.aof"))
        .build();
cache.put("1", "1");
cache.expire("1", 10);
cache.remove("2");
TimeUnit.SECONDS.sleep(1);

Registro de prueba

Expire en realidad las llamadas expireAt.

[DEBUG] [2020-10-02 12:20:41.979] [main] [c.g.h.c.c.s.i.a.CacheInterceptorAof.after] - AOF 开始追加文件内容:{"methodName":"put","params":["1","1"]}
[DEBUG] [2020-10-02 12:20:41.980] [main] [c.g.h.c.c.s.i.a.CacheInterceptorAof.after] - AOF 完成追加文件内容:{"methodName":"put","params":["1","1"]}
[DEBUG] [2020-10-02 12:20:41.982] [main] [c.g.h.c.c.s.i.a.CacheInterceptorAof.after] - AOF 开始追加文件内容:{"methodName":"expireAt","params":["1",1601612441990]}
[DEBUG] [2020-10-02 12:20:41.982] [main] [c.g.h.c.c.s.i.a.CacheInterceptorAof.after] - AOF 完成追加文件内容:{"methodName":"expireAt","params":["1",1601612441990]}
[DEBUG] [2020-10-02 12:20:41.984] [main] [c.g.h.c.c.s.i.a.CacheInterceptorAof.after] - AOF 开始追加文件内容:{"methodName":"remove","params":["2"]}
[DEBUG] [2020-10-02 12:20:41.984] [main] [c.g.h.c.c.s.i.a.CacheInterceptorAof.after] - AOF 完成追加文件内容:{"methodName":"remove","params":["2"]}
[DEBUG] [2020-10-02 12:20:42.088] [pool-1-thread-1] [c.g.h.c.c.s.l.r.CacheRemoveListener.listen] - Remove key: 1, value: 1, type: expire
[INFO] [2020-10-02 12:20:42.789] [pool-2-thread-1] [c.g.h.c.c.s.p.InnerCachePersist.run] - 开始持久化缓存信息
[INFO] [2020-10-02 12:20:42.789] [pool-2-thread-1] [c.g.h.c.c.s.p.CachePersistAof.persist] - 开始 AOF 持久化到文件
[INFO] [2020-10-02 12:20:42.798] [pool-2-thread-1] [c.g.h.c.c.s.p.CachePersistAof.persist] - 完成 AOF 持久化到文件
[INFO] [2020-10-02 12:20:42.799] [pool-2-thread-1] [c.g.h.c.c.s.p.InnerCachePersist.run] - 完成持久化缓存信息

contenido del documento

1.aof El contenido del archivo es el siguiente

{"methodName":"put","params":["1","1"]}
{"methodName":"expireAt","params":["1",1601612441990]}
{"methodName":"remove","params":["2"]}

Simplemente guarde cada operación en un archivo.

Implementación de carga AOF

carga

Similar al modo de carga de RDB, el modo de carga de aof también es similar.

Necesitamos restaurar el contenido almacenado en caché anterior según el contenido del archivo.

Idea de implementación: recorre el contenido del archivo y llama al método original en reflexión.

Código

analizar archivo

@Override
public void load(ICache<K, V> cache) {
    List<String> lines = FileUtil.readAllLines(dbPath);
    log.info("[load] 开始处理 path: {}", dbPath);
    if(CollectionUtil.isEmpty(lines)) {
        log.info("[load] path: {} 文件内容为空,直接返回", dbPath);
        return;
    }

    for(String line : lines) {
        if(StringUtil.isEmpty(line)) {
            continue;
        }
        // 执行
        // 简单的类型还行,复杂的这种反序列化会失败
        PersistAofEntry entry = JSON.parseObject(line, PersistAofEntry.class);
        final String methodName = entry.getMethodName();
        final Object[] objects = entry.getParams();
        final Method method = METHOD_MAP.get(methodName);
        // 反射调用
        ReflectMethodUtil.invoke(cache, method, objects);
    }
}

Precarga de mapas de métodos

La reflexión del método es fija, para mejorar el rendimiento, hacemos algunos preprocesos.

/**
 * 方法缓存
 *
 * 暂时比较简单,直接通过方法判断即可,不必引入参数类型增加复杂度。
 * @since 0.0.10
 */
private static final Map<String, Method> METHOD_MAP = new HashMap<>();
static {
    Method[] methods = Cache.class.getMethods();
    for(Method method : methods){
        CacheInterceptor cacheInterceptor = method.getAnnotation(CacheInterceptor.class);
        if(cacheInterceptor != null) {
            // 暂时
            if(cacheInterceptor.aof()) {
                String methodName = method.getName();
                METHOD_MAP.put(methodName, method);
            }
        }
    }
}

prueba

contenido del documento

  • default.aof
{"methodName":"put","params":["1","1"]}

prueba

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .load(CacheLoads.<String, String>aof("default.aof"))
        .build();

Assert.assertEquals(1, cache.size());
System.out.println(cache.keySet());

Cargue el archivo default.aof directamente en la caché.

resumen

La persistencia de archivos de Redis es realmente más rica.

Puede admitir el uso mixto de los modos rdb y aof.

El tamaño del archivo del modo aof será muy grande Para solucionar este problema, redis comprimirá los comandos con regularidad.

Se puede entender que aof es una tabla de flujo de operaciones. Lo que realmente nos importa es solo un estado final. No importa cuántos pasos se pasen en el medio, solo nos importa el valor final.

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 ~

Supongo que te gusta

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