Java redis escrito à mão do zero (6) explicação detalhada e implementação do princípio de persistência AOF do redis

Prefácio

Java implementa redis manualmente do zero (1) Como obter um cache de tamanho fixo?

Java implementa redis manualmente do zero (três) princípio de expiração de expiração de redis

Java implementa redis manualmente do zero (3) Como reiniciar sem perder dados de memória?

Java implementa redis do zero manualmente (quatro) add listener

Outra maneira de implementar a estratégia de expiração do redis (5) do zero

Simplesmente implementamos vários recursos do redis anteriormente.O Java implementa o redis manualmente a partir do zero (3) Como reiniciar sem perder dados de memória? O modo RDB semelhante ao redis é implementado em.

redis aof basics

Persistência Redis AOF detalhada

Alguma compreensão pessoal de AOF

Por que escolher AOF?

O desempenho do modo AOF é particularmente bom , quão bom é?

Os alunos que usaram o Kafka devem saber que o Kafka também usa o recurso de escrita sequencial.

Grave e adicione conteúdo de arquivo sequencialmente, evitando problemas de gravação de E / S de arquivos aleatórios, e o desempenho é basicamente comparável à memória.

AOF tem melhor desempenho em tempo real , que é relativo ao modo RDB.

Originalmente, usamos o modo RDB para manter todo o conteúdo armazenado em cache.Esta é uma ação relativamente demorada, geralmente a cada poucos minutos.

O modo AOF é principalmente para instruções para modificar o conteúdo e, em seguida, todas as instruções são adicionadas ao arquivo em ordem. Nesse caso, o desempenho em tempo real será muito melhor, podendo ser elevado ao segundo nível ou mesmo ao segundo nível.

Throughput AOF

O modo AOF pode ser persistido para todas as operações, mas isso causará uma diminuição significativa na taxa de transferência.

A maneira mais comum de melhorar a taxa de transferência é o envio em lote . Isso é semelhante no Kafka. Por exemplo, podemos persistir uma vez em 1s e colocar todas as operações em 1s no buffer.

Na verdade, esse é um problema de compensação, a arte de equilibrar desempenho e rendimento em tempo real.

Nos negócios reais, o erro de 1s é geralmente aceitável, portanto, este também é um método mais reconhecido na indústria.

AOF assíncrono + multithreading

Todas as operações no Kafka são realmente implementadas de forma assíncrona + callbacks.

Assíncrono + multithreading pode realmente melhorar o desempenho da operação.

Claro, antes do redis 6, ele era na verdade de thread único. Então, por que o desempenho ainda é tão bom?

Na verdade, o multi-threading também tem um preço, ou seja, a troca de contexto de thread é demorada e, para manter os problemas de segurança de simultaneidade, também precisa ser bloqueada, reduzindo assim o desempenho.

Portanto, temos que considerar a questão de saber se o benefício do assíncrono é proporcional ao tempo gasto.

Colocação de pedido AOF

Nossos modos AOF e RDB, em última análise, são baseados no sistema de arquivos do sistema operacional para persistência.

Para os desenvolvedores, isso pode ser alcançado chamando uma API, mas a ação real de persistir o pedido pode não ser concluída em uma etapa.

Para melhorar o rendimento, o sistema de arquivos também usa uma abordagem semelhante a um buffer. De repente, houve um pouco de matryoshka russa.

Mas um bom design é sempre semelhante. Por exemplo, o cache tem L1 / L2 e assim por diante a partir do design da CPU. As idéias são as mesmas.

Muitas das tecnologias de código aberto do Alibaba serão otimizadas ainda mais para o posicionamento do sistema operacional. Faremos um estudo aprofundado mais tarde.

As falhas do AOF

Falta um na estrada principal, não há bala de prata.

AOF é muito bom, e há uma falha em comparação com RDB, que é a instrução

implementação java

interface

A interface é consistente com a do 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);

}

Definição de anotação

A fim de ser consistente com as estatísticas demoradas, atualização e outras características, para as ações do tipo de operação serem adicionadas ao arquivo (anexar ao arquivo), também especificamos com base em atributos de anotação, em vez de gravados fixamente no código, o que é conveniente para expansão e 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;

}

Tivemos a @CacheInterceptoradicionado AOF especifica propriedade de anotação se deseja abrir AOF modo de operação.

Método de especificar um modo de

Especificamos este atributo de anotação no método que irá alterar os dados:

Operação expirada

Semelhante ao interceptor de transação do Spring, usamos a classe proxy para chamar expireAt.

O método de expiração não precisa adicionar uma função de interceptação.

/**
 * 设置过期时间
 * @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;
}

Operação de mudança

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

Implementação de interceptação persistente AOF

Definição 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
}

Aqui, precisamos apenas do nome do método e do objeto de parâmetro.

Pode ser mais simples por enquanto.

Interceptor persistente

Definimos os interceptores, quando o cache é definido como classes persistentes CachePersistAof, a informação será colocada em operação na lista de buffer 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);
        }
    }

}

Chamada de interceptador

Quando o atributo de anotação de AOF for verdadeiro, basta chamar o interceptor acima.

Para evitar desperdício aqui, a chamada só é feita quando a classe persistente está no modo AOF.

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

Implementação de persistência AOF

O modo AOF aqui e as classes de persistência RDB anteriores são apenas modos diferentes; na verdade, os dois são a mesma interface.

interface

Aqui, definimos uniformemente o tempo de diferentes classes de persistência para facilitar o acionamento de diferentes intervalos de tempo para diferentes tarefas de RDB e 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();
}

Implementação de classe de persistência

Implemente uma lista de buffer para cada interceptor para adicionar diretamente em sequência.

A implementação da persistência também é relativamente simples.Depois de anexar ao arquivo, basta limpar a lista de buffer diretamente.

/**
 * 缓存持久化-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);
        }
    }

}

Teste de persistência

Código de teste

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 teste

Expire, na verdade, chama 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] - 完成持久化缓存信息

conteúdo do documento

1.aof O conteúdo do arquivo é o seguinte

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

Basta armazenar cada operação em um arquivo.

Implementação de carregamento AOF

carga

Semelhante ao modo de carregamento do RDB, o modo de carregamento do aof também é semelhante.

Precisamos restaurar o conteúdo em cache anterior com base no conteúdo do arquivo.

Ideia de implementação: atravesse o conteúdo do arquivo e chame o método original em reflexão.

Código

analisar arquivo

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

Pré-carregamento de mapas de método

A reflexão do método é fixa, a fim de melhorar o desempenho, fazemos alguns pré-processamento.

/**
 * 方法缓存
 *
 * 暂时比较简单,直接通过方法判断即可,不必引入参数类型增加复杂度。
 * @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);
            }
        }
    }
}

teste

conteúdo do documento

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

teste

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

Carregue o arquivo default.aof diretamente no cache.

resumo

A persistência do arquivo Redis é realmente mais rica.

Ele pode suportar o uso misto dos modos rdb e aof.

O tamanho do arquivo do modo aof será muito grande. Para resolver esse problema, o redis compactará os comandos regularmente.

Pode ser entendido que aof é uma tabela de fluxo de operação. O que realmente importa é apenas um estado final. Não importa quantas etapas são passadas no meio, nos preocupamos apenas com o valor final.

O artigo fala principalmente sobre as ideias, e a parte da realização não é publicada devido a limitações de espaço.

Endereço de código aberto:https://github.com/houbb/cache

Se você acha que este artigo é útil para você, fique à vontade para curtir, comentar, marcar como favorito e seguir uma onda ~

Seu incentivo é minha maior motivação ~

Acho que você gosta

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