二、单机数据库的实现
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*/
};
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
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;
读写键空间时的维护操作
当使用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;
字段
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文件的创建于载入
创建:save
和bgsave
命令
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;
};
Redis的周期执行函数serverCron每隔100毫秒就会执行一次,该函数对正在运行的服务器进行维护,其中一项就是检查save的条件是否被满足。
2.3RDB文件结构
不同类型的键值对,Redis使用不同的方式保存。
2.4分析RDB文件
使用od
命令打印rdb文件: od -c dump.rdb
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文件。
4.事件
Redis是一个事件驱动程序,需要处理两类事件:
- 文件事件:Redis服务器通过套接字与客户端连接,文件事件就是对套接字操作的抽象。
- 时间事件:Redis中的一些操作(如serverCron)需要在给定的时间点执行,时间事件是对这类定时操作的抽象。
4.1文件事件
Redis基于Reactor模式开发了自己的网络事件处理器(被称为文件事件处理器):
- 文件事件处理器使用I/O多路复用模式同时监听多个套接字。
- 当被监听的套接字准备好accept,read,write,close等操作时,与之关联的文件事件就会产生。
尽管多个文件事件可能并发的出现,但I/O多路复用程序会将产生事件的套接字都放在一个队列里面,通过这个队列,以有序,同步的方式,每次一个套接字的方式向文件事件分派器传送套接字。 Redis的I/O多路复用程序是通过包装select,epoll,evport等实现的。
4.2时间事件
Redis的时间事件分为两类:
- 定时事件:在指定的时间点执行一次
- 周期事件:每隔一段时间执行一次
一个时间事件有三部分组成:
- id:事件的全局唯一ID
- when:毫秒精度的时间戳,记录了事件的到达时间
- timeProc:时间事件处理器
应用实例:serverCron函数(可以通过hz选项调整serverCron每秒执行次数)
时间事件的执行比预定的到达时间晚一点。
4.2.1实现
服务器将所有事件放在一个无序链表中,每当时间事件执行器运行时,就遍历链表查找已到达的事件,调用timeProc
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()
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;
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重写是否满足
- 正在持久化
5.3初始化服务器
- 初始化服务器状态
- 载入配置
- 初始化数据结构
- 还原数据库状态
- 执行事件循环