Java手書きredisをゼロから(6)redisAOF永続性原則の詳細な説明と実装

序文

Javaは最初からredisを手動で実装します(1)固定サイズのキャッシュを実現するにはどうすればよいですか?

Javaはredisを最初から手作業で実装します(3)redisの有効期限の原則

Javaは最初から手動でredisを実装します(3)メモリデータを失うことなく再起動する方法は?

Javaは手動でredisを最初から実装します(4つ)リスナーを追加します

redis(5)有効期限戦略を最初から実装する別の方法

以前、redisのいくつかの機能を実装しただけです。Javaはredisを最初から手動で実装します(3)メモリデータを失うことなく再起動するにはどうすればよいですか?redisと同様のRDBモードがに実装されています。

redisaofの基本

RedisAOFの永続性の詳細

AOFの個人的な理解

なぜAOFを選ぶのですか?

AOFモードのパフォーマンス特に優れていますが、どの程度優れていますか?

Kafkaを使用したことのある学生は、Kafkaが順次書き込み機能も使用することを知っている必要があります。

ランダムなファイルIO書き込みの問題を回避して、ファイルコンテンツを順番に書き込み、追加します。パフォーマンスは基本的にメモリに匹敵します。

AOF、RDBモードと比較して、より優れたリアルタイムパフォーマンスを備えています。

当初はRDBモードを使用して、キャッシュされたすべてのコンテンツを永続化しました。これは比較的時間のかかるアクションであり、通常は数分ごとです。

AOFモードは、主にコンテンツを変更するための命令であり、すべての命令が順番にファイルに追加されます。この場合、リアルタイムのパフォーマンスははるかに向上し、2番目のレベルまたは2番目のレベルにまで上げることができます。

AOFスループット

AOFモードはすべての操作で持続できますが、これによりスループットが大幅に低下します。

スループットを向上させる最も一般的な方法はバッチ処理です。これはKafkaでも同様です。たとえば、1秒に1回永続化し、1秒にすべての操作をバッファーに入れることができます。

これは実際にはトレードオフの問題であり、リアルタイムのパフォーマンスとスループットのバランスを取る技術です。

実際のビジネスでは、1の誤差は一般的に許容できるため、これも業界でより認識されている方法です。

AOFの非同期+マルチスレッド

Kafkaのすべての操作は、実際には非同期で実装されます+コールバック。

非同期+マルチスレッドは、実際に操作のパフォーマンスを向上させることができます。

もちろん、redis 6より前は、実際にはシングルスレッドでした。では、なぜパフォーマンスがまだそれほど良いのですか?

実際、マルチスレッドには代償もあります。つまり、スレッドコンテキストの切り替えには時間がかかり、同時実行の安全性の問題を維持するには、ロックする必要があるため、パフォーマンスが低下します。

したがって、ここでは、非同期の利点が費やされた時間に比例するかどうかの問題を検討する必要があります。

AOF発注

最終的な分析では、AOFモードとRDBモードは、永続性のためにオペレーティングシステムのファイルシステムに基づいています。

開発者の場合、apiを呼び出すことで実現できますが、注文を永続化する実際のアクションは1つのステップで完了しない場合があります。

スループットを向上させるために、ファイルシステムもバッファのようなアプローチを使用します。突然、ロシアのマトリオシュカが少し出てきました。

ただし、優れた設計は常に類似しています。たとえば、キャッシュにはcpuの設計からのL1 / L2などがあります。考え方は同じです。

アリババのオープンソーステクノロジーの多くは、オペレーティングシステムの配置に合わせてさらに最適化されます。詳細な調査は後で行います。

AOFの欠陥

幹線道路には行方不明で、特効薬はありません。

AOFはとても良いです、そしてRDBと比較して欠陥があります、それは命令です

javaの実装

インターフェース

インターフェイスは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);

}

注釈の定義

時間のかかる統計、更新、その他の特性との一貫性を保つために、操作タイプのアクションがファイルに追加される(ファイルに追加される)ために、コードに固定的に記述されるのではなく、注釈属性に基づいて指定します。これは、後の拡張と調整に便利です。

/**
 * 缓存拦截器
 * @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;

}

@CacheInterceptoraof動作モードを開くかどうかを指定するaofプロパティアノテーション追加されました。

aofモードの指定方法

データを変更するメソッドでこの注釈属性を指定します。

期限切れの操作

Springのトランザクションインターセプターと同様に、プロキシクラスを使用してexpireAtを呼び出します。

expireメソッドはaofインターセプトを追加する必要はありません。

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

操作の変更

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

AOF永続的インターセプトの実装

永続的なオブジェクトの定義

/**
 * 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
}

ここでは、メソッド名とパラメーターオブジェクトのみが必要です。

とりあえずもっと簡単にできます。

永続的なインターセプター

インターセプターを定義します。キャッシュが永続クラスとして定義されている場合CachePersistAof、情報はバッファーリスト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);
        }
    }

}

インターセプターコール

AOFの注釈属性がtrueの場合は、上記のインターセプターを呼び出すだけです。

ここでの無駄を避けるために、呼び出しは永続クラスがAOFモードの場合にのみ行われます。

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

AOF永続性の実装

ここでのAOFモードと以前のRDB永続化クラスはまったく異なるモードであり、実際、2つは同じインターフェースです。

インターフェース

ここでは、RDBと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();
}

永続性クラスの実装

インターセプターごとにバッファーリストを実装して、直接順次追加します。

永続性の実装も比較的簡単です。ファイルに追加した後、バッファリストを直接クリアするだけです。

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

}

持続性テスト

テストコード

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

テストログ

Expireは実際には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] - 完成持久化缓存信息

ドキュメントの内容

1.aof ファイルの内容は次のとおりです

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

各操作をファイルに保存するだけです。

AOFローディングの実装

負荷

RDBのロードモードと同様に、aofのロードモードも同様です。

ファイルのコンテンツに基づいて、以前にキャッシュされたコンテンツを復元する必要があります。

実装のアイデア:ファイルの内容をトラバースし、リフレクションで元のメソッドを呼び出します。

コード

ファイルの分析

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

メソッドマップのプリロード

メソッドの反映が修正されました。パフォーマンスを向上させるために、いくつかの前処理を行います。

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

テスト

ドキュメントの内容

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

テスト

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

default.aofファイルを直接キャッシュにロードします。

概要

Redisファイルの永続性は実際にはより豊富です。

rdbモードとaofモードの混合使用をサポートできます。

aofモードのファイルサイズは非常に大きくなります。この問題を解決するために、redisはコマンドを定期的に圧縮します。

aofは操作フローテーブルであることが理解できます。実際に気にするのは最終状態だけです。途中でいくつのステップを通過しても、最終値だけを気にします。

記事は主にアイデアについて述べていますが、スペースの制限のため、実現部分はすべて掲載されていません。

オープンソースアドレス:https://github.com/houbb/cache

この記事がお役に立てば幸いです。いいね、コメント、ブックマーク、そして波をフォローしてください〜

あなたの励ましが私の最大の動機です〜

おすすめ

転載: blog.51cto.com/9250070/2539791