前言

java从零手写实现redis(一)如何实现固定大小的缓存?

java从零手写实现redis(三)redis expire 过期原理

java从零手写实现redis(三)内存数据如何重启不丢失?

java从零手写实现redis(四)添加监听器

java从零手写实现redis(五)过期策略的另一种实现思路

我们前面简单实现了 redis 的几个特性,java从零手写实现redis(三)内存数据如何重启不丢失? 中实现了类似 redis 的 RDB 模式。

redis aof 基础

Redis AOF 持久化详解

AOF 的一些个人理解

为什么选择 AOF?

AOF 模式的性能特别好,有多好呢?

用过 kafka 的同学肯定知道,kafka 也用到了顺序写这个特性。

顺序写添加文件内容,避免了文件 IO 的随机写问题,性能基本可以和内存媲美。

AOF 的实时性更好,这个是相对于 RDB 模式而言的。

我们原来使用 RDB 模式,将缓存内容全部持久化,这个是比较耗时的动作,一般是几分钟持久化一次。

AOF 模式主要是针对修改内容的指令,然后将所有的指令顺序添加到文件中。这样的话,实时性会好很多,可以提升到秒级别,甚至秒级别。

AOF 的吞吐量

AOF 模式可以每次操作都进行持久化,但是这样会导致吞吐量大大下降。

提升吞吐量最常用的方式就是批量,这个 kafka 中也是类似的,比如我们可以 1s 持久化一次,将 1s 内的操作全部放入 buffer 中。

这里其实就是一个 trade-off 问题,实时性与吞吐量的平衡艺术。

实际业务中,1s 的误差一般都是可以接受的,所以这个也是业界比较认可的方式。

AOF 的异步+多线程

kafka 中所有的操作实际上都是异步+回调的方式实现的。

异步+多线程,确实可以提升操作的性能。

当然 redis 6 以前,其实一直是单线程的。那为什么性能依然这么好呢?

其实多线程也有代价,那就是线程上下文的切换是需要耗时的,保持并发的安全问题,也需要加锁,从而降低性能。

所以这里要考虑异步的收益,与付出的耗时是否成正比的问题。

AOF 的落盘

我们 AOF 与 RDB 模式,归根结底都是基于操作系统的文件系统做持久化的。

对于开发者而言,可能就是调用一个 api 就实现了,但是实际持久化落盘的动作并不见得就是一步完成的。

文件系统为了提升吞吐量,也会采用类似 buffer 的方式。这忽然有一点俄罗斯套娃的味道。

但是优秀的设计总是相似的,比如说缓存从 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); }

注解定义

为了和耗时统计,刷新等特性保持一致,对于操作类的动作才添加到文件中(append to file)我们也基于注解属性来指定,而不是固定写死在代码中,便于后期拓展调整。

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

我们在原来的 @CacheInterceptor 注解中添加 aof 属性,用于指定是否对操作开启 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
}

这里我们只需要方法名,和参数对象。

暂时实现的简单一些即可。

持久化拦截器

我们定义拦截器,当 cache 中定义的持久化类为 CachePersistAof 时,将操作的信息放入到 CachePersistAof 的 buffer 列表中。

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 持久化类只是不同的模式,实际上二者是相同的接口。

接口

这里我们统一定义了不同的持久化类的时间,便于 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();
}

持久化类实现

实现一个 Buffer 列表,用于每次拦截器直接顺序添加。

持久化的实现也比较简单,追加到文件之后,直接清空 buffer 列表即可。

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

方法映射的预加载

Method 反射是固定的,为了提升性能,我们做一下预处理。

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

小结

redis 的文件持久化,实际上更加丰富。

可以支持 rdb 和 aof 两种模式混合使用。

aof 模式的文件体积会非常大,redis 为了解决这个问题,会定时对命令进行压缩处理。

可以理解为 aof 就是一个操作流水表,我们实际上关心的只是一个终态,不论中间经过了多少步骤,我们只关心最后的值。

文中主要讲述了思路,实现部分因为篇幅限制,没有全部贴出来。

开源地址:https://github.com/houbb/cache

觉得本文对你有帮助的话,欢迎点赞评论收藏关注一波~

你的鼓励,是我最大的动力~

java 从零开始手写 redis(六)redis AOF 持久化原理详解及实现的更多相关文章

  1. java 从零开始手写 RPC (03) 如何实现客户端调用服务端?

    说明 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 写完了客户端和服务端,那么如何实现客户端和服务端的 ...

  2. java 从零开始手写 RPC (04) -序列化

    序列化 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何实 ...

  3. java 从零开始手写 RPC (05) reflect 反射实现通用调用之服务端

    通用调用 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何 ...

  4. java 从零开始手写 RPC (07)-timeout 超时处理

    <过时不候> 最漫长的莫过于等待 我们不可能永远等一个人 就像请求 永远等待响应 超时处理 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RP ...

  5. Java web Cookie详解(持久化+原理详解+共享问题+设置中文+发送多个Cookie)

    Java web Cookie详解 啥是cookie? 查询有道词典得: web和饼干有啥关系? 这个谜底等等来为大家揭晓 会话技术 web中的会话技术类似于生活中两个人聊天,不过web中的会话指的是 ...

  6. java 从零开始手写 RPC (01) 基于 websocket 实现

    RPC 解决的问题 RPC 主要是为了解决的两个问题: 解决分布式系统中,服务之间的调用问题. 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑. 这一节我们来学习下如何基于 we ...

  7. 高性能的Redis之对象底层实现原理详解

    对象 在前面的数个章节里, 我们陆续介绍了 Redis 用到的所有主要数据结构, 比如简单动态字符串(SDS).双端链表.字典.压缩列表.整数集合, 等等. Redis 并没有直接使用这些数据结构来实 ...

  8. Java模拟登录带验证码的教务系统(原理详解)

    一:原理 客户端访问服务器,服务器通过Session对象记录会话,服务器可以指定一个唯一的session ID作为cookie来代表每个客户端,用来识别这个客户端接下来的请求. 我们通过Chrome浏 ...

  9. Redis for Windows(C#缓存)配置文件详解

    Redis for Windows(C#缓存)配置文件详解   前言 在上一篇文章中主要介绍了Redis在Windows平台下的下载安装和简单使用http://www.cnblogs.com/aehy ...

  10. redis cluster 集群 安装 配置 详解

    redis cluster 集群 安装 配置 详解 张映 发表于 2015-05-01 分类目录: nosql 标签:cluster, redis, 安装, 配置, 集群 Redis 集群是一个提供在 ...

随机推荐

  1. 最近遇到的问题记录:UrlEncode、UrlDecode

    本文阅读前了解知识:什么时候需要使用UrlEncode和UrlDecode函数 作者使用谷歌浏览器,通过按下F12对第三方网站http协议的接口抓包进行分析操作. 场景 运维小哥哥偶尔使用某某外包公司 ...

  2. 浪潮CE3000F飞腾PC安装UOS/银河麒麟双系统的过程

    浪潮CE3000F飞腾PC安装UOS/银河麒麟双系统的过程 背景 为了进行兼容性验证, 部门采购过一批浪费CE3000F的PC机器. 前期系统安装的是UOS, 但是有同事借走机器后重装了银河麒麟V10 ...

  3. tidb备份恢复的方式方法

    tidb备份恢复的方式方法 摘要 可以单独每个数据库实例进行备份,但是这种机制实在是太慢了. 网上查资料发现可以使用 tiup br 的方式进行备份. 但是大部分文档都比较陈旧, 官网上面又比较贴心的 ...

  4. [转帖]etcd网络模块解析

    https://www.cnblogs.com/luohaixian/p/17509742.html 1. RaftHttp模块介绍 在etcd里raft模块和网络模块是分开的,raft模块主要负责实 ...

  5. [转帖]Elasticsearch8关闭安全认证功能

    https://juejin.cn/post/7203637198120878137 Elasticsearch8在默认情况下是开启安全认证的.但在开发或者简单尝试时,希望关闭它. 关闭安全认证的方式 ...

  6. [转帖]【P1】Jmeter 准备工作

    文章目录 一.Jmeter 介绍 1.1.Jmeter 有什么样功能 1.2.Jmeter 与 LoadRunner 比较 1.3.常用性能测试工具 1.4.性能测试工具如何选型 1.5.学习 Jme ...

  7. [转帖]python中input()、print()用法

    https://www.cnblogs.com/lei3082195861/p/16967109.html 1.input()函数常涉及的强制类型转换 第一种是在键入时进行转换,例如:a = int( ...

  8. [转帖]精通awk系列(19):awk流程控制之break、continue、next、nextfile、exit语句

    https://www.cnblogs.com/f-ck-need-u/   回到: Linux系列文章 Shell系列文章 Awk系列文章 break和continue break可退出for.wh ...

  9. [转帖]SpringBoot配置SSL 坑点总结【密码验证失败、连接不安全】

    文章目录 前言 1.证书绑定问题 2.证书和密码不匹配 3.yaml配置文件问题 3.1 解密类型和证书类型是相关的 3.2 配置文件参数混淆 后记 前言 在SpringBoot服务中配置ssl,无非 ...

  10. [转帖] GC耗时高,原因竟是服务流量小?

      原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 简介# 最近,我们系统配置了GC耗时的监控,但配置上之后,系统会偶尔出现GC耗时大于1s的报警,排查花了一些力气,故 ...