天天看点

《redis设计与实现》读后总结参考1. redis 协议2. redis载入与保存3. 事件4. 客户端5. 服务端6. 多机数据库的实现思考

参考

《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 重写时,fork子进程

aof写入:

  1. 所有写入都会保存记录在aof_buf缓冲区
  2. 每个事件循环结束前调用

    flushAppendOnlyFile

    将aof_buf缓冲区写入aof文件。 是否同步,根据

    appendfsync

    1. 若为always,同步到文件
    2. 若为everysec,若距离上次同步超过1s,则同步到文件。
    3. 若为no,不同步到文件,由os决定是否同步。

aof重写:

  1. fork出子进程,根据写时复制,读取快照,写入aof重写文件。
  2. 期间的新写入操作,都追加到aof重写缓冲区。
  3. 子进程完成时,向父进程发送一个信号。
  4. 父进程收到信号,阻塞完成信号处理函数:
    1. 将AOF重写缓冲区全部写入新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
    2. 对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换。

2.2 载入

aof开启时,优先采用aof。

3. 事件

文件事件

单线程Reactor模型。处理

AE_READABLE

AE_WRITABLE

事件。

原理:

  1. 用I/O多路复用(evport、epoll等之一)监听各个socket,并维护了一个处理队列。触发事件的socket会进入这个队列。
  2. 文件事件分派器不断从队列取出socket,并处理事件。一个socket的触发事件都被处理完成后,才取出下一个处理。

时间事件

无序链表。但实际使用时,其上只有一两个事件。当前版本正常模式下,只有

serverCron

一个时间事件。

serverCron

会做各种常规清理、调整工作,具体的事情可参照原文P.243。

服务器主函数

def main():

    # 初始化服务器
    init_server()

    # 一直处理事件,直到服务器关闭为止
    while server_is_not_shutdown():
        aeProcessEvents()

    # 服务器关闭,执行清理操作
    clean_server()           

复制

aeProcessEvents: 阻塞等待最近的时间事件到达。苏醒后,处理文件事件和时间事件

事件处理

文件、时间事件处理器都会适时主动让出执行权,减少阻塞。

4. 客户端

5. 服务端

5.1 命令请求的步骤

一个命令请求从发送到完成主要包括以下步骤:

  1. 客户端将命令请求发送给服务器;
  2. 服务器读取命令请求,并分析出命令参数;
  3. 命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;
  4. 服务器将命令回复返回给客户端。

5.2 计算每秒执行次数

  1. serverCron

    函数中的

    trackOperationsPerSecond

    每100毫秒执行一次。
  2. trackOperationsPerSecond

    根据上次调用后的间隔时间和执行次数,估算1秒执行次数,放入环形数组

    redisServer.ops_sec_samples

  3. 客户端执行

    INFO

    命令时,

    getOperationsPerSecond

    根据环形数组平均值计算一秒内估算执行次数。

5.3 启动流程

服务器从启动到能够处理客户端的命令请求需要执行以下步骤:

  1. 初始化服务器状态;
  2. 载入服务器配置;
  3. 初始化服务器数据结构;
  4. 还原数据库状态;
  5. 执行事件循环。

其它

看原书单元末尾的总结

6. 多机数据库的实现

6.1 主从复制

6.1.1 旧版主从复制

做法是:

  1. 从服务器向主服务器发送

    sync

  2. 主服务器执行

    BGSAVE

    ,保存rdb文件,发送给从服务器。
  3. 期间主服务器的额外写入都保存到缓冲区。
  4. 主服务器发送rdb后,将缓冲区也一并发送给从服务器。

缺点:

对于短线重连来说,

sync

效率太低。

6.1.2 新版主从复制

psync

的部分重同步中,主向从发送

+continue

,并发送断线期间的数据,以完成同步。

只要断线时的offset之后的内容都在复制积压缓冲区内,则可以部分重同步。

复制积压缓冲区默认为1MB。其大小可根据

second * write_size_per_second

来估算。

6.2

思考

  1. 似乎

    HSET

    SET

    更适合对象的存储,因为可以对每个

    field

    进行操作(而不用 反序列化->修改对象->序列化)。那常规的序列化存储对象的方法(比如用json serializer)是否合适呢?会不会效率低了。
    • 考虑如何用hset实现对象存储