注明:《Redis设计与实现》的个人学习总结,这本书对redis的讲解清晰易懂,如果深入学习可以看看这本书。
目录
第11章 AOF持久化
- 下面的这三个命令都会保存到AOF
- AOF就是通过重新执行命令操作恢复数据库。
- AOF所有命令都是redis命令的请求协议格式保存。
redis> SET msg "hello"
OK
redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> RPUSH numbers 128 256 512
(integer) 3
11.1 AOF持久化的实现
分为三个步骤
- append命令追加
- 文件写入
- 文件同步
11.1.1 命令追加
- 执行完写命令后会把命令写入到aof_buf缓冲区末尾
struct redisServer {
// ...
// AOF
缓冲区
sds aof_buf;
// ...
};
11.1.2 AOF文件的写入与同步
- redis服务器进程就是事件循环。每个事件可能是接收客户端请求,以及回复。
- 所以执行写命令的时候可以调用flushAppendOnlyFile函数。考虑是不是要把aof_buf缓冲区写入。和保存到AOF文件里面。
def eventLoop():
while True:
#
处理文件事件,接收命令请求以及发送命令回复
#
处理命令请求时可能会有新内容被追加到 aof_buf
缓冲区中
processFileEvents()
#
处理时间事件
processTimeEvents()
#
考虑是否要将 aof_buf
中的内容写入和保存到 AOF
文件里面
flushAppendOnlyFile()
- appendfsync选项决定了flushAppendOnlyFile的行为。默认是everysec,必须是距离上一次同步超过1s才能够写入。这次写入只是写入到操作系统的缓存区。然后再同步到磁盘
- appendfync参数介绍
- always:每个事件都要刷新aof缓存写入aof文件,并且同步。它比较慢,但是如果发生故障丢失的命令数据很少
- everysec:每隔一秒就要同步一次,这种最多只会丢失1s的数据。
- no:宕机的话所有数据丢失,但是写入aof文件速度最快。同步由操作系统决定。
11.2 AOF文件的载入与数据还原
- 载入只需要重新执行AOF的所有命令。
还原步骤
- 创建一个不带网络连接的伪客户端,执行AOF的保存的写命令。
- 从aof文件分析读取写命令。
- 使用伪客户端执行被读出的写命令。
- 然后一直重复2和3直到所有写命令被使用完。
- 服务器先读入select 0,然后就是set,sadd等命令执行。还原状态。
*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n
*3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$5\r\nhello\r\n
*5\r\n$4\r\nSADD\r\n$6\r\nfruits\r\n$5\r\napple\r\n$6\r\nbanana\r\n$6\r\ncherry\r\n
*5\r\n$5\r\nRPUSH\r\n$7\r\nnumbers\r\n$3\r\n128\r\n$3\r\n256\r\n$3\
11.3 AOF重写
- AOF持久化的原理就是执行保存的写命令来恢复数据库。
- 所以AOF会越来越大。因为命令不断在追加。
- 所以需要减少AOF的体积,这个时候就需要重写。
11.3.1 AOF文件重写的实现
- AOF重写通过读取数据库的状态而不是旧的AOF文件生成的。
- 假设执行下面的命令,AOF文件就要存入6个命令,重写AOF的话直接看数据库状态,只生成一条语句就可以完成了。
redis> RPUSH list "A" "B" // ["A", "B"]
(integer) 2
redis> RPUSH list "C" // ["A", "B", "C"]
(integer) 3
redis> RPUSH list "D" "E" // ["A", "B", "C", "D", "E"]
(integer) 5
redis> LPOP list // ["B", "C", "D", "E"]
"A"
redis> LPOP list // ["C", "D", "E"]
"B"
redis> RPUSH list "F" "G" // ["C", "D", "E", "F", "G"]
(integer) 5
- 对于下面的命令可以综合为一条SADD animals"Dog"“Panda”“Tiger”“Lion”“Cat”
redis> SADD animals "Cat"
// {
"Cat"}
(integer) 1
redis> SADD animals "Dog" "Panda" "Tiger" // {
"Cat", "Dog", "Panda", "Tiger"}
(integer) 3
redis> SREM animals "Cat" // {
"Dog", "Panda", "Tiger"}
(integer) 1
redis> SADD animals "Lion" "Cat" // {
"Dog", "Panda", "Tiger",
(integer) 2 "Lion", "Cat"}
- 整个重写过程
- 遍历数据库
- 遍历数据库的所有的键值对,然后重写,忽略过期的键。
- 关闭文件
- 对于下面的数据库,重写之后的所有命令
SELECT 0
RPUSH alphabet "a" "b" "c"
EXPIREAT alphabet 1385877600000
HMSET book "name" "Redisin Action"
"author" "Josiah L. Carlson"
"publisher" "Manning"
EXPIREAT book 1388556000000
SET message "hello world"
-
客户端缓冲区如果不够用,就需要通过多条命令来写。
11.3.2 AOF后台重写
- aof_rewrite函数完成了aof重写
- 但是重写aof的时候要保证服务器端被阻塞,所以可能就是重写线程由于服务器写入操作太多,一直被阻塞。
- 所以要把aof重写让子进程进行处理。
- 服务器进程可以处理命令
- 子进程带有服务器副本,所以可以避免争夺锁的情况。因为线程需要争夺数据的锁才能够进行修改。
- 但是子进程异步执行,主进程仍然可以接收请求修改数据库。所以可能导致aof和当前数据库的状态不一致。
- 所以可以通过一个缓冲区记录在子进程创建执行重写的时候主进程执行的写命令。
整个重写过程
- 执行写命令
- 写命令送到aof缓冲区
- 写命令送到aof重写缓冲区。
- 保证aof缓冲区定期被写入到文件
- 创建子进程开始,服务器就会把写命令写入到重写aof缓冲区。
结束重写aof文件之后
- aof重写缓冲区写入到新的aof文件中
- 新的aof文件改名。覆盖旧的文件。
11.4 重点回顾
- aof文件通过修改数据库写命令请求数据库的状态。
- aof文件的命令是按照redis的命令请求协议格式保存
- 写命令先写到缓冲区,再同步到文件。
- appendfsync决定同步的策略
- aof可以重写来压缩aof的大小
- 执行重写的时候会创建子进程来进行处理,原因就是子进程可以不与主进程争夺锁,关键就是做一个数据库的副本,并且通过重写缓冲区解决不一致问题。
第12章 事件
- redis服务器是事件驱动程序。处理两类事件
- 文件事件:文件事件就是服务器对套接字操作的抽象。服务器通过套接字来和客户端连接,客户端和服务器端通信就会造成文件事件,服务器监听事件并且完成一系列网络操作。
- 时间事件:定时操作的事件
12.1 文件事件
- redis基于reactor模式开发了自己的网络事件处理器这个就是文件事件处理器
- 文件事件处理器使用IO多路复用同时监视多个套接字。并且能够根据套接字内容连接不同的事件处理器。
- 被监听的套接字执行连接应答、读取、写入、关闭。文件事件就会产生,文件事件管理器就会调用套接字关联的事件处理器来处理任务
- 虽然文件事件处理器是单线程,但是IO多路复用能够监听多个套接字,让它拥有高性能处理请求能力。
12.1.1 文件事件处理器的构成
四个组成部分
- 套接字
- IO多路复用程序
- 文件事件分派器
- 事件处理器
- 文件事件就是对套接字操作的抽象,套接字执行连接应答,写入,读取,关闭等这些操作都可以是文件事件而且文件事件可以并发生成,因为有多个套接字。
- 多个文件事件,但是IO多路复用把所有产生事件的套接字放到队列,然后逐个交给文件事件分派器分派给不同的事件管理器。当处理完一个套接字之后才能够传下一个套接字。
12.1.2 I/O多路复用程序的实现
- IO多路复用在redis功能通过包装select、 epoll、evport和kqueue这些IO多路复用函数库实现的。所以在redis源码中IO多路复用有自己的文件ae_select.c、ae_epoll.c、ae_kqueue.c
12.1.3 事件的类型
- IO多路复用可以监听多个套接字的ae.h/AE_READABLE事件 和ae.h/AE_WRITABLE事件
- 当套接字可读(客户端对套接字执行write),有新的应答套接字出现(客户端对服务器端的监听套接字执行connect),这个时候就有AE_READABLE事件
- 当套接字可以写的时候(客户端对套接字read),那么就可以是WRITABLE事件
- 如果产生两种事件,那么就会优先处理readable事件。
12.1.4 API
- ae.c/aeCreateFileEvent函数接收套接字的描述,一个事件类型,事件处理器作为参数。把给定套接字和给定事件加入到监听范围。并且把事件和事件处理器进行关联。
- ae.c/aeDeleteFileEvent函数这个相当于就是删除上面的关联。并且取消IO多路复用对这个类型的事件的监听。
- ae.c/aeGetFileEvents函数接收套接字描述符,返回监听事件的类型。
- 如果套接字没有任何事件监听,那么返回none
- 如果读事件被监听那么返回AE_READABLE
- 如果是写事件被监听那么就返回writable
- ae.c/aeWait函数接收套接字描述符,事件类型和毫秒参数,如果在规定时间内事件产生或者是如果是超时的时候就返回
- ae.c/aeApiPoll函数接收个sys/time.h/struct timeval结构,在规定时间阻塞并等待所有被aeCreateFileEvent函数设置为监听状态的套接字产生的文件事件。
- ae.c/aeProcessEvents函数这是文件分派器使用aeApiPoll函 数来等待事件产生
- ae.c/aeGetApiName函数返回所有复用IO多路复用的库名称
12.1.5 文件事件的处理器
- 应答处理器,对连接服务器的各个客户端进行应答
- 命令请求处理器,为了接受客户端传送过来的请求
- 回复处理器,返回命令的结果给客户端
- 复制处理器,主从进行复制的时候就需要这个处理器。
1.连接应答处理器
- networking.c/acceptTcpHandler函数就是redis的连接应答处理器。作用是用于处理连接服务器端的监听套接字的客户端的应答。sys/socket.h/accept函数包装
- redis初始化会把应答处理器和服务器监听套接字的AE_READABLE关联起来。
- 客户端调用connect连接服务器监听套接字的时候套接字就会产生AE_READABLE事件,并且引发应答处理器处理应答。
2.命令请求处理器
- networking.c/readQueryFromClient函数就是命令请求处理器
- 负责从套接字读入客户端的命令请求内容。unistd.h/read函数包装
- 当客户端通过应答处理器连接服务器之后,它会把客户端套接字的AE_READABLE事件和请求处理器关联,发送请求就会产生事件,并且交给命令请求处理器处理。
3.命令回复处理器
- networking.c/sendReplyToClient函数这个就是命令回复处理器
- 当服务器有命令回复返回给客户端就会将客户端的套接字AE_WRITABLE事件和命令回复处理器关联,客户端准备好接收回复就会产生AE_WRITABLE事件,引发命令回复处理器进行处理,返回回复结果,实际上就是把结果写入到客户端套接字并且返回。
4.一次完整的客户端与服务器连接事件示例
-
服务器的监听套接字的AE_READABLE事件处于正在监听状态。
-
客户端发送connect,服务器监听套接字监听到connect并且产生AE_READABLE事件,并且让连接应答处理器开始创建客户端套接字,并且设定状态,还需要把客户端套接字和事件AE_READABLE与命令请求处理器进行关联。
-
客户端发送一个请求,这个时候客户端套接字监听到请求,并且产生AE_READABLE事件,并且调用时间请求处理器来处理请求。交给对应程序去处理请求。
-
然后为了执行回复,所以把客户端套接字(客户端套接字是在服务器这边的)的AE_WRITABLE事件和回复处理器进行关联,当客户端想要读取结果的时候,那么客户端套接字就会产生AE_WRITABLE事件,并且交给回复处理器写入结果到套接字返回。
12.2 时间事件
分为两类
- 定时事件:在指定时间执行一次
- 周期性事件:一段时间执行一次
一个时间事件的三个元素
- id:递增id,新事件id大于旧事件
- when:时间事件到达时间的unix时间戳
- timeProc:时间事件处理器。时间事件到达就会调用处理器来进行处理
什么类型的事件判断
- 返回ae.h/AE_NOMORE就是定时事件
- 返回非AE_NOMORE就是周期性时间,所以经常需要对when进行更新,周期性触发事件
12.2.1 实现
- 时间事件放到无序链表,每次时间事件执行就要遍历链表找到已经到达的事件。并且调用事件处理器。
- 链表不按when排序,所以时间事件执行器运行的时候就需要遍历整个链表。
无序链表并不影响时间事件处理器的性能
- redis正常只使用serverCron一个时间事件,服务器把无序链表当成指针使用,他只是保存事件,但是不影响事件的执行。
12.2.2 API
- ae.c/aeCreateTimeEvent函数接收毫秒数milliseconds和时间处理器proc作为参数,把时间事件添加到服务器。时间事件将在当前时间+毫秒数之后到达,那么就调用处理器执行
- ae.c/aeDeleteFileEvent函数接收一个id直接删除这个时间事件和时间事件处理器的关联。
- ae.c/processTimeEvents函数就是时间处理器的执行器。遍历所有已经到达的时间事件,并且调用事件的事件处理器。
def processTimeEvents():
#
遍历服务器中的所有时间事件
for time_event in all_time_event():
#
检查事件是否已经到达
if time_event.when <= unix_ts_now():
#
事件已到达
#
执行事件处理器,并获取返回值
retval = time_event.timeProc()
#
如果这是一个定时事件
if retval == AE_NOMORE:
#
那么将该事件从服务器中删除
delete_time_event_from_server(time_event)
#
如果这是一个周期性事件
else:
#
那么按照事件处理器的返回值更新时间事件的 when
属性
#
让这个事件在指定的时间之后再次到达
update_when(time_event, retval)
12.2.3 时间事件应用实例:serverCron函数
- 定期检查和维护通过serverCron函数来完成。它的工作
- 更新服务器统计信息
- 清空过期键
- 关闭失效的连接
- 尝试进行AOF和RDB的持久化操作
- 主服务器和从服务器的定期同步
- 集群就是做检测和同步
- serverCron每秒运行10次。100ms运行一次。这个函数就是经典的周期性事件
12.3 事件的调度与执行
- 服务器需要对两类型的事件进行调度。
- 通过ae.c/aeProcessEvents函数来进行调度。
def aeProcessEvents():
#
获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()
#
计算最接近的时间事件距离到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()
#
如果事件已到达,那么remaind_ms
的值可能为负数,将它设定为0
if remaind_ms < 0:
remaind_ms = 0
#
根据remaind_ms
的值,创建timeval
结构
timeval = create_timeval_with_ms(remaind_ms)
#
阻塞并等待文件事件产生,最大阻塞时间由传入的timeval
结构决定
#
如果remaind_ms
的值为0
,那么aeApiPoll
调用之后马上返回,不阻塞
aeApiPoll(timeval)
#
处理所有已产生的文件事件
processFileEvents()
#
处理所有已到达的时间事件
processTimeEvents()
- 下面是文件事件的处理函数,它是一直循环调用的,对于上面的processFileEvents是虚构的。处理已经产生的文件事件是通过aeProcessEvents.的。
def main():
#
初始化服务器
init_server()
#
一直处理事件,直到服务器关闭为止
while server_is_not_shutdown():
aeProcessEvents()
#
服务器关闭,执行清理操作
clean_server()
事件调度的规则
- aeApiPoll函数最大阻塞时间是到达时间最接近当前时间来决定。避免服务器频繁向时间事件的轮询。也可保证aeApiPoll阻塞太长的时间。
- 文件事件到达处理之后如果没有时间事件到达,那么就继续处理文件事件。文件事件执行多,那么到达时间也会越来越近就可以执行时间事件了。
- 同步有序原子执行时间,所以不会出现抢占,中断问题。耗时操作放到子进程,其它事件如果执行时间太长就会主动让出执行权。
- 时间事件在文件事件之后执行,所以时间事件处理比实际要慢一些。
12.4 重点回顾
- redis是事件驱动程序,包括了两类文件事件和时间事件
- 文件事件就是socket的操作事件,分为读写两类。只有一个回复是写,连接和接收请求都是读。每次触发事件的关键就是对socket进行了操作,然后就要调用处理事件关联的事件管理器。
- 时间事件分为周期性和定时事件,周期性时间通过更新when来继续执行。
- 执行两类文件的调度
- 优先文件事件,并且计算最近的到达时间的时间事件和当前时间的一个差值先阻塞一下时间事件轮询,先在这段差值时间里面等待和执行文件事件,时间过去之后执行文件事件,然后就能执行时间事件
第13章 客户端
- redis是一个一对多的服务器程序,一个服务器对多个客户端。
- redis.h/redisClient结构每个连接的客户端,服务器都都建立这个结构。
- 客户端的套接字描述符
- 客户端名字
- 客户端标志值
- 指向客户端正在使用的数据库指针和数据库号码
- 当前执行命令的个数
- 输入输出缓冲区
- 复制状态信息和数据结构
- 客户端执行brpop,blpop使用的数据结构
- 客户端事务状态和watch命令使用到的数据结构
- 客户端的身份验证标志
- redis的clients是一个链表结构。下面是三个客户端连接服务器的结构。
struct redisServer {
// ...
//
一个链表,保存了所有客户端状态
list *clients;
// ...
};
13.1 客户端属性
包含属性分为两类
- 通用属性
- 和特定功能相关的属性。操作数据库用到的db,dictid,执行事务的mstate。
13.1.1 套接字描述符
- fd记录了套接字描述符。可以是-1或者是大于-1的整数。
- 伪客户端是-1,执行命令请求来自文件,所以不需要套接字连接。aof和lua
- 普通客户端就是大于-1的。要用套接字和服务器端进行连接。
typedef struct redisClient {
// ...
int fd;
// ...
} redisClient;
13.1.2 名字
- 默认没有名字
- 可以自己设置
typedef struct redisClient {
// ...
robj *name;
// ...
} redisClient;
13.1.3 标志
-
记录了客户端的角色。和状态
- 主从复制的时候REDIS_MASTER是主服务器,REDIS_SLAVE就是从
- REDIS_PRE_PSYNC说明客户端版本低不能使用psync与从服务器同步
- REDIS_LUA_CLIENT处理lua脚本的伪客户端
- REDIS_MONITOR正在执行monitor命令
- ·REDIS_UNIX_SOCKET使用unix套接字连接客户端。
- REDIS_BLOCKED表示客户端被brpop或者blpop阻塞
- REDIS_UNBLOCKED表示已经从阻塞出来了
- REDIS_MULTI表示客户端正在执行事务
这些标志都在redis.h文件上
typedef struct redisClient {
// ...
int flags;
// ...
} redisClient;
flags = <flag1> | <flag2> | ...
13.1.4 输入缓冲区
- 输入缓冲区保存客户端输入的命令请求
- 比如SET key value,缓冲区就会写入*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
typedef struct redisClient {
// ...
sds querybuf;
// ...
} redisClient;
13.1.5 命令与命令参数
- argv命令参数
- argc命令参数个数。
typedef struct redisClient {
// ...
robj **argv;
int argc;
// ...
} redisClient;
13.1.6 命令的实现函数
- 根据argv[0]找到实现命令的函数
- 很明显查找的表就是一个字典。
- 找到这个redisCommand的时候client就会指向这个函数。并且交参数和执行函数
typedef struct redisClient {
// ...
struct redisCommand *cmd;
// ...
} redisClient;
13.1.7 输出缓冲区
- 命令回复会保存到客户端状态的输出缓冲区。通常两个,一个固定大小,一个大小可变。
- 固定大小用于回复字符串小的
- 可变用于回复字符串大的。
- 固定大小就是buf+bufpos(记录已经使用的字节数)
typedef struct redisClient {
// ...
char buf[REDIS_REPLY_CHUNK_BYTES];
int bufpos;
// ...
} redisClient;
- 可变缓冲区大小,可以通过链表把字符串对象串联起来。
typedef struct redisClient {
// ...
list *reply;
// ...
} redisClient;
13.1.8 身份验证
- authenticated主要是判断身份验证是否成功。
- 0失败
- 1成功
typedef struct redisClient {
// ...
int authenticated;
// ...
} redisClient;
13.1.9 时间
- ctime:创建客户端的时间,可以计算创建了多久
- lastinteraction:客户端和服务器端最后一次交互的时间。可以计算空转时间。
- obuf_soft_limit_reached_time第一次到达软性限制时间
typedef struct redisClient {
// ...
time_t ctime;
time_t lastinteraction;
time_t obuf_soft_limit_reached_time;
// ...
} redisClient;
13.2 客户端的创建与关闭
13.2.1 创建普通客户端
- 如果是普通网络连接就会创建普通客户端。
- connect就会调用事件处理器来完成客户端状态创建,并且加入到服务器的链表尾
13.2.2 关闭普通客户端
-
客户端进程关闭或者被杀死。那么网络连接就会关闭
-
客户端发送不符合协议格式的请求
-
设置timeout,空转时间超过timeout就会被断开。
-
发送的命令大小超过了输出缓冲区大小(1GB),主要是为了避免回复太大。占用服务器资源多。限制的模式
- 硬性限制
超出大小那么就立刻断开连接
- 软性限制
超过软性限制的时候,会记录这个时间到obuf_soft_limit_reached_time,设定时长,如果缓冲超过限制而且位置超过时长那么就关闭客户端。
13.2.3 Lua脚本的伪客户端
struct redisServer {
// ...
redisClient *lua_client;
// ...
};
13.2.4 AOF文件的伪客户端
13.3 重点回顾
- 新的客户端状态放到链表后,客户端状态保存在服务器端,标记不同客户端的状态
- flag表示客户端的角色
- 输入缓冲区不能超过1GB,防止回复太大占用大量的输出缓存和服务器资源
- 命令参数和参数个数,argv[0]可以通过字典找到命令的函数redisCommond并且给客户端状态赋值
- 输出缓冲区的限制值有两种方案,硬性到点就关闭,软性等你一会如果后面减少占用就不关闭否则还是要关闭的。
第14章 服务器
- 与客户端建立网络连接,处理各种请求
14.1 命令请求的执行过程
redis> SET KEY VALUE
OK
- 客户端和服务器端执行语句的整个流程
- 客户端发送命令请求给服务器端
- 服务器接收并且处理产生ok
- 服务器把ok发送回客户端。
- 客户端接收ok并且打印给客户看。
14.1.1 发送命令请求
- 用户输入命令请求,客户端转换协议格式并且发送。
14.1.2 读取命令请求
- 客户端写入请求到套接字,这个时候套接字可读
- 服务器调用处理器来处理请求
- 读取套接字的请求,保存到客户端状态的输入缓冲区
- 解析请求获取请求参数和个数,保存到agrv和agrc中
- 调用命令执行器。执行命令。
14.1.3 命令执行器(1):查找命令实现
- 命令执行器先查找agrv[0]的命令执行函数。
- 下面是rediscommand的属性
- 下面是sflags的参数
- set作为例子,函数是setCommand,参数个数为-3也就是至少3个参数,检查命令的占用内存状态,防止占用内存太大。
- 然后设置好redisClient的cmd指针。
14.1.4 命令执行器(2):执行预备操作
-
执行函数,参数都准备好了,但是仍然需要预备操作
- cmd是不是指向null
- 查看cmd的函数所需参数个数是不是符合规则
- 检查客户端的身份验证
- 如果打开maxmemory那么就要检查内存状况
- 如果服务器正在数据载入,那么客户端发送请求要有l标识
-
主要就是检查函数调用是否符合规则。
14.1.5 命令执行器(3):调用命令的实现函数
- 调用下面的就可以调用函数了
- proc指向的是要执行的函数。
- 处理之后的回复会保存到client的输出缓冲区
14.1.5 命令执行器(3):调用命令的实现函数
// client
是指向客户端状态的指针
client->cmd->proc(client);
14.1.6 命令执行器(4):执行后续工作
- 查询是否需要把命令加入慢查询日志
- redisCommond结构的calls调用次数+1
- 如果是AOF持久化,命令写入AOF缓冲区
- 同步到从服务器
14.1.7 将命令回复发送给客户端
- 命令实现函数会把命令回复存入到客户端状态的输出缓冲区,并且为客户端的套接字关联回复事件处理器,一旦套接字可写,那么回复事件处理器就会把输出缓冲区的回复发送到客户端。
14.1.8 客户端接收并打印命令回复
- 客户端接收并且转换成我们可以看懂的语言展示
14.2 serverCron函数
- 每个100ms就会执行一次这个函数主要是更新服务器里面的资源
14.2.1 更新服务器时间缓存
- 每次获取时间都需要系统调用,这个非常耗费性能,而且时间也是经常使用,所以可以通过缓存到redis上。
- 每100ms更新一次下面这些时间
- unixtime秒级
- mstime毫秒级
- 对于时间精准性要求不高的,比如日志,更新LRU时钟这些
- 精准性高的就是设置过期时间,添加慢查询日志,那么就要进行系统调用
struct redisServer {
// ...
//
保存了秒级精度的系统当前UNIX
时间戳
time_t unixtime;
//
保存了毫秒级精度的系统当前UNIX
时间戳
long long mstime;
// ...
};
14.2.2 更新LRU时钟
- 服务器会保存一个LRU时钟,redisObj会保存一个上一次访问的lru时间,通过两个时间可以计算出对象的空转时间
- serverCron会每10s更新一次这个LRU时钟。所以相对来说这个空转时间并不是特别准确。
struct redisServer {
// ...
//
默认每10
秒更新一次的时钟缓存,
//
用于计算键的空转(idle
)时长。
unsigned lruclock:22;
// ...
};
typedef struct redisObject {
// ...
unsigned lru:22;
// ...
} robj;
14.2.3 更新服务器每秒执行命令次数
- serverCron的trackOperationsPerSecond函数每100ms会执行一次更新每秒的命令次数。
- 这个更新是首先抽样ops_sec_last_sample_time这个是上一次的抽样时间和服务器时间,然后还需要上一次的已经执行ops_sec_last_sample_ops命令数和服务器已经执行的命令数计算出当前系统执行的了多少次命令
- 然后记录到ops_sec_samples数组,每次装满16个之后又会重新开始统计。
struct redisServer {
// ...
//
上一次进行抽样的时间
long long ops_sec_last_sample_time;
//
上一次抽样时,服务器已执行命令的数量
long long ops_sec_last_sample_ops;
// REDIS_OPS_SEC_SAMPLES
大小(默认值为16
)的环形数组,
//
数组中的每个项都记录了一次抽样结果。
long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES];
// ops_sec_samples
数组的索引值,
//
每次抽样后将值自增一,
//
在值等于16
时重置为0
,
//
让ops_sec_samples
数组构成一个环形数组。
int ops_sec_idx;
// ...
};
- 最后就是统计数组的样本总数,除以当前的系统的一个平均执行命令数(估算值)得出,系统的平均执行命令数。
long long getOperationsPerSecond(void){
int j;
long long sum = 0;
//
计算所有取样值的总和
for (j = 0; j < REDIS_OPS_SEC_SAMPLES; j++)
sum += server.ops_sec_samples[j];
//
计算取样的平均值
return sum / REDIS_OPS_SEC_SAMPLES;
}
14.2.4 更新服务器内存峰值记录
- 主要就是更新一下内存的状态峰值
struct redisServer {
// ...
//
已使用内存峰值
size_t stat_peak_memory;
// ...
};
14.2.5 处理SIGTERM信号
- 这个信号就是直接关闭redis,但是最好不要使用这个不然RDB都没保存好就删除了。
- sigtrem信号会关联处理器sigtremHandler函数。这个函数就会把server的shutdown_asap=1每次serverCron都会检查这个标识来决定是不是关闭redis
// SIGTERM
信号的处理器
static void sigtermHandler(int sig) {
//
打印日志
redisLogFromHandler(REDIS_WARNING,"Received SIGTERM, scheduling shutdown...");
//
打开关闭标识
server.shutdown_asap = 1;
}
14.2.6 管理客户端资源
- serverCron函数都会调用clientsCron函数检查客户端
- 是否和服务器连接超时
- 输入缓冲区的大小超过长度,那么会就释放之前的并且重新构建
14.2.7 管理数据库资源
- serverCron每次都会调用databasesCron函数,针对过期键进行周期性的处理。
14.2.8 执行被延迟的BGREWRITEAOF
- 服务器的aof_rewrite_scheduled标识有没有延迟了bgrewriteaof。
- 如果是1那么说明延迟,serverCron函数会检查标识并且执行
struct redisServer {
// ...
//
如果值为1
,那么表示有 BGREWRITEAOF
命令被延迟了。
int aof_rewrite_scheduled;
// ...
};
14.2.9 检查持久化操作的运行状态
- rdb_child_pid和aof_child_pid用于记录这两个持久化方式子进程的id
- 如果是-1说明并没有在进行持久化操作,如果不为-1那么就会执行wait3来来看看子进程是否已经持久化完成。
- 信号到达说明子进程完成,那么还需要做后续操作,比如新的RDB替换旧的RDB或者是重写AOF
- 没有信号那么就不动。
- 如果是-1那么还需要检查
- bgrewriteaof是不是被延迟了。
- 服务器的自动保存持久化条件是不是符合了,比如900秒内修改1次,符合那么就写入到RDB。可以执行一个bgsave,对于RDB来说只是修改键值对。
- 查看重写AOF的条件是不是满足,比如AOF的文件已经存储太长了,所以需要重写。
struct redisServer {
// ...
//
记录执行BGSAVE
命令的子进程的ID
:
//
如果服务器没有在执行BGSAVE
,
//
那么这个属性的值为-1
。
pid_t rdb_child_pid; /* PID of RDB saving child */
//
记录执行BGREWRITEAOF
命令的子进程的ID
:
//
如果服务器没有在执行BGREWRITEAOF
,
//
那么这个属性的值为-1
。
pid_t aof_child_pid; /* PID if rewriting process */
// ...
};
14.2.10 将AOF缓冲区中的内容写入AOF文件
- 把aof缓冲区内容刷新到aof文件。
14.2.11 关闭异步客户端
- 如果发现输出缓冲区的回复太大就会关闭掉这个客户端防止占用大量资源。
14.2.12 增加cronloops计数器的值
- 主要就是记录serverCron的执行次数。
14.3 初始化服务器
14.3.1 初始化服务器状态结构
- 一开始就是初始化服务器的状态结构,比如运行id,配置文件路径,默认的端口号等。
- 下面的initServerConfig处理配置
- 服务器运行id
- 默认运行频率
- 配置文件路径
- 运行架构
- 服务器默认端口号
- 持久化条件
- 命令表
- 但是没有创建日志,数据库等的数据结构。原因就是这些数据结构其实依赖于配置文件上面的设置。
void initServerConfig(void){
//
设置服务器的运行id
getRandomHexChars(server.runid,REDIS_RUN_ID_SIZE);
//
为运行id
加上结尾字符
server.runid[REDIS_RUN_ID_SIZE] = '\0';
//
设置默认配置文件路径
server.configfile = NULL;
//
设置默认服务器频率
server.hz = REDIS_DEFAULT_HZ;
设置服务器的运行架构
server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
//
设置默认服务器端口号
server.port = REDIS_SERVERPORT;
// ...
}
14.3.2 载入配置选项
- 我们可以人工修改配置文件,并且在开启redis的时候指定conf。
- 服务器调用initServerConig函数初始化默认server变量之后就会载入配置文件修改server变量。
- 比如下面的32个数据库,默认是16个。
- 比如端口号这些
#
将服务器的数据库数量设置为 32
个
databases 32
#
关闭 RDB
文件的压缩功能
rdbcompression no
14.3.3 初始化服务器数据结构
-
initServerConfig创建了命令表,但是服务器状态还包括
- server.clients链表,这些都是和服务器连接的客户端状态
- server.db数组,数据库数组
- server.lua。
- server.pubsub_channels订阅字典
- server.slowlog用于保存慢日志
-
到现在才创建数据结构,就是为了先把配置文件加载之后,修改server变量,那么这个时候创建才是准确的。
-
initServer就是创建数据库,clients链表的函数。initServerConfig主要加载配置文件和初始化变量。
-
initServer重要配置
- 为服务器创建进程信号处理器
- 创建共享对象,比如OK这些常用字符串。
- 打开服务器监听端口,并且为监听套接字关联应答事件处理器。
- 为serverCron创建时间事件
- 初始化后台IO模块
- 确认持久化类型
14.3.4 还原数据库状态
- 载入RDB或者是AOF
14.3.5 执行事件循环
- 初始化完成等待命令传输到套接字,然后通过IO多路复用传送到各个事件处理器进行处理。
14.4 重点回顾
- 命令请求到发送完成的步骤
- 客户端发送命令给服务器
- 服务器取出命令请求,分析命令参数
- 找到命令的执行函数,根据参数执行函数
- 函数执行完成给输出缓冲区写入回复,然后返回给客户端。
- serverCron函数每100ms执行一次。包括更新时间,服务器执行命令次数,清除过期键等
- 服务器启动到可以执行命令过程
- initServerConfig初始化server变量
- 然后加载配置文件
- initServer初始化数据结构
- 还原数据库状态
- 执行事件循环。
第15章 复制
- 通过slaveof复制其它服务器。复制的那个服务器就是从服务器,被复制那个就是主服务器,还要保证两个服务器之间的一个数据一致性。
15.1 旧版复制功能的实现
- redis复制的功能分为sync同步和命令传播
- 同步从服务器状态更新到跟主服务器相同
- 命令传播,主服务器在修改的时候也要把修改命令发送给从服务器。
15.1.1 同步
- 客户端向服务器发送slaveof命令,要从服务器复制主服务器。那么第一步就是做一个同步操作
- 从服务器向主服务器发送一个sync
- 接收到sync的主服务器执行bgsave来把rdb文件复制生成,并且使用一个缓冲区记录在bgsave过程中的修改命令
- 把rdb文件发送给从服务器,从服务器载入
- 主服务器把缓冲区的命令发送给从服务器。
15.1.2 命令传播
- 但是同步之后不代表数据库就会一直不变,这个时候主服务器可能会发生改变,那么这个时候就需要把改变的命令发送给从服务器进行一个同步操作,这个发送就是命令传播。
15.2 旧版复制功能的缺陷
- 从服务器的复制有两种情况
- 初次复制那么就需要直接同步
- 断线之后重新复制,这个也使用了同步
- 对于第二种情况来说这种也使用同步的问题其实就非常大。因为断线的时候最多只有几条数据丢失了,可能只有几条执行命令没有执行但是却要执行整个复制流程,需要bgsave。IO成本,CPU成本都是耗费非常大的。
- bgsave生成RDB耗费CPU和IO
- 还要把RDB发送从服务器的网络资源耗费
- 从服务器需要载入,载入期间无法处理请求
15.3 新版复制功能的实现
- 这个时候出现了psync
- 完整重同步和初次复制一样就是直接完全进行一次同步操作。
- 部分重同步主要处理断线之后的情况,只要把断开执行的写命令发送给从服务器就可以解决了。
15.4 部分重同步的实现
实现的三个重要概念
- 主服务器的复制偏移量和从服务器的复制偏移量
- 主服务器的复制积压缓冲区
- 服务器运行id
15.4.1 复制偏移量
- 这种其实就是一个偏移位置,用于作为对比。
- 主服务器每次传输n个字节给从服务器,那么偏移量就会+n
- 从服务器如果接收到了那么也是+n
- 对比偏移量很容易知道主从是不是数据一致。下面这里A断线导致没有收到33个字节。所以A服务器重新启动就会发送自己的复制偏移量给主服务器,那么怎么处理?就和复制积压缓冲区相关了。
15.4.2 复制积压缓冲区
- 就是一个FIFO固定长度的先进先出队列。默认大小是1MB
- 对于命令传播程序来说,写命令发送给从服务器的同时还会写到队列,利用队列的固定长度,这样也不会积压太多的缓存,能够控制一定的范围恢复。只能保存最近的修改命令。
- 当从服务器发送自己的复制偏移量给主服务器
- 如果offset之后的数据仍然存在积压缓存区,那么就把后面的发送给从服务器
- 如果没有了那么就只能是进行完整重同步了。
- 调整挤压缓存区的大小很重要,如果写入多的情况,那么积压缓存区要尽量大,否则就尽量小。
15.4.3 服务器运行ID
- 从服务器初次对主服务器复制会把主服务器的运行id保存。
- 当从服务器断线重连就会发送这个运行id和接收的主服务器id进行比较。如果相同才能够进行部分重同步操作,否则就完整重同步。
15.5 PSYNC命令的实现
- psync的调用方法
- 如果没有进行过任何复制,那么从服务器调用PSYNC ? -1命令来进行一次完整重同步
- 否则发送PSYNC runid offset完成部分重同步
- psync的三种回复
- +FULLRESYNC runid offset说明准备执行完整重同步
- +CONTINUE就是部分重同步
- -ERR表示主服务器版本低于2.8无法识别psync命令。
15.6 复制的实现
15.6.1 步骤1:设置主服务器的地址和端口
- 把客户端给的host和port保存到从服务器的redisServer两个变量上
- 实际的复制工作在ok之后
127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
OK
struct redisServer {
// ...
//
主服务器的地址
char *masterhost;
//
主服务器的端口
int masterport;
// ...
};
15.6.2 步骤2:建立套接字连接
- 从服务器作为客户端给主服务器发送连接请求,连接成功从服务器会给套接字关联上一个复制的文件事件处理器。处理复制工作,比如接收rdb和命令。
- 主服务器接收连接之后会为从服务器创建一个客户端状态,从服务器可以发送请求,主服务器应答。
15.6.3 步骤3:发送PING命令
-
为了确定读写正常所以需要给主服务器发送ping
-
如果主服务器回复,但是从服务器读取回复慢,并且超出时间限制,说明网络连接不好,从服务器主动断开对主服务器的套接字连接,并且重连
-
如果主服务器发送一个错误,说明暂时主服务器无法处理,从服务器可以断开连接
-
如果从服务器收到pong说明网络连接正常。
15.6.4 步骤4:身份验证
- 如果从服务器设置了masterauth相当于就是主服务器的密码,主服务器也要设置对应的requirepass。
- 如果他们其中一个缺了都会报错。如果没问题那么就会执行进行复制。
15.6.5 步骤5:发送端口信息
- 从服务器执行REPLCONF listening port 发送端口给主服务器。
- 主要是检查状态使用的。
typedef struct redisClient {
// ...
//
从服务器的监听端口号
int slave_listening_port;
// ...
} redisClient;
15.6.6 步骤6:同步
- 同步之后主服务器和从服务器互为服务器和客户端。
- 完整重同步,主服务器就是客户端把保存在缓冲区的命令发送给从服务器。
- 如果是部分重同步,主服务器作为客户端发送复制积压缓冲区的命令。
- 从服务器更多是发送psync,但是主服务器主要是发送命令传播或者是部分重复制的写命令。
15.6.7 步骤7:命令传播
15.7 心跳检测
- 从服务器每秒都会发送命令
- 检测网络连接
- 实现min-slaves
- 检测命令丢失
REPLCONF ACK <replication_offset>
15.7.1 检测主从服务器的网络连接状态
- 如果主服务器超过1s没有收到从服务器的replication ack那么连接就出问题了。
- info replication的lag可以看出上次从服务器给主服务器发送心跳的时间间距多少秒。
15.7.2 辅助实现min-slaves配置选项
- 防止主服务器在不安全的情况下执行写命令。
- 从服务器少数3个的时候或者延迟超过10s那么主服务器就会拒绝写命令。
min-slaves-to-write 3
min-slaves-max-lag 10
15.7.3 检测命令丢失
- 如果因为网络故障主服务器发送的写命令丢失,那么就可以通过这个从服务器的心跳来查看到从服务器的复制偏移量,并且主服务器重新发送积压缓冲区的写命令。
15.8 重点回顾
- 2.8版本以前的复制不能很好处理断线重连,但是2.8之后通过psync的部分重同步完成了断线的优化
- 部分重同步的关键就是复制偏移量,复制积压缓冲区,服务器执行id
- 复制刚开始从服务器作为主服务器的客户端,发送执行复制请求。复制期间他们互为服务器和客户端。
- 主服务器可以通过从服务器的心跳检测,测试网络,命令丢失检测。