redis服务器
redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理器来维持服务器自身的运转。
命令请求的过程
加入我们使用了这样一天个命令
1 | redis> SET KEY VALUE |
整个执行的流程
- 客户端会将命令转换为协议格式,然后通过连接到服务器的套接字按协议格式的命令请求发送给服务器。
- 当客户端与服务器之间连接的套接字变得可读时,服务器调用命令求情处理器。
- 读取套接字中协议格式的命令,将它保存到客户端状态的输入缓冲区里。
- 对输入缓冲区中的数据进行分析,将请求参数存入argv中,参数个数保存在argc中。
- 根据请求参数(查找命令表),调用相应的命令执行器,执行指定的命令(在正式执行之前还会检测出参数个数是否正确,客户端是否通过身份认证,如果服务器打开了maxmenory功能,那么在执行命令之前,先检查服务器的内存占用情况,并在需要时进行内存回收,之后再进行执行,并且服务器没被阻塞)。
- 再实现函数调用之后,还会执行一系列的后续工作,比如检查是否需要记录到慢查询日志,更新命令的redisCommand结构的milliseconds属性,并将器calls属性加一,如果开启了AOF持久化功能,那么将刚才的命令写入到AOF缓冲区中,如果有其它服务器正在复制这个服务器,那么服务器会见刚刚执行的命令传播到所有从服务器。
- 见结果回复保存到客户端的输出缓冲区里面,并为客户端套接字关联命令回复处理器,当客户端变为可写状态时,服务器就会执行命令回复处理器,并将保存在客户端输出缓冲区中的信息。
- 回复处理器情况输出缓冲区,为下一次命令请求做准备。
- 客户端接收到协议格式的命令返回后,会将这些命令转换为人类可读的格式,并打印给用户。
serverCron函数
serverCron函数默认100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转。
更新服务器时间缓存
Redis中许多服务都依赖于服务器时间,为了避免获取系统时间的大量开销,服务器状态(redisServer)中有unixtime属性和mstime属性被用作当前时间缓存。
serverCorn函数每100毫秒执行一次,获取系统时间更新这两个属性,所以这两个属性记录的时间精度不是很高。
对于需要高精度的时间的时候,Redis还是会执行系统调用来获取最准确的系统时间。更新对象的LRU时钟
服务器状态中的lruclock属性保存了服务器的LRU时钟,也是服务器时间缓存的一种。
每个Redis对象中都有一个lru属性,该属性保持了对象最后一次被直接命中访问的时间。
当服务器需要计算一个数据库键的空转时间的时候,就会使用服务器的lruclock属性的记录减去对象的lru属性的记录,得出对象的空转时间。
serverCorn函数默认会以每10秒一次的频率更新lruclock属性,因为这个时钟不是实时的。所以根据这个属性计算出来的Lru时间实际是一个模糊的估算值。
更新服务器每秒执行命令次数
serverCron函数中的trackOperationsPerSecond函数会以每100毫秒一次的频率执行,通过抽样计算的方式,估算服务器在最近一秒钟处理的命令请求数量。trackOperationsPerSecond函数会根据ops_sec_last_sample_time记录的上次抽样时间和服务器的当前时间,以及ops_sec_last_sample_ops记录的上一次抽样的已执行命令数量和服务器当前的已执行命令数量,计算出两次trackOperationsPerSecond掉用直接,服务器平均每毫秒处理了多少个命令请求,然后将这个值乘以1000,就得到了服务器在一秒钟内能处理多少个命令请求的估计值。更新服务器内存分治记录
服务器状态中stat_peak_memory属性记录了服务器的内存峰值大小。
1 | struct redisServer{ |
每次serverCron执行的时候,都会查看服务器的已使用内存,并与stat_peak_menory进行比较。如果当前的值更大,就更新stat_peak_menory
- 处理SIGTERM信号
在启动服务器的时候,Redis会为每个服务器进程关联SIGTERM信号关联处理器sigtermHandler函数,这个信号处理器负责在服务器接到SIGTERM信号时,打开服务器状态的shutdown_asap标识。每次ServerCorn函数运行时,程序都会对服务器状态的shutdown_asap属性进行检查,并根据属性的值决定是否关闭服务器。
管理客户端资源
serverCron函数每次执行都会调用clientCron函数检查客户端与服务器的连接是否超时(如果超时则断开),输入缓冲区是否(超过了一定的长度,如果超过了就释放原来的缓冲区,分配一个新的缓冲区)管理数据库资源
serverCron函数每次执行都会调用databasesCron函数,这个函数对对服务器中的一部分数据库进行检查,删除其中的过期键,如果有需要时,对字典进行收缩。执行被延迟的BGREWRITEAOF
在服务器执行BGSAVE命令期间,如果客户端向服务器发来的BGREWRITEAOF会被延迟,服务器状态中的aof_rewrite_scheduled标识记录了服务器是否延迟了BGREWRITEAOF命令。检查持久化操作的运行状态。
服务器状态中使用rdb_child_pid属性和aof_child_pid属性记录执行BGSAVE命令和BGREWRITEAOF命令的子进程ID,这两个属性也可以用于检测BGSAVE命令或者BGREWRITEAOF命令是否正在执行。将AOF缓冲区内的内容写入AOF文件
如果服务器开启了AOF持久化,并且AOF缓冲区内有待写入的数据,那么serverCron函数会调用相应的程序,将AOF缓冲区中的内容写入AOF文件中。关闭异步客户端
关闭那些输出缓冲区大小超出限制的客户端。- 增加cronloops计数器的值。
服务器状态中cronloops属性记录了serverCron函数执行的次数。
服务器的初始化
初始化服务器的第一步就是创建一个redisServer类型的示例变量,server作为服务器的状态,并为结构中的每个属性设置默认值。
初始化server变量的工作由redis.c/initsServerConfig函数完成,它除妖进行以下的工作:
- 设置服务器的运行ID
- 设置服务器的默认运行频率
- 设置服务器的默认配置文件路径
- 设置服务器的运行架构
- 设置服务器的运行架构
- 设置服务器的默认端口号
- 设置服务器的默认RDB持久化条件和AOF持久化条件
- 初始化服务器的LRU时钟
- 创建命令表
在启动服务器时,用户可以用户给定配置参数或指定配置文件来修改服务器的默认配置。如果用户为这些属性设置了新的值,那么服务器就使用用户设定的这些值,否则使用默认值。
初始化服务器数据结构:
initServerConfig函数初始化了server状态,服务器还需要创建其它数据结构(initServer负责):
- server.clinets链表,器包含了所有与服务器向连的客户端的状态结构。构成链表的节点redisClients代表了一个redis客户端。
- server.db数组,该数组包含了所有的数据库。
- 用于保存频道订阅信息的server.pubsub_channels字典,以及用于保存模式订阅信息的server.pubsub_patterns链表。
- 用于执行Lua脚本的Lua环境server.lua属性
- 用于保存慢查询日志的server.slowlog属性
除了初始化数据结构,initServer还进行了一些非常重要的设置操作,包括:
- 为服务器设置进程信号量
- 创建共享对象(比如共享的字符串对象)
- 打开服务器的监听端口
- 为serverCron函数创建时间事件
- 入股AOF持久化以及打开,那么打开现有的AOF文件,如果没有现有的文件则创建。
- 初始化服务器后台的IO模块。
还原数据库状态:
在完成对服务器状态server的初始化之后,服务器需要在入RDB文件或者AOF文件。并根据文件内容还原数据库状态。
如果启用了AOF持久化功能,则使用AOF还原数据库,如果没有启用则使用RDB文件还原数据库。
完成数据库状态还原后,在日志中打印还原数据库的耗时。
执行事件循环:
在完成数据库状态的还原后,开始进入事件循环。
多机数据库的实现
复制
Redis中可以通过执行SLAVEOF命令设置服务器去复制另一个服务器。比如:
1 | 复制127.0.0.1 6379主机,成为从服务器 |
我们在主服务器上添加的键值对,在从服务器和主服务器上都能查询到。
复制功能的实现
旧版
Redis的复制,分为同步和命令传播两个操作。
同步是让从服务器的状态更新至主服务器的状态。
命令传播,是当主服务器的数据库状态被更改后,让从服务器和主服务器保持一致的方式。
同步
从服务器与主服务器同步需要从服务器向主服务器发送SYNC命令,具体步骤如下:
- 从服务器向主服务器发送SYNC命令
- 主服务器收到命令后,执行BGSAVE命令,后台生成RDB文件。同时使用缓冲区记录从现在开始执行的写命令
- 主服务器BGSAVE执行完毕后,主服务器件RDB文件发送给从服务器。从服务器载入RDB文件,与将服务器状态更新至与RDB文件中的状态一致。
- 主服务器将缓冲区的命令发送给RDB文件,从服务器执行这些命令,保持与主服务器状态一致。
命令传播:
当主服务器因为执行命令导致主服务器与从服务器的状态不一致后,主服务器会向从服务器发送命令,使主服务器和从服务器的状态保持一致。
旧版复制功能的缺陷
旧版复制功能的最大的缺陷就是断线复制,需要重新执行一次同步操作(从发送SYNC开始)。而SYNC命令使意向非常消耗资源的操作。
新版
从Redis2.8开始,使用PSYNC命令来执行复制时的同步操作。
PSYNC具有完整重同步和部分重同步两种模式:
和旧版的同步操作的区别住哟啊在于部分重同步。步骤:
- 断线重连后,从服务器向主服务器发送PSYNC命令。
- 主服务器收到PSYNC命令后,向从服务器返回+CONTINUE回复,表示执行部分重同步
- 从服务器接收到+CONTINUE命令之后,准备执行部分从同步。
- 主需要器向从服务器发送断线期间收到的写命令
- 从服务器接收这些写命令并执行,完成同步。
部分重同步的实现原理:
主要由三个部分组成:
- 主服务器的复制偏移量和从服务器的复制偏移量
- 从服务器的复制积压缓冲区
- 服务器的运行ID
复制偏移量
主服务器和从服务器都分别维护了两个复制偏移量。
主服务器的复制偏移量记录了发送了的字节数量。
从服务器的复制偏移量记录了接收了的字节数量。
通过对主从服务器的复制偏移量可以很容易的知道主从服务器是否处于一致状态。
复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度先进先出队列,默认大小为1MB。当主服务器在进行命令传播的时候,不仅会把写命令发送给所有的从服务器,还会将写命令入队到复制积压缓冲区。
当主服务器收到从服务器的复制偏移量之后,主服务器会根据偏移量之后的数据是否在复制积压缓冲区内来决定以何种同步方式进行同步。如果采用部分重同步的方式,主服务器就把复制积压缓冲区内的偏移量之后的命令发送给从服务器。
服务器ID
部分重同步还需要服务器ID,无论是主服务器还是从服务器都需要有自己的服务器ID。运行ID在服务器启动的时候随机生成。当从服务器对主服务器进行初次复制时,主服务器会把自己的ID传送给从服务器,而从服务器会将这个ID保存起来。
从服务器通过保存的主服务器ID,可以判断断线前连接的服务器是否是断线前连接的主服务器。
PSYNC的执行流程
心跳检测:在命令传播阶段,还会从服务器还会以每秒一次的频率,向主服务器发送ACK <replication_offset>
。它有三个作用,一是检测主从服务器的网络连接状态,二是辅助实现min-salaves选项,三是检测命令丢失。
Sentinel(哨兵)
Sentinel(哨兵)是Redis的高可用解决方案。由一个或多个哨兵实例组成的哨兵系统可以监视任意多个主服务器,以及这些主服务器的从犯服务器。当被监视的主服务器进入下线状态时,自动将下线主服务器属下的从服务器升级为新的主服务器。然后由新的主服务器代替已下线的主服务器继续处理请求命令。
哨兵监视系统的工作例子:
- 下图时哨兵系统的例子
- 主服务器下线,从服务器终止复制
- 若下线的主服务器没有在规定的时间内上线,哨兵系统会让其从服务器升级为主服务器,并对新的主服务器的所有从服务器发送复制指令
- 如果向下的主服务器从新上线,那么会被哨兵降级为从服务器。
启动并初始化Sentinel
启动一个Sentinel可以使用的命令
1 | redis-sentinel /path/to/your/sentinel.conf //方案一 |
- 初始化服务器
因为Sentinel的本质时一个特殊的redis服务器,所以它会和普通的redis服务器一样被初始化,但是Sentinel不会通过在入RDB或AOF来还原数库状态,因为它不需要承担数据库的角色。 - 将普通的redis服务器使用的代码替换为Sentinel专用代码
Sentinel虽然是一种特殊的Redis服务器,当终究不是Redis服务器,所以代码实现也是不一样的,它会把普通的Redis服务代码替换为Sentinel专用代码,因此如端口号命令表等东西也是不同的。 初始化Sentinel状态
在启用了Sentinel专用代码之后,服务器会出初始化一个sentinelState结构,它保存了服务器中所有和Sentinel功能相关的状态。1
2
3
4
5
6
7
8
9
10
11struct sentinelState{
uint64_t current_epoch; //当前纪元
dict *masters;//指向保存了所有被监视的主服务器信息的字典
int tilt;//是否进入tilt模式
int running_scripts;//目前正在执行的脚本数量
mstime_t tilt_start_time;//进入tilt模式的时间
mstime_t previous_time;//最后一次执行时间处理器的时间
list *script_queue; //一个FIFO队列,包含了所有要执行的用户脚本
}根据给定的配置文件,初始化Sentinel的监视主服务器列表(masters属性)
masters属性是一个指向字典的指针,字典的键是主服务器的名称,值为保存主服务器信息的sentinelRedisInstance结构1
2
3
4
5
6
7
8
9
10
11
12typedef struct sentinelRedisInstance{
int flags; //标识值,记录了实例的类型,以及实例的当前状态
char *name; //实例名称
char *runid;//实例的运行ID
uint64_t config_epoch;//配置纪元
sentinelAddr *addr;//实例的地址
mstime_t down_after_period;//实例多少毫米之后才会被判断为主观下线
int quorum;//判断这个实例为客观下线所需的支持投票数量
int parallel_syncs;//在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
mstime_t failover_timeoout;//刷新故障转移状态的最大时限
}sentinelRedisInstance;
masters字典的初始化需要根据被载入的Sentinel配置文件进行的。
- 创建连向主服务器的网络连接
最后一步是创建连向主服务器的网络连接,Sentinel将成为主服务器的客户端,它可以从主服务器发送命令,并从命令回复中获取相关信息。
一般是有两个连接,一个是命令连接,一个是订阅连接。
Sentinel的工作流程
1. 获取主服务器信息
Sentinel默认每十秒向连接的主服务发送INFO命令,并根据INFO命令的回复来获取主服务器的当前信息。
INFO命令的回复信息中主要包含了两个方面的信息:
- 主服务器本身的信息,如服务器运行Id,服务器角色等信息。
- 另以方面是主服务器属性的从服务器信息。这使得Sentinel无需配置从服务器的地址就可以自动发现从服务器。
2. 获取从服务器信息
Sentinel发现主服务器有了新的从服务器后,Sentinel会为新的从服务器创建新颖的实例结构,还会创建连接到从服务器的命令连接和订阅连接。
3. 向主服务器和从服务器发送信息
在默认情况下,Sentinel会一每两秒一次的频率,通过命令连接线被监视的主服务器和从服务器发送命令。
4. 接收来自主服务器和从服务器的频道信息
当Sentinel与一个主服务器或者从服务器建立起订阅连接之后,Sentinel就会通过订阅连接,向服务器发送以下命令。SUBSCRIBE _sentinel_:hello
Sentinel对sentinel:hello频道的订阅会一直持续到Sentinel与服务器连接断开为止。
5. 更新sentinels字典
Sentinel为主服务器创建的实例机构中的sentinels字典保存了除Sentinel本身之外,所有同样监视这个主服务器的其它Sentinel的资料。当一个Sentinel接收到其它Sentinel发来的信息是,目标Sentinel会从信息中分析并提取除以下两个方面参数:
- 与Sentinel有关的参数:源Sentinel信息。
- 与主服务器有关的参数:源Sentinel正在监视的主服务器的信息。
6. 创建连向其它Sentinel的命令连接
当Sentinel通过频道信息发现一个新的Sentinel时,它不仅会从新Sentinel在sentinels字典中创建相应的实例结构,还会创建一个新Sentinel的命令连接。
7. 检测主观下线状态
默认情况下,Sentinel每秒都会想其它创建了连接的实例发送PING命令,并通过实例返回PING命令回复来判断实例是否在线。如果一个实例在down-after-minlliseconds毫秒内,连续向Sentinel返回回复无效。就会判定为主观下线,在结构的flags属性中打开SRI_S_DOWN标识。(多个Sentinel时,它们的主观下线时长可能不同)
8. 检查客观下线状态
当一个Sentinel将一个主观服务器判断为主观下线之后,会向同一个监视这个主服务器的其它Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态。当认为主服务器下线的Sentinel的数量超过Sentinel配置中设置的quorum参数的值。那么该Sentinel就会认为主服务器进入客观下线状态。
9. 选举领头Sentinel
当一个主服务器被判定为客观下线后,所有监视这个主服务器的Sentinel会进行协商,选出一个领头的Sentinel,并由领头的Sentinel进行故障转移操作。
选取规则如下:
- 所有在线的Sentinel都由被选为领头Sentinel的资格。
- 每次进行领头Sentinel进行选举,无论选举是否成功,所有Sentinel的配置纪元的值都会自增一。
- 在一个配置纪元里,每个Sentinel都有一次将某个Sentinel设置为局部领头的机会。
- 每个发现主服务器下线的Sentinel都会要求其它的Sentinel把自己设置为局部领头。
- 一个源Sentinel向另一个目标Sentinel发送SENTINEL is-master-down-by-addr命令,并且表示源Sentnel要求目标Sentinel将前者设置为后者的局部领头Sentinel。
- Sentinel设置局部领头Sentinel的规则是先到先得的。
- 目标Sentinel在接收到SENTINEL is-master-down-by-addr后会向源Sentinel发送一条信息,其中leader_runid和leader_epoch参数分别记录了目标Sentinel的局部领头和Sentinel的运行id。
- 如果收到命令的回复总,leader_epoch和自己的一致,且leader_runid和自己的运行id一致,那么表示自己被设置为了局部领头。
- 如果某个Sentinel被半数以上的Sentinel设置为了局部领头Sentinel,那么其会成为领头Sentinel。
- 如果在一段时间内没有一个Sentinel被选举为领头,那么一段时间后会进行重新选举。
10. 故障转移
当选出领头Sentinel后,领头的Sentinel会进行故障转移:
- 在已下线的主服务器的从服务器中选择一个作为新的主服务器。
- 让一下线的主服务器属下的从服务器改为复制新的主服务器
- 将已下线的主服务器设置为新的主服务器的从服务器。
集群
Redis集群式Redis提供的分布式数据库法案,集群通过分片来进行数据共享,并提供复制和故障转移功能。
节点
一个Redis集群通常是由多个节点组成的,在刚开始的时候,每个节点都是相互独立的,他么都处于一个只包含自己的集群当中。
连接各个集群可以使用:
1 | CLUSTER MEET <ip> <post> |
向一个节点发送CLUSTER MEET命令,可以让该节点与指定的节点握手,握手成功后,就会把这个节点添加到自己所在的集群中。
一个节点在启动的时候会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式。
节点的数据结构:
clusterNode结构保存了一个节点的当前状态,比如节点的创建时间、节点的名字、节点的当前配置纪元、节点的IP地址和端口号等信息。
1 | struct clusterNode{ |
link指向了一个clusterLink结构。
1 | typedef struct clusterLink{ |
每个节点都保存了一个clusterState结构,这个结构记录了在当前节点的视角下,集群目前所处的状态。
1 | typedef struct clusterNode{ |
槽指派
Redis集群中通过分片的方式来保存数据库中的键值对。整数数据库被划分为16384个槽,数据库中的每个键都属于一个槽,一个节点可以处理一个或多个槽,当所有的槽都被处理时,集群处于上线状态。
槽指派
1 | CLUSTER ADDSLOTS <slot> [slot ...] |
clusterNode结构中的slots属性和numsslsot属性记录节点复制处理哪些槽
1 | struct clusterNode{ |
一个节点除了会见自己负责处理的槽记录在clusterNode结构和solt属性和numslots属性中之外,还会将自己的slots数组通过消息发送给集群中的其它节点。
clusterState结构中,记录了集群中所有槽的指派信息:
1 | typedef struct clusterState{ |
在集群中执行命令
当客户端向集群发送命令时,接收命令的节点会计算出数据库键属于那个槽,并检查这个槽是否指派给了自己。
- 如果这个槽刚好是自己,那么就执行命令
- 如果这个槽不是属于自己,那么就会向客户端返回一个MOVED错误,指引客户端转向正确的节点
重新分片
Redis集群的重新分片操作可以将任意数量已经指派给某个节点的槽改为指派给另一个节点。并且相关槽所属的键值对也会从源节点被移动到目标节点。
重新分片操作可以在向进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。
事务
Redis通过MULTI、EXEC、WATCH等命令来实现事务的功能。
Redis的事务首先以MULTI命令开始,接着放入事务当中,最后由EXEC命令将在这个事务提交给服务器执行。
事务的实现
一个事务会经历以下三个阶段:
- 事务开始
- 事务入队
- 事务执行
事务入队
每个redis客户端都有自己的事务状态,这个事务站台保存在客户端状态的mstate属性里面:
1 | typedef stcuct redisCline{ |
每个事务状态包含了一个事务队列:
1 | typedef struct multiState{ |
事务队列是一个multiCmd类型的数组:
1 | typedef struct multiCmd{ |
执行事务
当一个处于事务状态的客户端向服务器发送EXEC
命令时,这个EXEC命令会立即执行。服务器会遍历这个客户端的事务队列,执行队列中保存的所有的命令,最后将执行命令所得到的结果全部返回给客户端。
WATCH命令的实现
WATCH命令是一个乐观锁。它可以在事务提交前监视任意数量的键,并在事务提交时检查监视的键是否过期了,如果过期了,服务器会拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
每个reids数据库中都保存着一个watched_keys字典。这个字典的键是某个被WATCH命令监视的数据库键,而字典的值则是一个链表。
1 | typedef struct redisDb{ |
通过watched_keys字典,服务器可以清楚的知道,哪些数据库键正在被监视,以及哪些客户端正在监视这些数据库键。
如果被监视的键被修改,那么监视该键的所有客户端的REDIS_DIRY_CAS标识将被打开。当服务器接收到一个客户端发来的EXEC命令时,服务器会根据客户端是否打开了REDSI_EIDY_CAS标识是否打开了。
Redis的事务和传统的关系型数据库的事务的不同之处在于Redis不支持事务的回滚机制,即使事务中的某个命令出错了,整个事务也会继续执行下去。在事务命令的入队期间,如果有错误的命令,那么Redis会拒绝执行这个事务。如果事务在执行的过程中出错了,那么数据库不会进行任何的修改,因此对事务的一致性产生任何使用。
v1.5.2