《Redis的设计与实现》读书笔记(2)

2019.06.03

二、单机数据库的实现

1.数据库

1.1 服务器中的数据库

struct redisServer {

    /* General */

    // 配置文件的绝对路径
    char *configfile;           /* Absolute config file path, or NULL */

    // serverCron() 每秒调用的次数
    int hz;                     /* serverCron() calls frequency in hertz */

    // 数据库
    redisDb *db;
    ...
    int dbnum;                      /* Total number of configured DBs 默认值为16*/
};

20190603104739.png

1.2 切换数据库

切换数据库是对客户端而言的:在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库。

/* With multiplexing we need to take per-client state.
 * Clients are taken in a liked list.
 *
 * 因为 I/O 复用的缘故,需要为每个客户端维持一个状态。
 *
 * 多个客户端状态被服务器用链表连接起来。
 */
typedef struct redisClient {
    ...
      // 当前正在使用的数据库
    redisDb *db;
    ...
} redisClient;

select 1

20190603105334.png

1.3 数据库键空间

typedef struct redisDb {

    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;                 /* The keyspace for this DB */

    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    dict *expires;              /* Timeout of keys with a timeout set */

    // 正处于阻塞状态的键
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */

    // 可以解除阻塞的键
    dict *ready_keys;           /* Blocked keys that received a PUSH */

    // 正在被 WATCH 命令监视的键
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */

    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */

    // 数据库号码
    int id;                     /* Database ID */

    // 数据库的键的平均 TTL ,统计信息
    long long avg_ttl;          /* Average TTL, just for stats */

} redisDb;

20190603110454.png

读写键空间时的维护操作

当使用redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作:

  • 根据键是否存在来更新hit次数和miss次数
  • 读取一个键后,更新LRU时间(RedisObject中的LRU字段)
  • 如果服务器在读取一个键时,发现键已过期,会先删除这个键
  • 如果使用了watch命令监视了某个键,在该键被修改后,会标记为dirty,从而让事务程序注意到这个键
  • 每修改一个键后,会对dirty加1,会触发服务器的持久化与复制操作
  • 如果开启了数据库通知功能,会发送通知(pub/sub)

1.4设置键的生存或者过期时间

1.4.1设置查看过期时间

  • expire <key> <ttl>:将key的生存时间设置为ttl秒
  • pexpire <key> <ttl>:将key的生存时间设置为ttl毫秒
  • expireat <key> <timestamp>:将key的生存时间设置为timestamp指定的秒数时间戳
  • pexpireat <key> <timestamp>:将key的生存时间设置为timestamp指定的毫秒数时间戳
  • ttl <key>: 查看key的剩余生存时间(秒)
  • pttl <key>: 查看key的剩余生存时间(毫秒)

四个命令最终都是通过pexpireat命令实现的。

1.4.2保存过期时间

根据redisDb的dict *expires;字段

20190606125625.png

1.4.3移除过期时间

persist命令

1.5过期键删除策略

  • 主动策略
    • 定时删除:在设置过期时间的同时,设置一个定时器;内存友好,CPU不友好
  • 被动策略
    • 惰性删除: 每次取键的时候判断一下,看是否过期,过期就删除; CPU友好,内存不友好,可能造成内存泄漏
    • 定期删除:每隔一段时间,程序对数据库进行一次检查,删除里面的过期键;折中。

1.6Redis的过期删除策略

惰性删除和定期删除结合。定期删除策略有redis.c/activeExpireCycle函数实现,在规定的时间内,分多次遍历服务器的各个数据库。

1.7AOF、RDB和复制功能对过期键的处理

  • 执行save或bgsave命令产生的RDB文件不会包括已过期的键
  • 执行bgrewriteaof所产生的重写AOF不会包含已过期的键
  • 当一个过期键被删除时,服务器会显示的在AOF追加一个del命令
  • 当主服务器删除一个过期键时,会向所有服务器发送一条del命令,显示删除过期键
  • 从服务器发现过期时,不会自作主张删除,会等待来自主服务器的del命令,保证一致性

2.RDB持久化

2.1RDB文件的创建于载入

创建:savebgsave命令

  • save:阻塞的,在save期间,客户端的请求都会被拒绝
  • bgsave:由子进程执行,服务器进程仍可以继续客户端请求。在bgsave期间,服务器会拒绝save,bgsave命令,防止条件竞争。拒绝bgrewriteaof命令,性能考虑。

载入:在服务器启动时自动执行(AOF的优先级比RDB高,如果开启了AOF持久化功能,会优先使用AOF文件载入),载入期间服务器是阻塞的

2.1自动间隔性保存

配置save选项,让服务器每隔一段时间自动执行一次bgsave命令。通过redisServer中的struct saveparam *saveparams字段存储save选项。

save 900 1:在900秒内,对数据库至少进行了一次修改,bgsave命令就会被执行

struct redisServer {
    ...
    struct saveparam *saveparams;   /* Save points array for RDB */
    int saveparamslen;              /* Number of saving points */
    // 最后一次完成 SAVE 的时间
    time_t lastsave;                /* Unix time of last successful save */
     // 自从上次 SAVE 执行以来,数据库被修改的次数
    long long dirty;                /* Changes to DB from the last save */

    // BGSAVE 执行前的数据库被修改次数
    long long dirty_before_bgsave;  /* Used to restore dirty on failed BGSAVE */
    ...

}

// 服务器的保存条件(BGSAVE 自动执行的条件)

struct saveparam {

    // 多少秒之内
    time_t seconds;

    // 发生多少次修改
    int changes;

};

20190606160509.png

Redis的周期执行函数serverCron每隔100毫秒就会执行一次,该函数对正在运行的服务器进行维护,其中一项就是检查save的条件是否被满足。

2.3RDB文件结构

不同类型的键值对,Redis使用不同的方式保存。

20190606160812.png

20190606160853.png

20190606160916.png

20190606160929.png

2.4分析RDB文件

使用od命令打印rdb文件: od -c dump.rdb

20190606161849.png

3.AOF持久化

RDB文件保存的是键值对,AOF文件保存的是命令。

3.1 AOF持久化的实现

AOF文件的持久化可分为命令追加,文件写入和文件同步三个步骤。

3.1.1 文件追加

struct redisServer {
    ...
     // AOF 缓冲区
    sds aof_buf;      /* AOF buffer, written before entering the event loop */
    ...

}

在写完一个指令后,命令会被追加到aof_buf的末尾。

3.1.2 文件写入与文件同步

Redis服务器进程是一个事件循环,服务器在每次结束一个事件循环之前,会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区的内容写入和保存到AOF文件里。flushAppendOnlyFile函数行为需要appendSync选项配置,默认是everysec选项,每秒同步一次

伪代码:

def eventLoop():
    while True:
        # 处理文件事件,可能写aof_buf
        processFileEvent()

        # 处理时间事件
        processTimeEvent()

        flushAppendOnlyFile()

3.2AOF文件的载入和数据还原

服务器读入并重新执行一遍AOF文件中保存的命令。步骤:

  • 创建一个不带网络连接的伪客户端:因为Redis的命令只能在客户端上下文中执行。
  • 从AOF文件中分析并读取出一条写命令。
  • 使用伪客户端执行命令。
  • 执行2,3步直到完成

3.3AOF重写(bgaofrewrite)

因为AOF是通过保存被执行的命令来记录数据库状态的,所以随着时间的流逝,AOF文件会越来越大。为了解决体积膨胀问题,Redis提供了AOF重写功能,Redis会创建一个新的AOF文件来代替体积膨胀的旧文件,新AOF中没有冗余的命令,体积远小于旧问文件。

AOF重写是通过读取服务器现在状态实现的。首先读取键现在的值,然后通过一条命令代替之前记录这个键值对的多条命令。

AOF后台重写:

因为aof_rewrite函数会进行大量的写入操作,使用子进程执行AOF重写。一个问题:子进程在重写过程中,服务器进程仍然在处理命令请求,会造成数据不一致。为解决不一致问题,设置了一个AOF重写缓冲区,当子进程完成AOF重写时,会向父进程发送一个完成信号,父进程在接到信号时,会调用一个信号处理操作(是阻塞的,不能处理请求):

  • 将AOF重写缓冲区的内容写入新的AOF文件中。
  • 将新AOF文件重命名,覆盖旧AOF文件。

20190606180941.png

4.事件

Redis是一个事件驱动程序,需要处理两类事件:

  • 文件事件:Redis服务器通过套接字与客户端连接,文件事件就是对套接字操作的抽象。
  • 时间事件:Redis中的一些操作(如serverCron)需要在给定的时间点执行,时间事件是对这类定时操作的抽象。

4.1文件事件

Redis基于Reactor模式开发了自己的网络事件处理器(被称为文件事件处理器):

  • 文件事件处理器使用I/O多路复用模式同时监听多个套接字。
  • 当被监听的套接字准备好accept,read,write,close等操作时,与之关联的文件事件就会产生。

20190606182338.png

尽管多个文件事件可能并发的出现,但I/O多路复用程序会将产生事件的套接字都放在一个队列里面,通过这个队列,以有序,同步的方式,每次一个套接字的方式向文件事件分派器传送套接字。 Redis的I/O多路复用程序是通过包装select,epoll,evport等实现的。

4.2时间事件

Redis的时间事件分为两类:

  • 定时事件:在指定的时间点执行一次
  • 周期事件:每隔一段时间执行一次

一个时间事件有三部分组成:

  • id:事件的全局唯一ID
  • when:毫秒精度的时间戳,记录了事件的到达时间
  • timeProc:时间事件处理器

应用实例:serverCron函数(可以通过hz选项调整serverCron每秒执行次数)

时间事件的执行比预定的到达时间晚一点。

4.2.1实现

服务器将所有事件放在一个无序链表中,每当时间事件执行器运行时,就遍历链表查找已到达的事件,调用timeProc

20190606184241.png

4.3事件的调度与执行

事件的调度与执行由aeProcessEvents负责,伪代码:

def aeProcessEvents():
    time_event = aeSearchNearestTimer()
    remaind_ms = time_event.when - unix_ts_now()
    if(remaind_ms<0):
        remaind_ms = 0
    timeval = create_timeval_with_ms(remaind_ms)
    AeApipoll(timeval)
    processFileEvents()
    processTimeEvents()

20190606185109.png

5.客户端

Redis服务器是一个典型的一对多服务器程序:一个服务器可以与多个客户端建立网络连接。Redis服务器使用单线程单进程的方式处理命令请求。服务器为每个客户端建立了相应的RedisClient结构。

5.1客户端属性

typedef struct redisClient {

    // 套接字描述符
    int fd;

    // 当前正在使用的数据库
    redisDb *db;

    // 当前正在使用的数据库的 id (号码)
    int dictid;

    // 客户端的名字
    robj *name;             /* As set by CLIENT SETNAME */

    // 查询缓冲区
    sds querybuf;

    // 查询缓冲区长度峰值
    size_t querybuf_peak;   /* Recent (100ms or more) peak of querybuf size */

    // 参数数量
    int argc;

    // 参数对象数组
    robj **argv;

    // 记录被客户端执行的命令
    struct redisCommand *cmd, *lastcmd;

    // 请求的类型:内联命令还是多条命令
    int reqtype;

    // 剩余未读取的命令内容数量
    int multibulklen;       /* number of multi bulk arguments left to read */

    // 命令内容的长度
    long bulklen;           /* length of bulk argument in multi bulk request */

    // 回复链表
    list *reply;

    // 回复链表中对象的总大小
    unsigned long reply_bytes; /* Tot bytes of objects in reply list */

    // 已发送字节,处理 short write 用
    int sentlen;            /* Amount of bytes already sent in the current
                               buffer or object being sent. */

    // 创建客户端的时间
    time_t ctime;           /* Client creation time */

    // 客户端最后一次和服务器互动的时间
    time_t lastinteraction; /* time of the last interaction, used for timeout */

    // 客户端的输出缓冲区超过软性限制的时间
    time_t obuf_soft_limit_reached_time;

    // 客户端状态标志
    int flags;              /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */

    // 当 server.requirepass 不为 NULL 时
    // 代表认证的状态
    // 0 代表未认证, 1 代表已认证
    int authenticated;      /* when requirepass is non-NULL */

    // 复制状态
    int replstate;          /* replication state if this is a slave */
    // 用于保存主服务器传来的 RDB 文件的文件描述符
    int repldbfd;           /* replication DB file descriptor */

    // 读取主服务器传来的 RDB 文件的偏移量
    off_t repldboff;        /* replication DB file offset */
    // 主服务器传来的 RDB 文件的大小
    off_t repldbsize;       /* replication DB file size */
    
    sds replpreamble;       /* replication DB preamble. */

    // 主服务器的复制偏移量
    long long reploff;      /* replication offset if this is our master */
    // 从服务器最后一次发送 REPLCONF ACK 时的偏移量
    long long repl_ack_off; /* replication ack offset, if this is a slave */
    // 从服务器最后一次发送 REPLCONF ACK 的时间
    long long repl_ack_time;/* replication ack time, if this is a slave */
    // 主服务器的 master run ID
    // 保存在客户端,用于执行部分重同步
    char replrunid[REDIS_RUN_ID_SIZE+1]; /* master run id if this is a master */
    // 从服务器的监听端口号
    int slave_listening_port; /* As configured with: SLAVECONF listening-port */

    // 事务状态
    multiState mstate;      /* MULTI/EXEC state */

    // 阻塞类型
    int btype;              /* Type of blocking op if REDIS_BLOCKED. */
    // 阻塞状态
    blockingState bpop;     /* blocking state */

    // 最后被写入的全局复制偏移量
    long long woff;         /* Last write global replication offset. */

    // 被监视的键
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */

    // 这个字典记录了客户端所有订阅的频道
    // 键为频道名字,值为 NULL
    // 也即是,一个频道的集合
    dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */

    // 链表,包含多个 pubsubPattern 结构
    // 记录了所有订阅频道的客户端的信息
    // 新 pubsubPattern 结构总是被添加到表尾
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */
    sds peerid;             /* Cached peer ID. */

    /* Response buffer */
    // 回复偏移量
    int bufpos;
    // 回复缓冲区
    char buf[REDIS_REPLY_CHUNK_BYTES];

} redisClient;

20190606191612.png

5.2客户端的创建与连接

5.2.1创建普通客户端

在客户端调用connect连接服务器时,服务器调用事件处理器,为客户端创建响应的客户端状态。

5.2.2关闭普通客户端

一个普通客户端可能由于多种原因被关闭:

  • 客户端退出或被杀死
  • 客户端向服务器端发送了不符合协议的请求,会被服务器端关闭
  • 客户端成立client kill的目标
  • 服务器端设置了timeout时间,当客户端空转时间超过timeout时
  • 发送的命令请求大小超过了输入缓冲区的限制大小
  • 回复的大小超过了输出缓冲区限制大小

5.2.3Lua脚本的伪客户端

服务器在初始化时创建伪客户端,伪客户端会一直存在,只有服务器被关闭时,伪客户端才会被关闭。

5.2.4AOF的伪客户端

服务器在载入AOF时,会创建伪客户端,在载入完成时,会关闭这个伪客户端。

5.服务器

5.1命令执行的过程

  • 客户端将命令转换为协议格式向服务器发送
  • 读取命令请求
    • 读取套接字中的命令请求,保存到输入缓冲区中
    • 对缓冲区进行分析,提取argv,argc
    • 调用命令执行器,执行命令
  • 命令执行器
    • 查找命令实现,在命令表中查找参数指定的命令,并将命令保存到cmd属性中
    • 执行预备操作:进行一些检查操作,使得命令可以正常执行
    • 调用命令执行函数,通过cmd属性的proc
    • 执行后续操作:AOF,复制,慢查询日志等
  • 将命令回复发送给客户端

5.2serverCron函数

Redis中的serverCron函数默认每100ms执行一次,负责管理服务器资源,保持服务器良好运转。serverCron函数的操作:

  • 更新服务器时间缓存
  • 更新lru时钟(lruclock属性)
  • 更新服务器每秒执行命令次数
  • 更新内存峰值记录
  • 处理sigterm信号
  • 管理客户端资源(超时,缓冲区)
  • 管理服务器资源(删除过期键,定期删除)
  • 检查持久化操作的运行状态
    • 正在持久化
      • 有完成信号到达,执行替换操作,对于aof重写,还要把aof重写缓冲区的内容写入
      • 没有完成信号,不操作
    • 没有持久化
      • 看bgaofrewrite是否延迟
      • 自动保存条件是否满足
      • aof重写是否满足

20190606200737.png

5.3初始化服务器

  • 初始化服务器状态
  • 载入配置
  • 初始化数据结构
  • 还原数据库状态
  • 执行事件循环
发表评论