04 数据库
数据库
1 数据库的实现
服务器数据库
Redis服务器负责与多个客户端建立连接,处理客户端的命令请求,在数据库中保存命令产生的数据,并通过资源管理来维持服务器自身的运转。
Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库:
1 | struct redisServer { |
- 其中dbnum的值有服务器配置的database选项决定,默认为16。
redisServer结构保存了一个clients链表,保存了所有连接的客户端的状态信息。
客户端数据库
默认情况下,Redis客户端的目标数据库是0号数据库,客户端可以执行SELECT命令来切换。
服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的当前信息,以及执行相关功能需要的数据结构:
- 客户端的套接字描述符
- 客户端的名字
- 客户端的标志值(flag)
- 客户端正在使用的数据库的指针及号码
- 客户端当前要执行的命令、参数
- 客户端的输入输出缓冲区
- 客户端的复制状态信息
- 客户端的事务状态
- 客户端执行发布与订阅功能用到的数据结构
- 客户端的身份验证标识
- 客户端的统计信息,如创建时间、最后一次通行时间、缓冲区大小超出限制的时间
1 | typedef struct redisClient { |
创建客户端
客户端使用connect函数连接到服务器,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并添加到链表的末尾。
关闭客户端
- 客户端进程被杀死
- 发送的协议不符合格式
- 客户端成了
CLIENT KILL命令的目标 - 服务器配置了timeout选项,客户端空转被断开
- 超出输入/输出缓冲区限制。输出缓冲区的限制包括:硬性限制、弱性限制。超过软性限制一段时间,客户端也会被关闭。
数据库键空间
Redis是一个键值对(key-value pair)数据库服务器。redisDb结构的dict字典保存了数据库的所有键值对,这个字典就是键空间:
1 | typedef struct redisDb { |
键空间和用户所见的数据库是直接对应的:
- 键空间的键也就是数据库的键。每个键都是一个字符串对象。
- 键空间的值也是数据库的值。每个值可以使字符串对象、列表对象、哈希表对象、集合对象、有序集合对象。

所有针对数据库的操作,实际上都是通过键空间字典来实现。
2 数据库的操作
数据库基本操作
增删查改
- 添加新键。添加一个新键值对到数据库,就是将新键值对添加到键空间字典中。
- 删除键。删除数据库中的一个键,就是在键空间中删除键所对应的键值对对象。
- 更新键。更新数据库的一个键,就是对键空间里键所对应的值对象进行更新。根据值对象类型的不同,更新的具体方法也不同。
- 对键取值。对一个数据库键取值,就是在键空间中取出键所对应的值对象。
读写键空间时的维护操作
当Redis对数据库读写时,不仅对键空间执行指定的操作,还会执行一些额外的维护:
- 读取一个键后,更新服务器的键命中次数或不命中次数。这两个值可通过
INFO stats命令查看。 - 读取一个键后,更新LRU时间。
OBJECT idletime <key>查看。 - 读取键时发现已过期,删除。
- 如果有客户端
WATCH了某个键,修改后将键标记为dirty,从而让事物程序注意到它。 - 每次修改一个键后,将dirty键计数器的值+1,这个计数器会触发服务器的持久化和赋值操作。
- 如果服务器开启了通知功能,键修改后,服务器会按照配置发送通知。
3 服务器初始化过程
初始化服务器状态结构
初始化服务器的第一步就是创建一个redisServer类型的实例变量server,并为结构中的各个属性设置默认值。这个工作由redis.c/initServerConfig函数完成:
- 设置服务器运行id
- 为id加上结尾字符
- 设置默认的配置文件路径
- 设置默认服务器频率
- 设置服务器的运行架构,64位 or 32位
- 设置服务器的默认端口
- 设置服务器的默认RDB和AOF条件
- 初始化服务器的LRU时钟
- 创建命令表
载入配置选项
启动服务器时,用户可以通过配置参数或者配置文件来修改服务器的默认配置。
redis.c/initServerConfig函数初始化完server变量后,开始载入用户给定的配置。
初始化服务器数据结构
载入用户的配置选项之后,才能正确地初始化数据结构,由initServer函数负责:
server.clients链表server.db数组server.pubsub_channels字典server.luaLua环境server.slowlog
除此之外,initServer还:
- 为服务器设置进程信号处理器
- 创建共享对象
- 打开服务器的监听端口,并为套接字关联应答事件处理器
- 为
serverCron函数创建时间事件 - 打开或创建的AOF文件
- 初始化后台I/O模块
还原数据库状态
初始化完server后,服务器要载入RDB或AOF文件,还原数据库状态
执行事件循环
开始执行服务器的loop。
4 命令请求的执行过程
SET KEY VALUE命令的执行过程:
- 客户端向服务器发送命令请求
SET KEY VALUE。 - 服务器接收并处理命令请求,在数据库中设置操作,并产生命令回复
OK。 - 服务器将
OK发送给客户端。 - 客户端接收服务器返回的命令
OK,并打印给用户。
发送命令请求
用户:键入命令请求
客户端:将命令请求转换为协议格式然后发送给服务器
读取命令请求
当连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器执行以下操作:
- 读取套接字协议格式中的命令请求,并将其保存在客户端状态的输入缓冲区里。
- 对输入缓冲区的命令请求进行分析,提取命令参数及其个数,保存到客户端状态的argv和argc属性。
- 调用命令执行器,执行指定的命令。
命令执行器(1):查找命令实现
命令执行器要做的第一件事是根据客户端状态的argv[0]参数,在命令表(command table)中查找参数指定的命令,并将其保存到客户端状态的cmd属性里。
命令表是一个字典,键是命令名字,值是一个redisCommand结构。命令表使用的是大小写无关的查找算法。
命令执行器(2):执行预备操作
有了执行命令所需的命令实现函数、参数、参数个数,但程序还需要一些预备操作:
- 检查客户端状态的
cmd指针是否为NULL。 - 根据
cmd属性指向redisCommand结构的arity属性,检查命令请求的参数个数是否正确。 - 检查客户端是否通过了身份验证,未通过必须使用
AUTH命令。 - 如果服务器打开了
maxmemory功能,检查内存占用情况,有需要时进行内存回收。 - 如果上一次
BGSAVE出错,且服务器打开了stop-writes-on-bgsave-error功能,且服务器要执行一个写命令,拒绝执行。 - 如果客户端正在用
SUBSCRIBE订阅频道,服务器只会执行订阅相关的命令。 - 如果服务器正在进行输入载入,那么客户端发送的命令必须带有1标识才能被执行。
- 如果服务器因为Lua脚本而超时阻塞,那么服务器只会执行客户端发来的
SHUTDOWN nosave和SCRIPT KILL命令。 - 如果客户端正在执行事务,那么服务器只会执行客户端发来的
EXEC、DISCARD、MULTI、WATCH命令,其余命令进入事务队列。 - 如果服务器打开监视器功能,要将执行的命令和参数等信息发给监视器,其后才真正执行命令。
命令执行器(3):调用命令的实现函数
client->cmd->proc(client);
相当于执行语句:
sendCommand(client);
命令回复会保存在输出缓冲区,之后实现函数还会为套接字关联命令回复处理器,将回复返回给客户端。
命令执行器(4):执行后续工作
- 如果开启了慢查询,添加新的日志。
redisCommand结构的calls计数器+1。- 写入AOF缓冲区。
- 同步从服务器。
将命令回复发送给客户端
当客户端套接字变为可写时,服务器将输出缓冲区的命令发送给客户端。发送完毕后,清空输出缓冲区。
客户端接收并打印命令回复
服务器:回复处理器将协议格式的命令返回给客户端。
客户端:将回复格式化成人类可读的格式,打印。
4 serverCron函数执行过程
更新服务器时间缓存
每次获取系统的当前时间都要执行一次系统调用,为了减少系统调用,服务器状态中保存了当前时间的缓存:
1 | struct redisServer { |
serverCron默认会100毫秒更新一次这两个属性,所以它们的精确度并不高。对于一些高精度要求的操作,还是会再次执行系统调用。
更新LRU时钟
1 | struct redisServer { |
更新服务器每秒执行命令次数
serverCron函数中的trackOperationPerSecond函数以每100毫秒一次的频率执行,该函数以抽样计算的方式,估算并记录服务器在最近一秒内处理的命令请求数量,这个值可以用过INFO status命令查看。
1 | struct redisServer { |
客户端执行INFO命令,服务器会调用getOperationsPerSecond函数,根据ops_sec_samples中的抽样结果,计算出instantaneous_ops_per_sec属性的值。
更新服务器内存峰值记录
1 | struct redisServer { |
每次serverCron执行,程序都会查看当前的内存数量,更新stat_peak_memory。INFO memory可查看。
处理SIGTERM信号
启动时,Redis会为服务器进程的SIGTERM信号关联处理器sigtermHandler函数。它在接到该信号后,打开服务器状态的shutdown_asap标识。每次serverCron执行,程序都会检查该标识,并决定是否关闭服务器。
1 | struct redisServer { |
管理客户端资源
serverCron每次都会调用clientsCron函数,后者会对一定数量的客户端作如下检查:
- 连接是否超时
- 输入缓冲区是否超过长度,如果是,新建缓冲区
管理数据库资源
serverCron每次都会调用databasesCron函数,检查一部分的数据库,删除过期键,对字典进行收缩等。
执行被延迟的BGREWRITEAOF
服务器执行BGSAVE期间,会阻塞BGREWRITEAOF命令。
1 | struct redisServer { |
检查持久化操作的运行状态
1 | struct redisServer { |
serverCron执行时,只要两个属性有一个为-1,则执行wait3函数,检查是否有信号发来服务器进程:
- 如果有信号达到,表明新的RDB文件生成完毕,或AOF文件重写完毕,服务器需要执行相应命令的后续操作
- 没有信号就不做操作
如果两个属性都不为-1,表明服务器没有再做持久化操作,则:

serverCron的其他操作:
将AOF缓冲区的内容写入AOF文件
关闭异步客户端(超出输入缓冲区限制)
增加cronloops计数器(它的唯一作用就是复制模块中实现『每执行
serverCron函数N次就执行一次指定代码』的功能”)
5 服务器客户端通知过程
数据库通知是Redis 2.8新增加的功能,让客户端通过订阅可给定的频道或模式,来获取数据库中键的变化,以及数据库命令的执行情况。
“某个键执行了什么命令”的通知成为「键空间通知」。“某个命令被什么键执行了”是「键时间通知」。服务器配置的notify-keyspace-events选项决定了服务器发送通知的类型。
发送通知的功能由notify.h/notifyKeyspaceEvent函数实现的:
1 | void notifyKeyspaceEvent(int type, char *event, int dbid); |
伪代码如下:
1 | def notifyKeyspaceEvent(type, event, key, bdid): |










