读书笔记-《Redis设计与实现》-第二部分:单机数据库的实现

第九章:数据库

struct redisServer {

    redisDb *db; // 一个数组,保存着服务器中的所有数据库

    int dbnum; // 服务器的数据库数量,默认是16

    struct saveparam *saveparams; // 记录了保存条件的数组 它包括time_t seconds int changes秒数和修改次数

    long long dirty; // 修改计数器

    time_t lastsave; // 上一次保存的时间

    sds aof_buf; // AOF缓冲区

    ...

}

typedef struct redisClient {

    redisDb *db; // 客户端正在使用的数据库

    ...

}

typedef struct redisDb {

    dict *dict; // 数据库键空间,保存着数据库中的所有键值对 key永远是字符串对象,value可以是五大类型

    dict *expires // 过期字典,保存着键的过期时间;key为指向上面那个dict的key,value为过期时间的毫秒时间戳

    ...

}

设置过期时间:

EXPIRE 设定键的生存时间为x秒
PEXPIRE 设定键的生存时间为x毫秒
EXPIREAT 设定键过期时间为秒数时间戳
PRXPIREAT 设定键过期时间为毫秒时间戳(底层最终都转为了它)

过期键删除策略:

定时删除 设置键过期时间时,创建一个定时器,过期时立即执行删除。内存友好,CPU不友好。此外,定时器需要使用Redis的时间事件,其实现方式是无序链表,查询为O(N)——无法高效应对大量时间事件。
惰性删除

每次获取键时,检查是否过期,过期则删除。CPU友好,内存不友好,甚至可看作内存泄露,Redis是非常吃内存的。

db.c/expireIfNeeded

定期删除

每隔一段时间,进行检查,删除过期的。兼顾前两者的优点。

redis.c/activeExpireCycle 默认每次检查16个数据库,每个数据库随机检查20个键,并限时2.5秒,记录当前检查进度。(联想:这里超时则退出,通过检查进度下次继续,保证了删除的时间上限。在开发中同样可以使用这个技巧,对于一些实时性要求较高的接口,设定一个处理时间的上限,一旦超过则返回兜底数据。)

过期键对RDB、AOF都不会造成影响。值得注意的是RDB的从服务模式时,会载入过期键,但是进行主从同步的时候,会清空从数据库。

(联想:HashMap现在要提供一个put(key, value, timestap)的方法,到了时间则删除,应该怎么实现?)

这是我曾经遇到过的面试题,当时也是奔着定时删除去了,一心纠结于用什么来实现定时器。现在想想太蠢了,如果每个键都加个定时器,无论用什么定时器,都非常的耗性能(时间+空间,具体当然要看定时器的实现)。这里就完全可以参考Redis对过期键的处理。首先加入惰性删除,每次访问时,如果发现已经过期则返回空,并删除这个结点。然后一样的,为了防止内存泄漏,还要有定期删除,可以GC回收垃圾时随机检查(例如每次检查10个存活的HashMap,每个HashMap选10个键),检查的逻辑其实就是惰性删除的逻辑,访问一下就好。

第十章:RDB持久化(Redis Database)

可手动执行或服务器根据配置定期执行。

生成RDB文件命令:SAVE(阻塞服务器进程)、BGSAVE(派生出一个子进程,此时Redis会尽量减少别的内存开销)

载入RDB文件:优先使用AOF文件进行还原数据库状态,如果未开启AOF持久化才使用RDB文件载入,启动时自动载入,阻塞。

SAVE、BGSAVE、BGREWRITEAOF命令不能同时进行。

RDB文件结构:这里RDB保存的二进制文件,有许多地方都需要开始或结尾标识符,大写的单词为常量,不同类型的value会有一些压缩算法。

REDIS db_version SELECTDB 0 pairs SELECTDB 3 pairs EOF check_sum

pairs具体结构:没有过期时间的键则没有EXPIRETIME_MS和ms。

EXPIRETIME_MS ms TYPE key value

第十一章:AOF持久化(Append Only File)

步骤:命令追加、文件写入、文件同步。

Redis服务器进程就是一个事件循环,循环中文件事件负责接收客户端命令请求,以及向客户端发送命令回复,而时间事件则像serverCron这样的函数定时运行。

def eventLoop():
    while True:
    processFileEvents() // 处理文件事件,接收命令、发送命令;可能有新内容加到aof_buf缓冲区
    processTimeEvents() // 处理时间事件
    flushAppendOnlyFile() // 考虑是否要将aof_buf中的内容写入到AOF文件中
appendfsync flushAppendOnlyFile逻辑 性能
always 写入并同步到AOF文件 循环效率最低,故障丢失一个循环的数据
everysec 默认 写入,如果1秒内没有同步,则同步;由一个线程完成 循环效率中等,故障丢失一秒的数据
no 写入,不同步;下次同步时间由操作系统决定; 循环效率最高,故障丢失数据最多,并且单次同步AOF文件时间最长

AOF重写:从数据库中读取键当前的值,然后用一条命令去代替所有对这个键操作的命令。

AOF重写带来大量的写入操作,所以调用的线程将长时间阻塞,又因为Redis是单线程,所以这里AOF重写被放入了子进程。当重写工作完成后,向父进程发送信号,父进程收到后调用信号处理函数,将AOF重写缓冲区的内容写入新AOF文件,并代替旧的AOF文件。

(联想:如何看待RDB和AOF?哪种效率更高?有什么区别?为什么提供两种持久化的机制?)

相同点:BGSAVE和BGREWRITEAOF都需要创建子进程,由于大多数操作系统都采用写时复制技术来优化子进程的使用效率,所以在此期间要尽量减少内存的开销。

  持久化频率 持久化方式 适合场景
RDB 手动阻塞调用 / 指定时间内达到指定修改次数 把某个时间点的数据库状态保存成二进制文件 适合备份
AOF 默认1秒1次 优化成指令,保存指令 适合重启后恢复数据

第十二章:事件

文件事件:服务器对套接字操作的抽象。

时间事件:服务器对定时操作的抽象。包括定时事件、周期性事件。属性为id、when、timeProc,timeProc是一个函数,当时间事件到达时,调用相应的函数来处理事件。存放在一个无序链表中。正常情况下,只有serverCron一个周期性时间事件。

文件事件处理器:

套接字 ->

队列,保证有序、同步、每次一个

IO多路复用程序 ->

包装常用库来实现,底层实现可更换

文件事件分派器 ->

Redis基于Reactor开发

事件处理器

客户端与服务器通信过程:

1.客户端向服务器发送连接请求,服务器执行连接应答处理器

2.客户端向服务器发送命令请求,服务器执行命令请求处理器

3.服务器向客户端发送命令回复,服务器执行命令回复处理器

事件处理角度下的服务器运行流程:

等待文件事件的产生 -> 处理已产生的文件时间 -> 处理已到达的时间事件(服务器轮流处理文件事件与时间事件,不会抢占;时间事件的实际处理时间通常比设定的晚)

第十三章:客户端

struct redisServer {

    list *clients; // 链表,保存所有客户端的状态

    time_t unixtime; // 秒级时间缓存,用于精度要求不高的地方

    long long mstime; // 毫秒级时间缓存,用于精度要求不高的地方

    unsigned lruclock:22; // 时间缓存的一种,和redisObject上的一起计算对象的空转时间

    size_t stat_peak_memory; // 已使用的内存峰值

    int shutdown_asap; // 关闭服务器的标识,1关闭,0不关闭

    int aof_rewrite_scheduled; // 1代表有AOF重写命令被延迟了

    pid_t rdb_child_pid; // BGSAVE子进程的ID,没有为-1

    pid_t aof_child_pid; // BGREWRITEAOF子进程的ID,没有为-1

    int cronloops; // serverCron函数执行次数

    ...

}

typedef struct redisClient {

    int fd; // 客户端类型,-1为伪客户端如AOF文件或Lua脚本

    robj *name; // 名字

    int flags; // 标志,记录了客户端的角色以及状态 例如主从、阻塞、安全、版本

    sds querybuf; // 输入缓冲区,不能超过1GB,否则会关闭这个客户端

    robj **argv; // 命令参数,例如 SET KEY VALUE,根据argv[0]的值在命令Hash表中查找对于的实现函数

    int argc; // 命令参数的个数,例如3

    struct redisCommand *cmd; // 命令的实现函数

    char buf[REDIS_REPLY_CHUNK_BYTES]; // 固定缓冲区,用来回复简单的数字、字符串等等

    int bufpos; // 固定缓冲区的大小

    list *reply; // 非固定缓冲区

    int authenticated; // 身份验证,0未通过,1通过

    time_t ctime; // 客户端创建的时间

    time_t lastinteraction; // 与服务器最后一次互动的时间

    time_t obuf_soft_limit_reached_time; // 输出缓冲区第一次到达软性限制的时间,硬性限制是到达指定大小立即关闭就像输入缓冲区一样,软性则是根据时间和大小一起来判断,如果这段时间一直超过一个数值,则关闭客户端

    ...

}

(联想:这里的set指令与setCommand,都是可扩展的,如果是我们来设计,应该怎么设计,能否使用到设计模式?)

明确两点:1.指令和command都是可扩展的。   2.希望不修改调用的代码,即不用编写 if(命令=set) 这样的代码。

package util;

public interface Command {

    void execute();

}

package util;

public class SetCommand implements Command{

    @Override
    public void execute() {
        System.out.println("SetCommand execute...");
    }

}

package util;

public enum CommandType {

    SET("SET", "util.SetCommand");

    private String code;
    private String command;

    CommandType(String code, String command) {
        this.code = code;
        this.command = command;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getCommand() {
        return command;
    }

    public void setCommand(String command) {
        this.command = command;
    }

}

package util;

public class Test {

    public static void main(String[] args) throws Exception {
        String code = "SET";
        executeCommand(code);
    }

    public static void executeCommand(String code){
        Command command = null;
        try {
            for (CommandType type : CommandType.values()) {
                if (type.getCode().equals(code)) {
                    command = (Command) Class.forName(type.getCommand()).newInstance();
                }
            }
        } catch (Exception e) {
            System.out.println("execute command fail! check the commandType and the class name.");
        }
        if (null != command) {
            command.execute();
        }
    }

}

代码如上,这样的话Test.executeCommand都是不需要修改的,根据调用方main的参数不同,自动执行不同的command。每次新增了command只需要新增Command.java的实现以及CommandType.java的枚举。

第十四章:服务器

命令请求的执行过程:

1.客户端向服务器发送命令请求 SET KEY VALUE

转换成协议格式,连接到服务器的套接字,发送

2.服务器接收并处理请求,在数据库中设置,并产生命令回复OK

当连接套接字可读时,调用命令处理器。读取套接字中协议格式的命令请求,保存到输入缓冲区中,分析后保存到argv和argc中。查找命令的实现,执行预备操作,调用指令的实现函数,执行后续操作

3.服务器将命令回复OK发送给客户端

4.客户端接收服务器返回的OK,并将这个回复打印给用户

从协议格式转为可阅读格式

serverCron函数:

1.更新服务器时间缓存

2.更新LRU时钟

3.更新服务器每秒执行命令次数(最近一秒的,估算)

4.更新服务器内存峰值记录

5.处理SIGTERM信号

6.管理客户端资源(超时、缓冲区超长)

7.管理数据库资源(过期、字典收缩)

8.执行被延迟的BGREWRITEAOF

9.检查持久化操作的运行状态(依次判断有无延迟的AOF重写、是否可执行BGSAVE、是否可执行AOF重写)

10.将AOF缓冲区写入到AOF文件

11.关闭异步客户端

12.增加cronloops计数器的值(serverCron函数的执行次数)

初始化服务器:

1.初始化服务器状态结构(redis.c/initServerConfig 默认配置项)

2.载入配置选项(可通过启动参数或文件修改默认配置)

3.初始化服务器数据结构(客户端链表、数据库数组等)

4.还原数据库状态

5.执行事件循环

发布了25 篇原创文章 · 获赞 12 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_25498677/article/details/86499882
今日推荐