你知道 Redis 使用的什么协议吗?
有个小伙伴面试回来说面试官问了他一些 Redis 问题,但是他好像没有回答上来。
我说,你 Redis 不是用的很溜吗,什么问题难住你了。
他说,事情是这样的,刚开始,问了一些基础的问题,比如 Redis 的几种基本数据类型和使用场景,以及主从复制和集群的一些问题,这些都还好。
然后问 Redis 的两种持久化方式,他说与 RDB 和 AOF 两种方式,RDB 数据文件小,恢复速度快,但是对性能有影响,而且不适合实时存储。而 AOF 是现在最常用的持久化方式,它的一大优点就是实时性,并且对 Redis 半身性能影响最小。
那面试又问了,你知道 AOF 持久化之后的文件是什么格式吗?
答:好像就是文本文件吧?
好,文本文件,那你知道它有什么规则吗?或者说,它和 Redis 的协议有什么关系吗?
答:啊,这个,恩,不太清楚呢。
现在就来看一下 AOF 和 RESP 协议的关系
从两种持久化方式说起。
RESP 协议是什么
动手实现一个简单的协议解析命令行工具
先从持久化说起,虽然一提到 Redis,首先想到的就是缓存,但是 Redis 不仅仅是缓存这么简单,它的定位是内存型数据库,可以存储多种类型的数据结构,还可以当做简单消息队列使用。既然是数据库,持久化功能是必不可少的。
Redis 的两种持久化方式
Redis 提供了两种持久化方式,一种是 RDB 方式,另外一种是 AOF 方式,AOF 是目前比较流行的持久化方案。
RDB 方式
RDB持久化是通过快照的方式,在指定的时间间隔内将内存中的数据集快照写入磁盘。它以一种紧凑压缩的二进制文件的形式出现。可以将快照复制到其他服务器以创建相同数据的服务器副本,或者在重启服务器后恢复数据。RDB是Redis默认的持久化方式,也是早期版本的必须方案。
RDB 由下面几个参数控制。
# 设置 dump 的文件名
dbfilename dump.rdb
# 持久化文件的存储目录
dir ./
# 900秒内,如果至少有1个key发生变化,就会自动触发bgsave命令创建快照
save 900 1
# 300秒内,如果至少有10个key发生变化,就会自动触发bgsave命令创建快照
save 300 10
# 60秒内,如果至少有10000个key发生变化,就会自动触发bgsave命令创建快照
save 60 10000
持久化流程
上面说到了配置文件中的几个触发持久化的机制,比如 900 秒、300秒、60秒,当然也可以手动执行命令 save
或bgsave
进行触发。bgsave
是非阻塞版本,通过 fork 出子进程的方式来进行快照生成,而 save
会阻塞主进程,不建议使用。
1、首先 bgsave
命令触发;
2、父进程 fork 出一个子进程,这一步是比较重量级的操作,也是 RDB 方式性能不及 AOF 的一个重要原因;
3、父进程 fork 出子进程后就可以正常的相应客户端发来的其他命令了;
4、子进程开始进行持久化工作,对现有数据进行完整的快照存储;
5、子进程完成操作后,通知父进程;
RDB的优点:
RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据 快照。非常适用于备份,全量复制等场景。比如每6小时执行bgsave备份, 并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。 Redis加载RDB恢复数据远远快于AOF的方式。
RDB的缺点:
RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运 行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。 RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式 的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。
AOF 方式
# appendonly参数开启AOF持久化
appendonly yes
# AOF持久化的文件名,默认是appendonly.aof
appendfilename "appendonly.aof"
# AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的
dir ./
# 同步策略
# appendfsync always
appendfsync everysec
# appendfsync no
# aof重写期间是否同步
no-appendfsync-on-rewrite no
# 重写触发配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 加载aof出错如何处理
aof-load-truncated yes
# 文件重写策略
aof-rewrite-incremental-fsync yes
AOF 文件里存的是什么
RESP 协议
实现简单; 快速解析; 可阅读;
redis-cli
或者一些客户端工具连接 Redis 服务端。./redis-cli
用 telnet 试试
redis-cli
工具,还可以用 telnet 查看,用 telnet 就可以看出 RESP 返回的原始数据格式了。telnet 127.0.0.1 6379
get str:hello
这条,返回的结果除了 world
值本身,上面还多了一行 $5
,是不是有点迷糊了。协议规则
请求命令
*<参数数量> CR LF
$<参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
...
$<参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF
\r\n
作为分隔符,会表明此条命令的具体参数个数,在命令上看来,空格分隔的都表示一个参数,例如 set str:hello world
这条命令就是3个参数,会表明每个参数的字符数和具体内容。*3\r\n$3\r\nset\r\n$9str:hello\r\n$5world\r\n
服务端回复
"+"
ping
命令的回复,+PONG\r\n
"-"
auth
,auth 命令后面需要有一个密码参数的,如果不输入就会返回错误回复类型。-ERR wrong number of arguments for 'auth' command\r\n
":"
INCR
、DECR
自增自减命令,返回的结果是这样的 :2\r\n
"$"
$5\r\nworld\r\n
,$
后面的数字 5 表示返回的结果有 5 个字符,后面是返回结果的实际内容。"*"
LRANGE key start stop
或者 hgetall
等返回多条结果的命令,比如 lrange
命令返回的结果:*2\r\n$6\r\nnews-2\r\n$6\r\nnews-1\r\n
实现一个简单的 Redis 交互工具
public Socket createSocket() throws IOException {
Socket socket = null;
try {
socket = new Socket();
socket.setReuseAddress(true);
socket.setKeepAlive(true);
socket.setTcpNoDelay(true);
socket.setSoLinger(true, 0);
socket.connect(new InetSocketAddress(host, port), DEFAULT_TIMEOUT);
socket.setSoTimeout(DEFAULT_TIMEOUT);
outputStream = socket.getOutputStream();
inputStream = socket.getInputStream();
return socket;
} catch (Exception ex) {
if (socket != null) {
socket.close();
}
throw ex;
}
}
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号
好文章,我在看❤️