参考
《redis设计与实现》
1. redis 协议
1.1 redis请求与回复协议格式
参考 redis请求与回复协议格式
请求同一为:
*<number of arguments> CR LF
$<number of bytes of argument 1> CR LF
<argument data> CR LF
...
$<number of bytes of argument N> CR LF
<argument data> CR LF
回复根据类型,有五种:
- 用单行回复,回复的第一个字节将是“+”
- 错误消息,回复的第一个字节将是“-”
- 整型数字,回复的第一个字节将是“:”
- 批量回复,回复的第一个字节将是“$”
- 多个批量回复,回复的第一个字节将是“*”
1.2 编写redis协议并执行
剖析Redis协议
按照redis协议格式编写.ptl
文件 ,然后用管道命令交给Redis一并执行。
关于redis管道:
参考 redis通过pipeline提升吞吐量
就是说,以前是:每条命令进行一次请求和响应。
现在是:将请求的多条命令合并成一条,一并发送给服务端执行,执行结果由客户端一次性返回。
好处是大大减少了网络I/O,增加了效率和并发能力。
2. redis载入与保存
2.1 保存
2.1.1 rdb模式
- SAVE 阻塞服务器进程
- BGSAVE fork子进程保存,完成后通知主进程。
BGSAVE
serverCron
函数每100ms检查一次saveparams
:
struct redisServer {
// ...
// 记录了保存条件的数组
struct saveparam *saveparams;
// ...
};
当任意一个条件满足时执行BGSAVE。
2.1.2 aof模式
- BGREWRITEAOF 重写时,会创建一个当前 AOF 文件的体积优化版本,fork子进程
aof写入:
- 所有写入都会保存记录在aof_buf缓冲区
- 每个事件循环结束前调用
flushAppendOnlyFile
将aof_buf缓冲区写入aof文件。 是否同步,根据appendfsync
:- 若为always,同步到文件
- 若为everysec,若距离上次同步超过1s,则同步到文件。
- 若为no,不同步到文件,由os决定是否同步。
aof重写:
- fork出子进程,根据写时复制,读取快照,写入aof重写文件。
- 期间的新写入操作,都追加到aof重写缓冲区。
- 子进程完成时,向父进程发送一个信号。
- 父进程收到信号,阻塞完成信号处理函数:
- 将AOF重写缓冲区全部写入新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
- 对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换。
2.2 载入
aof开启时,优先采用aof。
3. 事件
文件事件
单线程Reactor模型。处理AE_READABLE
和AE_WRITABLE
事件。
原理:
-
用I/O多路复用(evport、epoll等之一)监听各个socket,并维护了一个处理队列。触发事件的socket会进入这个队列。
-
文件事件分派器不断从队列取出socket,并处理事件。一个socket的触发事件都被处理完成后,才取出下一个处理。
时间事件
无序链表。但实际使用时,其上只有一两个事件。当前版本正常模式下,只有serverCron
一个时间事件。
serverCron
会做各种常规清理、调整工作,具体的事情可参照原文P.243。
服务器主函数
def main():
# 初始化服务器
init_server()
# 一直处理事件,直到服务器关闭为止
while server_is_not_shutdown():
aeProcessEvents()
# 服务器关闭,执行清理操作
clean_server()
aeProcessEvents: 阻塞等待最近的时间事件到达。苏醒后,处理文件事件和时间事件
事件处理
文件、时间事件处理器都会适时主动让出执行权,减少阻塞。
4. 客户端
5. 服务端
5.1 命令请求的步骤
一个命令请求从发送到完成主要包括以下步骤:
- 客户端将命令请求发送给服务器;
- 服务器读取命令请求,并分析出命令参数;
- 命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;
- 服务器将命令回复返回给客户端。
5.2 计算每秒执行次数
-
serverCron
函数中的trackOperationsPerSecond
每100毫秒执行一次。 -
trackOperationsPerSecond
根据上次调用后的间隔时间和执行次数,估算1秒执行次数,放入环形数组redisServer.ops_sec_samples
。 - 客户端执行
INFO
命令时,getOperationsPerSecond
根据环形数组平均值计算一秒内估算执行次数。
5.3 启动流程
服务器从启动到能够处理客户端的命令请求需要执行以下步骤:
- 初始化服务器状态;
- 载入服务器配置;
- 初始化服务器数据结构;
- 还原数据库状态;
- 执行事件循环。
其它
看原书单元末尾的总结
6. 多机数据库的实现
6.1 主从复制
6.1.1 旧版主从复制
做法是:
- 从服务器向主服务器发送
sync
。 - 主服务器执行
BGSAVE
,保存rdb文件,发送给从服务器。 - 期间主服务器的额外写入都保存到缓冲区。
- 主服务器发送rdb后,将缓冲区也一并发送给从服务器。
缺点:
对于短线重连来说,sync
效率太低。
6.1.2 新版主从复制
psync
的部分重同步中,主向从发送+continue
,并发送断线期间的数据,以完成同步。
只要断线时的offset之后的内容都在复制积压缓冲区内,则可以部分重同步。
复制积压缓冲区默认为1MB。其大小可根据
second * write_size_per_second
来估算。
6.2
思考
- 似乎
HSET
比SET
更适合对象的存储,因为可以对每个field
进行操作(而不用 反序列化->修改对象->序列化)。那常规的序列化存储对象的方法(比如用json serializer)是否合适呢?会不会效率低了。- 考虑如何用hset实现对象存储
网友评论