第8章 对象与编码

查看数据库键的值对象的编码OBJECT ENCODING
Redis五种类型对象:字符串对象(string),列表对象(list),哈希对象(hash),集合对象(set),有序集合对象(zset)。
不同类型和编码的对象
类型
编码
对象
REDIS_ENCODING_INT
使用整数值实现的字符串对象
REDIS_ENCODING_EMBSTR
使用embstr编码的简单动态字符串实现的字符串对象
REDIS_ENCODING_RAW
使用简单动态字符串实现的字符串对象
REDIS_ENCODING_ZIPLIST
使用压缩列表实现的列表对象
REDIS_ENCODING_LINKEDLIST
使用双端列表实现的列表对象
REDIS_ENCODING_ZIPLIST
使用压缩列表实现的哈希对象
REDIS_ENCODING_HT
使用字典实现的哈希对象
REDIS_ENCODING_INTSET
使用整数集合实现的集合对象
REDIS_ENCODING_HT
使用字典实现的集合对象
REDIS_ENCODING_ZIPLIST
使用压缩列表实现的有序集合对象
REDIS_ENCODING_SKIPLIST
使用跳跃表和字典实现的有序集合对象
类型检查的实现
类型特定命令所进行的类型检查时通过redisObject结构的type属性来实现的:
  • 在执行一个类型特定命令之前,服务器会先检查输入数据库键的值对象是否为可执行命令所需的类型,如果是的话,服务器就对键执行指定的命令;
  • 否则,服务器将拒绝执行命令,并向客户端返回一个类型错误。

第9章 数据库

Redis服务器使用惰性删除和定期删除两种策略。
键空间通知:某个键执行了什么命令
键事件通知:某个命令被什么键执行了

第10章 RDB持久化

RDB文件用于保存和还原Redis服务器所有数据库中的所有键值对数据。
SAVE命令会阻塞服务器,客户端发送的所有命令请求都会被拒绝;BGSAVE会fork子进程来执行持久化,父进程在等待子进程信号的同时仍然可以继续处理请求。
BGSAVE期间:
  • SAVE命令会被拒绝,为了避免父子进程同时执行rdbSave调用,产生竞争;
  • BGSAVE命令会被拒绝,为了防止产生竞争;
  • BGREWRITEAOF不能同时执行,BGSAVE期间,客户端发送的BGREWRITEAOF命令会被延迟到BGSAVE命令执行完毕之后执行;BGREWRITEAOF期间,客户端发送的BGSAVE命令会被服务器拒绝。因为两个命令实际工作都是由子进程执行,这两个命令在操作方面并没有什么冲突的地方,不能同时执行只是一个性能方面的考虑。

第11章 AOF持久化

AOF文件通过保存所有修改数据库的写命令请求来记录服务器的数据库状态。
AOF持久化功能的实现可以分为命令追加(append),文件写入,文件同步(sync)三个步骤。
AOF重写:首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是AOF重写功能的实现原理。
AOF后台重写:创建子进程进行AOF重写。
子进程执行AOF重写期间,服务器进程需要执行以下三个工作:
1) 执行客户端发来的命令
2) 将执行后的写命令追加到AOF缓冲区
3) 将执行后的写命令追加到AOF重写缓冲区
在整个AOF后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低。

第12章 事件

  • 文件事件
    • #ifdef HAVE_EVPORT #include "ae_evport.c" #else #ifdef HAVE_EPOLL #include "ae_epoll.c" #else #ifdef HAVE_KQUEUE #include "ae_kqueue.c" #else #include "ae_select.c" #endif #endif #endif
      事件类型:
    • AE_READABLE
    • AE_WRITEABLE
    • AE_NONE
    • 处理器类型:
    • 连接应答处理器
    • 命令请求处理器
    • 命令回复处理器
    • 主从复制处理器
  • 时间事件
    • 定时事件
    • 周期性事件

第13章 客户端

PUBSUB SCRIPT LOAD命令不会对服务器进行修改,需要使用REDIS_FORCE_AOF标志才会被写入到AOF文件。
伪客户端存在的两种情况:
  • AOF恢复过程中(仅存在于加载AOF文件时)
  • 加载Lua脚本中(一直存在)
普通客户端被关闭原因:
  1. 客户端进程退出或者被杀死
  1. 客户端向服务器发送了带有不符合协议格式的命令请求
  1. 客户端成为了CLIENT KILL命令目标
  1. 服务器设置了timeout选项,当客户端空闲时间超过timeout值时,客户端会被关闭。不过timeout有一些例外情况:如果客户端是主服务器(打开了REDIS_MASTER标志),从服务器(打开了REDIS_SLAVE标志),正在被BLPOP等命令阻塞(打开了REDIS_BLOCKED标志),或者正在执行SUBSCRIBE, PSUBSCRIBE等订阅命令,那么即使客户端空闲时间超过timeout,客户端也不会被服务器关闭
  1. 客户端发送的命令请求大小超过了输入缓冲区的限制大小(默认为1GB)
  1. 要发送给客户端的命令回复大小超过了输出缓冲区的限制大小
设置输出缓冲区的软性限制和硬性限制:
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>

第14章 服务器

命令请求的执行过程:
  1. 客户端发送命令请求
  1. 服务器读取命令请求
  1. 命令执行器:查找命令实现
  1. 命令执行器:执行预备操作
  1. 命令执行器:调用命令的实现函数
  1. 命令执行器:执行后续工作
  1. 将命令回复发送给客户端
  1. 客户端接收并打印命令回复
serverCron函数:
  • 每隔100毫秒执行一次
  • 更新服务器时间缓存
  • 更新LRU时钟
  • 更新服务器每秒执行命令次数
  • 更新服务器内存峰值记录
  • 处理SIGTERM信号
  • 管理客户端资源
  • 管理数据库资源
  • 执行被延迟的BGREWRITEAOF
  • 检查持久化操作的运行状态
  • 将AOF缓冲区中的内容写入AOF文件
  • 关闭异步客户端
  • 增加cronloops计数器的值
初始化服务器:
  1. 初始化服务器状态结构
  1. 载入配置选项
  1. 初始化服务器数据结构
  1. 还原数据库状态
  1. 执行事件循环

第15章 复制

SYNC:RDB完整重同步
PSYNC:完整重同步(首次),部分重同步(断线重连)
部分重同步构成:
  • 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量
  • 主服务器的复制积压缓冲区(replication backlog)
  • 服务器的运行ID(run ID)
 
PSYNC执行部分重同步和完整重同步时可能遇上的情况
PSYNC执行部分重同步和完整重同步时可能遇上的情况
复制功能实现步骤:
  1. 设置主服务器的地址和端口
  1. 建立套接字连接
  1. 发送PING命令
  1. 身份验证
  1. 发送端口信息
  1. 同步
  1. 命令传播
心跳检测REPLCONF ACK <replication_offset>

第16章 Sentinel

启动sentinel:
redis-sentinel /path/to/your/sentinel.conf
或者
redis-server /path/to/your/sentinel.conf --sentinel
启动步骤:
  1. 初始化服务器
  1. 将普通Redis服务器使用的代码替换成Sentinel专用代码
  1. 初始化Sentinel状态
  1. 根据给定的配置文件,初始化Sentinel的监视主服务器列表
  1. 创建连向主服务器的网络连接
对于每个被sentinel监视的主服务器来说,sentinel会创建两个连向主服务器的异步网络连接:
  • 一个是命令连接,这个连接专门用于向主服务器发送命令,并接收命令回复
  • 另一个是订阅连接,这个连接专门用于订阅主服务器的__sentinel__:hello频道
监视同一个服务器的sentinel相互之间只会创建命令链接。
主观下线状态:down-after-milliseconds时间内未收到正确回复(+PONG, -LOADING, -MASTERDOWN)
客观下线状态:当认为主服务器已经进入下线状态的sentinel的数量,超过sentinel配置中设置的quorum参数的值,那么该sentinel就会认为主服务器已经进入客观下线状态。
sentinel选举规则:
  • 所有在线的sentinel都有被选举为领头sentinel的资格。
  • 每次进行领头sentinel选举之后,不论选举是否成功,所有sentinel的配置纪元(configuration epoch)的值都会自增一次。
  • 在一个配置纪元里面,所有sentinel都有一次将某个sentinel设置为局部领头sentinel的机会,并且局部领头一旦设置,在这个配置纪元里面就不能再更改。
  • 每个发现主服务器进入客观下线的sentinel都会要求其他sentinel将自己设置为局部领头sentinel。
  • 当一个Sentinel(源Sentinel)向另一个Sentinel(目标Sentinel)发送SENTINEL is-master-down-by-addr命令,并且命令中的runid参数不是*符号而是源Sentinel的运行ID时,这表示源Sentinel要求目标Sentinel将前者设置为后者的局部领头Sentinel。
  • Sentinel设置局部领头Sentinel的规则是先到先得:最先向目标Sentinel发送设置要求的源Sentinel将成为目标Sentinel的局部领头Sentinel,而之后接收到的所有设置要求都会被目标Sentinel拒绝。
  • 目标Sentinel在接收到SENTINEL is-master-down-by-addr命令之后,将向源Sentinel返回一条命令回复,回复中的leader_runid参数和leader_epoch参数分别记录了目标Sentinel的局部领头Sentinel的运行ID和配置纪元。
  • 源Sentinel在接收到目标Sentinel返回的命令回复之后,会检查回复中leader_epoch参数的值和自己的配置纪元是否相同,如果相同的话,那么源Sentinel继续取出回复中的leader_runid参数,如果leader_runid参数的值和源Sentinel的运行ID一致,那么表示目标Sentinel将源Sentinel设置成了局部领头Sentinel。
  • 如果有某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成为领头Sentinel。
  • 因为领头Sentinel的产生需要半数以上Sentinel的支持,并且每个Sentinel在每个配置纪元里面只能设置一次局部领头Sentinel,所以在一个配置纪元里面,只会出现一个领头Sentinel。
  • 如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出领头Sentinel为止。
故障转移步骤:
  1. 在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器
  1. 让已下线主服务器属下的所有从服务器改为复制新的主服务器
  1. 将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器

第17章 集群

节点连接:CLUSTER MEET <ip> <port>
查看节点:CLUSTER NODES
节点握手过程:
  1. 节点A会为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面
  1. 之后,节点A将根据CLUSTER MEET命令给定的IP地址和端口号,向节点B发送一条MEET消息(message)
  1. 如果一切顺利,节点B将接收到节点A发送的MEET消息,节点B会为节点A创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面
  1. 之后,节点B将向节点A返回一条PONG消息
  1. 如果一切顺利,节点A将接收到节点B返回的PONG消息,通过这条PONG消息节点A可以知道节点B已经成功地接收到了自己发送的MEET消息
  1. 之后,节点A将向节点B返回一条PING消息
  1. 如果一切顺利,节点B将接收到节点A返回的PING消息,通过这条PING消息节点B可以知道节点A已经成功地接收到了自己返回的PONG消息,握手完成
 
节点的握手过程
节点的握手过程
之后,节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也与节点B进行握手,最终,经过一段时间之后,节点B会被集群中的所有节点认识。
槽指派:CLUSTER ADDSLOTS <slot> [slot ...]
只有当16384个槽全部被指派时,集群才处于上线状态。
计算键属于哪个槽:CRC16(key) & 16383
redis-trib对集群的单个槽进行重新分片的步骤:
  1. redis-trib对目标节点发送CLUSTER SETSLOT<slot>IMPORTING<source_id>命令,让目标节点准备好从源节点导入(import)属于槽slot的键值对。
  1. redis-trib对源节点发送CLUSTER SETSLOT<slot>MIGRATING<target_id>命令,让源节点准备好将属于槽slot的键值对迁移(migrate)至目标节点。
  1. redis-trib向源节点发送CLUSTER GETKEYSINSLOT<slot><count>命令,获得最多count个属于槽slot的键值对的键名(key name)。
  1. 对于步骤3获得的每个键名,redis-trib都向源节点发送一个MIGRATE<target_ip><target_port><key_name>0<timeout>命令,将被选中的键原子地从源节点迁移至目标节点。
  1. 重复执行步骤3和步骤4,直到源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止。
  1. redis-trib向集群中的任意一个节点发送CLUSTER SETSLOT<slot>NODE<target_id>命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽slot已经指派给了目标节点。
 
对槽slot进行重新分片的过程
对槽slot进行重新分片的过程
MOVED错误:当节点发现键所在的槽并非由自己负责处理的时候,节点就会向客户端返回一个MOVED错误,指引客户端转向正在负责槽的节点,并再次发送之前想要执行的命令。
ASK错误:分片过程中,客户端命令要处理的数据库键恰好属于正在被迁移的槽时,源节点会先在自己的数据库里面查找,如果找到就直接执行,找不到就向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,首先发送ASKING命令,然后再次发送之前想要执行的命令。
 
ASK错误和MOVED错误的区别
ASK错误和MOVED错误的区别
设置从节点:CLUSTER REPLICATE <node_id>
选举新的主节点:
  1. 集群的配置纪元是一个自增计数器,它的初始值为0。
  1. 当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值会被增一。
  1. 对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。
  1. 当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
  1. 如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。
  1. 每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。
  1. 如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,这个从节点就会当选为新的主节点。
  1. 因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有N个主节点进行投票,那么具有大于等于N/2+1张支持票的从节点只会有一个,这确保了新的主节点只会有一个。
  1. 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。
节点发送的消息主要有以下五种:
  • MEET消息
  • PING消息
  • PONG消息
  • FAIL消息
  • PUBLISH消息

第18章 发布与订阅

Redis的发布与订阅功能由PUBLISH, SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE, PUBSUB等命令组成。

第19章 事务

一个事务从开始到结束通常会经历以下三个阶段:
  1. 事务开始
  1. 命令入列
  1. 事务执行
监视机制的触发:对数据库的修改命令,在执行之后会对watched_keys进行检查,如果有客户端正在监视被修改的键,那么会将对应客户端的REDIS_DIRTY_CAS标识打开,表示客户端的事务安全性已被破坏。
no-appendfsync-on-rewrite选项打开时,任何模式下的redis的事务都不具有持久性。

第20章 Lua脚本

伪客户端负责处理Lua脚本中包含的所有Redis命令。
Lua脚本使用redis.call函数或者redis.pcall函数执行一个Redis命令,需要完成以下步骤:
  1. Lua环境将redis.call函数或者redis.pcall函数想要执行的命令传给伪客户端。
  1. 伪客户端将脚本想要执行的命令传给命令执行器。
  1. 命令执行器执行伪客户端传给它的命令,并将命令的执行结果返回给伪客户端。
  1. 伪客户端接收命令执行器返回的命令结果,并将这个命令结果返回给Lua环境。
  1. Lua环境在接收到命令结果之后,将该结果返回给redis.call函数或者redis.pcall函数。
  1. 接收到结果的redis.call函数或者redis.pcall函数会将命令结果作为函数返回值返回给脚本中的调用者。
lua_scripts字典,键存储Lua脚本的sha1校验和,值存储对应的Lua脚本。
Lua环境为用户要执行的Lua脚本创建一个Lua函数,名字由f_前缀加上脚本的sha1校验和组成,函数体则是脚本本身。

第21章 排序

第22章 二进制位数组

Redis使用字符串对象来表示位数组。逆序保存。
二进制位统计算法
  • 遍历算法
  • 查表算法
  • variable-precision SWAR算法(计算汉明重量)
uint32_t swar(uint32_t i) { // step 1 i = (i & 0x55555555) + ((i >> 1) & 0x55555555); // step 2 i = (i & 0x33333333) + ((i >> 2) & 0x33333333); // step 3 i = (i & 0x0f0f0f0f) + ((i >> 4) & 0x0f0f0f0f); // step 4 i = (i * (0x01010101) >> 24); return i; }
Redis实现:二进制长度小于128时使用查表(键长8位),否则使用SWAR算法。

第23章 慢查询日志

slowlog-log-slower-than
slowlog-max-len

第24章 监视器

MONITOR命令会打开客户端的REDIS_MONITOR标志,并且这个客户端本身也会被添加到monitors链表的表尾。
badge