《Redis设计与实现》—— Redis底层原理与实现(中)

八、数据库

8.1 服务器中的数据库

​ Redis服务器将所有数据库状态保存在redis.h/redisServer结构的db数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb代表一个数据库;

struct redisServer {
    // ...
    // 一个数组,保存着服务器中的所有数据库
    redisDb *db;
    
    //服务器数据库数量,初始化时决定创建多少个数据库
    int dbnum;
}

在这里插入图片描述

客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,是指向一个redisDb结构的指针。

typedef struct redisClient {
    //记录客户端当前正在使用的数据库,可以切换
    redisDb *db;
}

在这里插入图片描述

8.2 数据库键空间

redisDb的dict字典保存了数据库中的所有键值对,我们称该字典为键空间;

typedef struct redisDb {
    //键空间,保存所有键值对
    dict *dict;
}redisDb;

在这里插入图片描述

8.3 键的生存时间或过期时间

1、键过期时间保存

typedef struct redisDb {
    //过期字典,保存键的过期时间
    dict *expires;
}redisDb;

在这里插入图片描述

设置过期时间:

  • EXPIRE
  • PEXPIRE
  • EXPIREAT
  • PEXPIREAT

移除过期时间:

PERSIST

8.4 过期键的删除策略

Redis采用惰性删除和定期删除两种策略相结合的方式;

1、惰性删除策略

程序在取键是才进行过期检查,若过期则删除;

在这里插入图片描述

2、定期删除策略

每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响

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

1、AOF

AOF文件写入:当过期键被惰性或定期删除后,程序会向AOF文件追加一条DEL命令来记录该键已被删除;

举个例子:客户端的GET message命令获取过期message键,则服务器会有三个动作:

  1. 从数据库删除message键;
  2. 追加一条DEL message命令到AOF文件;
  3. 向执行GET命令的客户端返回空回复;

AOF重写:程序会对数据库中的键进行检查,过期的键不会保存到重写的AOF文件中;

2、RDB

生成RDB文件:和AOF重写类似,已过期的键不会被保存到新创建的RDB文件中;

载入RDB文件:

  • 主服务器模式:过期键会被忽略;
  • 从服务器模式:不论是否过期,都会被载入数据库;

3、复制

  • 主服务器:删除一个键后会向所有从服务器发送DEL命令;
  • 从服务器:执行客户端命令时,即使键过期也不会删除,而是正常返回键值,只有接收到主服务器的DEL命令,才会删除该键;

第二部分 单机数据库的实现

九、RDB持久化

9.1 RDB文件的创建与载入

创建:

redis> SAVE  #等待直到RDB文件创建完毕
OK

redis> BGSAVE #派生子进程,由子进程创建RDB文件
Background saving started

载入过程:

在这里插入图片描述

9.2 自动间隔性保存

可以通过配置save选项,让服务器每隔一段时间就自动保存一次BGSAVE命令;

save 900 1 # 服务器在900秒内,对数据库进行了至少1次修改
save 300 10
save 60 10000

之后会设置服务器状态redisServer结构的saveparams属性:

struct redisServer {
	//记录了保存条件的数组
    struct saveparam *saveparams;
    
    //上次成功执行SAVE命令或BGSAVE命令后,服务器进行了多少次修改
    long long dirty;
    
    //上次执行保存的时间
    time_t lastsave;
}

struct saveparam {
    //秒数
    time_t seconds;
    //修改数
    int changes;
}

在这里插入图片描述

9.3 RDB文件结构

完整的RDB文件结构:

在这里插入图片描述

在这里插入图片描述

  • REDIS为固定字符串;
  • db_version为版本号;
  • databases包含0~多个数据库;
  • EOF标志文件正文结束;
  • check_sum保存校验和;通过前面四个字段计算得出,用于检查文件是否有出错或损坏;

databases部分

在这里插入图片描述

key_value_pairs部分

不带过期时间的键值对:

在这里插入图片描述

带过期时间的键值对:(键值对过期了才会带上过期时间)

在这里插入图片描述

TYPE:记录了value的类型,长度为1字节,值可以是以下常量其中之一
	REDIS_RDB_TYPE_STRING
	REDIS_RDB_TYPE_LIST
	REDIS_RDB_TYPE_SET
	REDIS_RDB_TYPE_ZSET
	REDIS_RDB_TYPE_HASH
	REDIS_RDB_TYPE_LIST_ZIPLIST
	REDIS_RDB_TYPE_SET_INTSET
	REDIS_RDB_TYPE_ZSET_ZIPLIST
	REDIS_RDB_TYPE_HASH_ZIPLIST

key:字符串对象,编码方式和REDIS_RDB_TYPE_STRING类型的value一样;

value:根据TYPE类型不同以及保存内容长度不同,value的结构和长度也会不同;

EXPIRETIME_MS:告知读入程序,接下来读入的是一个以ms为单位的过期时间;

ms:记录一个以ms为单位的UNIX时间戳

9.4 RDB文件分析

​ 使用od命令分析RDB文件,给定-c参数可以以ASCII编码方式打印输入文件,-x参数可以以十六进制方式打印输入文件

1、不包含任何键值对的RDB文件

创建一个数据库状态为空的RDB文件后以od命令打印RDB文件:

$ od -c dump.rdb

在这里插入图片描述

因为不包含任何数据库数据,所以RDB文件由下面四个部分组成:

  • 五字节的"REDIS"字符串;
  • 四字节的版本号:0006;
  • 一字节的EOF常量:337;
  • 八字节的校验和:334 … V

2、包含字符串键的RDB文件

redis> SET MSG "HELLO"
OK
redis> SAVE
OK

在这里插入图片描述

  • 五字节的"REDIS"字符串;
  • 四字节的版本号:0006;
  • 376 \0:切换到0号数据库(376为特殊值SELECTDB);
  • \0 003 M S G 005 HELLO:\0 为TYPE(不同的TYPE值不同),003代表MSG的长度,005为HELLO的长度
  • 一字节的EOF常量:337;
  • 八字节的校验和:207 … 343

3、带有过期时间的字符串键的RDB文件

redis> SETEX MSG 10086 "HELLO"
OK
redis> SAVE
OK

在这里插入图片描述

  • 五字节的"REDIS"字符串;
  • 四字节的版本号:0006;
  • 376 \0:切换到0号数据库(376为特殊值SELECTDB);
  • 374:代表特殊值EXPIRETIME_MS。
  • \ 2 …001 \0 \0:代表8字节长的过期时间
  • \0 003 M S G 005 HELLO:\0 为TYPE(不同的TYPE值不同),003代表MSG的长度,005为HELLO的长度
  • 一字节的EOF常量:337;
  • 八字节的校验和:212 … 306

十、AOF持久化

​ AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态。

在这里插入图片描述

10.1 持久化实现

AOF持久化功能可分为append、文件写入、文件同步三个步骤。

1、append

服务器执行完一个命令便会将命令追加到服务器状态的aof_buf缓冲区的末尾:

struct redisServer {
    // ...
    
    // AOF缓冲区
    sds aof_buf;
    
    // ...
};

2、AOF文件的写入与同步

Redis服务器进程是一个事件循环,负责接收客户端命令、客户端发送命令回复以及定时运行的函数,最后会调用flushAppendOnlyFile函数来看看是否将aof_buf缓冲区中的内容写入和保存到AOF文件,伪代码如下:

def eventLoop():
	while True:
	# 处理文件事件
	processFileEvents()
	# 处理时间事件
	processTimeEvents()
	# 考虑将aof_buf中的内容写入和保存到AOF文件中
	flushAppendOnlyFile()

flushAppendOnlyFile()由redis.conf中的appendsync配置值来决定。

appendsync的值flushAppendOnlyFile行为
always将aof_buf缓冲区中的所有内容写入并同步到AOF文件
everysec将aof_buf缓冲区中的所有内容写入AOF文件,如果上次同步事件距离现在超过1s,则再次进行同步,由子线程fork执行
no将aof_buf缓冲区中的所有内容写入AOF文件,但不对AOF文件进行同步,何时同步由操作系统决定。

10.2 AOF文件的载入与数据还原

在这里插入图片描述

10.3 AOF重写

​ 为避免文件体育越来越大,且还原速度会变慢,以及存在冗余的命令,比如一个List键分多次放入值,需要多条指令等。

1、AOF文件重写的实现

通过直接读取服务器当前的数据库状态来实现。

举个例子:

redis> SADD animals "Cat"
(integer) 1

redis> SADD animals "Dog" "Panda" "Tiger"
(integer) 3

redis> SREM animals "Cat"
(integer) 1

redis> SADD animals "Lion" "Cat"
(integer) 1

直接读取服务器的animals键的值,就可以用一条指令SADD animals “Dog” …

2、AOF后台重写

通过子进程来执行,不过为了解决子进程进行AOF重写期间,服务器进程还在处理其他命令,可能出现数据库状态不一致,所以设置了一个AOF重写缓冲区,其作用如图所示:

在这里插入图片描述

子进程执行期间,服务器执行客户端的命令,会将命令同时追加到AOF缓冲区和重写缓冲区,在子进程结束后便会通知服务器将重写缓冲区的内容写入新的AOF文件并替换旧文件。

十一、事件

​ 服务器事件分为:文件事件和时间事件

11.1 文件事件

文件事件处理器以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字。

文件事件处理器的四个组成部分:

在这里插入图片描述

I/O多路复用程序结构:

在这里插入图片描述

11.2 时间事件

Redis的时间事件分为两类:

  • 定时事件:一段程序在指定事件之后执行一次;
  • 周期性事件:一段程序每隔指定时间就执行一次;

时间时间主要有三个属性:

id:服务器为时间事件创建的唯一ID,从小到大顺序递增;

when:毫秒精度的UNIX时间戳,记录事件到达时间;

timeProc:时间事件处理器,一个函数;

1、实现

时间事件会放在一个无序链表中,每当时间事件执行器运行时,就会遍历整个链表,查到已到达的时间事件就调用相应的事件处理器。

在这里插入图片描述

id从大到小排序,新事件在表头,when是无序的。

2、事件的调度与执行

在这里插入图片描述

十二、客户端

​ 服务器为每隔客户端建立了redis.h/redisClient结构用于保存客户端当前状态信息。

struct redisServer {
    // ...
    // 一个链表,保存了所有客户端状态
    list *clients;
    // ...
};

在这里插入图片描述

如图所示为三个客户端连接的情况。

12.1 客户端属性

通用属性:

typedef struct redisClient {
    // ...
    //套接字描述符,fd = 1时是伪客户端,普通客户端 > -1
    int fd; 
    // 客户端名称,默认为NULL,可指向一个字符串对象
    robj *name;
    //客户端角色以及所处状态,REDIS_MASTER标志为主服务器,REDIS_SLAVE标志位从服务器....
    int flags;
    //输入缓冲区:保存客户端发送的命令
    sds querybuf;
    //数组,每一项位一个字符串对象,argv[0]为执行的命令,之后为命令参数
    robj **argv;
    //argc负责记录argv数组的长度
    int argc;
    //argv[0]对应的redisCommand
    struct redisCommand *cmd;
    //输出缓冲区(固定大小缓冲区)REDIS_REPLY_CHUNK_BYTES默认为16*1024
    char buf[REDIS_REPLY_CHUNK_BYTES];
    //记录buf目前已使用的字节数量
    int bufpos;
    //可变大小的输出缓冲区(当固定缓冲区用完或存不下时使用)
    list *reply;
    //身份验证(值为0表示未通过验证,1代表通过验证)
    int authenticated;
    //创建客户端时间,用来计算客户端与服务器已连接了多少秒
    time_t ctime;
    //记录了客户端与服务器最后一次进行互动的时间
    time_t lastinteraction;
    //记录了输出缓冲区第一次到达软性限制的时间
    time_t obuf_soft_limit_reached_time;
    
}redisClient;

在这里插入图片描述

在这里插入图片描述

redis> CLIENT list
addr=127.0.0.1:53428 fd=6 name=user age=1242 idle=12
# age记录了客户端与服务器连接了多少时间
# idle距离客户端与服务器最后一次进行互动已经过去多少秒

12.2 客户端的创建与关闭

1、创建普通客户端

​ 每有一个客户端连接,便会将新的客户端状态添加到服务器状态结构clients链表的末尾;

在这里插入图片描述

2、关闭普通客户端

​ 为避免客户端回复过大,占用过多的服务器资源,服务器会时刻检查客户端的输出缓冲区的大小,并在缓冲区的大小超出范围时,执行相应的限制操作。服务器做了两种限制:

硬性限制:输出缓冲区大小超过了硬性限制设置的大,则关闭客户端;

软性限制:若输出缓冲区超过了软性限制,但没超过硬性限制,则会时刻健康客户端,若持续时间超过服务器设定时间,则关闭;通过client-output-buffer-limit属性值来设置;

# 普通客户端的硬性限制和软性限制都为0,表示不限制客户端的输出缓冲区
client-output-buffer-limit normal 0 0 0
# 从服务器客户端的硬性限制设置为256MB,软性限制为64MB,软性限制时长为60s
client-output-buffer-limit slave 256mb 64mb 60
# 将执行发布与订阅功能的客户端的硬性限制设置为32MB,软性限制为8MB,软性限制时长为60s
client-output-buffer-limit pubsub 32mb 8mb 60

十三、服务器

13.1 命令请求执行过程

一个命令从发到到获得回复的过程包括下述操作:

  1. 客户端向服务器发送命令请求;
  2. 服务器接收并处理,在数据库中进行设置操作,并产生回复;
  3. 服务器将回复发送给客户端;
  4. 客户端接收服务器回复并打印;

1、客户端向服务器发送命令请求

在这里插入图片描述

举个例子:假设用户在客户端键入命令

SET KEY VALUE
客户端会将命令转换成协议:
*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n

2、读取命令请求

服务器调用命令请求处理器来执行以下操作:

  • 读取套接字中协议格式的命令并保存到客户端状态的输入缓冲区querybuf;
  • 对querybuf中的命令进行分析,提取命令参数以及参数个数,分别保存于argv属性和argc属性;
  • 调用命令执行器,执行客户端指定命令;

3、命令回复发送给客户端

​ 命令实现函数会将命令回复保存到客户端的输出缓冲区buf[REDIS_REPLY_CHUNK_BYTES]并为客户端套接字关联命令回复处理器,当客户端套接字变为可写状态时,服务器会执行命令回复处理器,将保存在客户端buf[REDIS_REPLY_CHUNK_BYTES]中的命令回复发送给客户端;

4、客户端接收并打印命令回复

在这里插入图片描述

13.2 命令执行器

命令执行器是13.1中提到的服务器用于调用客户端指定命令,其执行步骤如下:

1、查找命令实现

​ 命令执行器由argv[0]参数在命令表中找参数指定的命令并将找到的命令保存到客户端的cmd属性;

在这里插入图片描述

在这里插入图片描述

2、执行预备操作

  • 检查cmd是否指向NULL,是则不再执行并返回一个错误;
  • 由cmd指向的redisCommand结构的arity属性,检查给定的参数个数是否正确,不对则不再执行并返回一个错误。例如值为-3代表参数个数必须>=3;
  • 检查客户端是否已通过身份验证;
  • 如果客户端正在执行事务,则服务器只会执行客户端发来的EXEC、DISCARD、MULTI、WATCH四个命令,其他命令放进事务队列;

3、调用命令的实现函数

就是执行setCommand(client)语句执行指定的操作;

4、执行后续工作

例如如果服务器开启了AOF持久化功能,那么AOF持久化模块会将刚执行的命令请求写入AOF缓冲区等等;

13.3 serverCron函数

​ serverCron函数默认每隔100ms执行一次,负责管理服务器资源保持服务器的良好运行;

  • 更新服务器时间缓存;
  • 更新LRU时钟;
  • 更新服务器每秒执行命令次数;
  • 更新服务器内存峰值记录;
  • 处理SIGTERM信号;(用于关闭服务器)
  • 管理客户端资源(连接超时、输入缓冲超过限定);
  • 管理数据库资源(删除过期键等,具体可见第八节);
  • 执行被延迟的BGREWRITEAOF;
  • 检查持久化操作的运行状态;
  • 将AOF缓冲区的内容写入AOF文件;
  • 关闭异步客户端;(关闭输出缓冲区超过限制的客户端)
  • 增加cronloops计数器的值;

13.4 初始化服务器

1、初始化服务器状态结构

调用initServerConfig函数,主要工作:

void initServerConfig(void) {
    //设置服务器运行ID;
	getRandomHexChars(server.runid, REDIS_RUN_ID_SIZE);
	
	//为运行id加上结尾字符
	server.runid[REDIS_RUN_ID_SIZE] = '\0';
	
	//设置服务器默认运行频率;
	server.hz = REDIS_DEFAULT_HZ;
	
	//设置服务器默认配置文件路径;
	server.configfile = NULL;
	
	//设置服务器的运行架构
	server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
	
	//设置默认服务器端口号
	server.port = REDIS_SERVERPORT;
	
	//设置服务器默认的RDB、AOF持久化条件
	//初始化服务器的LRU时钟
} 

2、载入配置选项

举例:

# 终端输入
$redis-server --port 10086
# 在配置文件修改配置,在redis.conf中修改
database 32
# 关闭RDB文件的压缩功能
rdbcompression no

3、初始化服务器数据库结构

比如:

  • server.clients链表;
  • server.db数组;
  • ...

4、还原数据库状态

服务器载入RDB护着AOF文件并根据文件记录的内容还原服务器的数据库状态;

5、执行事件循环

开始执行服务器事件循环(loop)至此服务器初始化工作完成。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值