首页 / REDIS / Redis设计与实现
Redis设计与实现
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了Redis设计与实现,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含53066字,纯文字阅读大概需要76分钟。
内容图文
![Redis设计与实现](/upload/InfoBanner/zyjiaocheng/867/8bee587ab9c84c3794296f16f81b85f2.jpg)
title: Redis设计与实现(note)
date: 2020-09-30 14:27:00
categories:
- Redis
tags: - Redis
- 数据库
chapter2 简单动态字符串
2.1 SDS的定义
struct sdshdr{
// 记录buf数组中已使用字节的数量
// 等于SDS所保存字符串的长度(不含'\0')
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
}
2.2 SDS 与 C 字符串的区别
常数复杂度获取字符串长度
与C字符串不一样,SDS在被链接时可以通过结构中记录的len字段直接获取长度信息,防止连接字符串溢出时,也可以通过这一字段直接调整缓冲区长度
减少修改字符串时带来的内存重分配次数
to do 国庆期间带书去旅游没有电脑做笔记
chapter8 对象
8.1 对象的类型与编码
Redis中的每个对象都由一个redisObject结构表示,该结构中和保存数据有关的三个属性分别是type属性、encoding属性和ptr属性
typedef struct redisObject{
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层实现数据结构指针
void *ptr;
// ...
}robj;
类型
编码和底层实现
通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一定的编码,极大地提升了Redis的灵活性和效率,因为Redis可以根据不用的使用场景来为一个对象设置不同的编码,从而优化对象在某一场景下的效率
8.2 字符串对象
如果字符串对象保存的是一个字符串值,并且字符串值的长度大于39字节,那么字符串对象将使用简单动态字符串来保存这个字符串值
编码的转换
因为Redis没有为embstr编码的字符串编写任何的修改程序,所以对embstr编码的字符串对象实际上是只读的,当我们对embstr编码的字符串对象执行任何修改命令时,程序会先将对象的编码从embstr转换成raw,然后执行修改命令
字符串命令的实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7r8xIy69-1609686516233)(https://gitee.com/hsby/img/raw/master/20201008145101.png)]
8.3 列表对象
编码转换
当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码
-
列表对象的所有字符串元素的长度都效于64字节
-
列表对象保存的元素数量小于512个;
不能满足这两个条件的列表对象需要使用linkedlist编码
8.4 哈希对象
哈希对象的编码可以是ziplist 或者 hashtable
ziplist编码的哈希对象
hashtable编码的哈希对象
编码转换
当哈希对象可以同时满足一下两个条件时,哈希对象使用ziplist编码
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
- 哈希对象保存的键值对数量小于512个
不能满足这两个条件的哈希对象需要使用hashtable编码
哈希命令的实现
8.5 集合对象
集合对象的编码可以是intset 或者 hashtable
intset编码集合对象
hashtable编码集合对象
编码的转换
当集合对象可以同时满足一下两个条件时,对象使用intset编码:
-
集合对象保存的所有都是整数值
-
集合对象保存的元素数量不超过512个
当集合对象可以同时满足以下两个条件时,对象使用inteset编码
8.4 有序集合对象
有序集合的编码可以时ziplist 或者 skiplist
ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存
压缩列表内的集合元数按分支从小到大进行排序
编码的转换
当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码
- 有序集合保存的元素数量小于128个
- 有序集合保存的所有元素成员的长度都小于64字节
不能满足以上两个条件的有序集合集合将使用skiplist编码
有序集合命令的实现
8.7 类型检查与命令多态
多态命令的实现
我们可以将DEL、EXPIRE、TYPE等命令也称为多态命令,因为无论输入的键是什么类型,这些命令都可以正确地执行
8.8 内存回收
通过引用计数机制实现内存回收
typedef struct redisObject{
//...
// 引用计数
int refcount;
//...
}
8.9 对象共享
不共享包含字符串的对象,性能消耗高
8.10 对象的空转时长
chapter9 数据库
9.1 服务器中的数据库
Redis服务器将所有都保存在服务器状态redis.h/redisServer
结构中
struct redisServer{
//...
// 一个数组,保存着服务器中所有数据库
redisDb *db;
//...
}
struct redisServer{
//...
// 服务器的数据库数量
int dbnum;
//...
}
9.2 切换数据库
客户端状态redisClient结构的db属性记录了客户端当前的目标数据库
typedef struct redisClient{
//...
// 记录客户端当前正在使用的数据库
redisDB *db;
//...
}
9.3 数据库键空间
typedef struct redisDb{
//...
// 数据库键空间,保存着数据库中的所有键值对
dict *dict;
//...
}
9.4 设置键的生存时间
通过EXPIRE命令或者PEXPIRE命令设置一个秒或毫秒的过期时间
通过EXPIREAT或者PEXPIREAT命令设置一个秒或者毫秒的过期时间戳
保存过期时间
redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典
- 过期字典是一个指针,这个指针指向空间中某个键对象
- 过期字典的值是一个long long类型的整数,这个整数保存了键所指向的数据库键的过期时间——一个毫秒精度的UNIX时间戳
typedef struct redisDb{
//...
// 过期字典,保存着键的过期时间
dict *expires;
//...
}
移除过期时间
PERSIST命令可以移除一个键的过期时间
计算并返回剩余生存时间
9.7 AOF、FDB 和复制功能对过期键的处理
9.8 数据库通知
SUBSCRIBE __keyspace@0__:message
// 使客户端进入接收键空间通知模式
SUBSCRIBE __keyevent@0__:del
// 使客户端进入接收键事件通知模式
发送通知
9.9 重点回顾
chapter10 RDB持久化
10.1 RDB文件的创建和载入
有两个Redis命令可以用于生成RDB文件,SAVE和BGSAVE
SAVE阻塞服务器进程进行RDB文件的创建,BGSAVE则创建服务器子进程进行RDB文件的创建
因为AOF文件的耿信频率通常比RDB文件的更新频率高,所以
- 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态
- 只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态
10.2 自动间隔性保存
设置保存条件
服务器会为save选项设置默认条件:
save 900 1
save 300 10
save 60 10000
接着服务器根据save选下昂设置的条件设置服务器状态redisServer结构的saveparams属性:
struct redisServer{
//...
// 记录了保存条件的数组
struct saveparam *saveparams;
//...
};
struct saveparam{
// 秒数
time_t seconds;
// 修改数
int changes;
};
dirty计数器和lastsave属性
struct redisServer{
//...
// 修改计数器
long long dirty;
// 上一次执行保存的时间
time_t lastsave;
//...
};
检查保存条件是否满足
Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话就执行BGSAVE命令
10.3 RDB文件结构
datebases 部分
TYPE取值
value 的编码
1. 字符串对象
2. 列表对象
3. 集合对象
保存结构和列表对象相似
4. 哈希表对象
5. 有序集合对象
10.4 分析RDB文件
包含带有过期时间的字符串键的RDB文件
包含一个集合键的RDB文件
10.5 重点回顾
chapter11 AOF持久化
11.1 AOF持久化的实现
命令追加
当AOF持久化处于开启状态时,服务器执行完一个写命令之后,会以协议格式将被执行的写明了追加到服务器状态的aof_buf缓冲区
struct redisServers{
//...
// AOF缓冲区
sds aof_buf;
//...
}
AOF文件的写入与同步
11.2 AOF文件的载入
11.3 AOF重写
AOF文件重写的实现
AOF后台重写
? 当需要进行AOF重写时,服务器进程创建子进程和一个AOF重写缓冲区,避免与父进程数据库状态混淆,在此之后服务器会将接收到的写命令同时写入AOF缓冲区和AOF重写缓冲区,当子进程将此前状态重写到AOF文件完成后,发送信号给服务器进程,服务器进程的信号处理函数将AOF重写缓冲区的状态覆盖到AOF文件中
chapter12 事件
12.1 文件事件
Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件时间处理器:
- 文件时间处理器使用IO多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不用的事件处理器
- 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件时间就会产生,这是文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件
文件事件处理器的构成
IO多路复用程序将同时响应的套接字放在一个队列中,每次向文件事件分派器分派一个套接字
IO多路复用程序的实现
事件的类型
- 当套接字可读时,产生AE_READABLE事件
- 当套接字可写时,产生AE_WRITEABLE事件
- 当同一个套接字,可读可写时,先处理AE_READABLE事件,再处理AE_WRITEABLE事件
API
文件事件的处理器
12.2 时间事件
Redis 的事件事件分为以下两类:
- 定时事件:让一段程序在指定的时间之后执行一次。
- 周期性事件:让一段程序每隔指定时间就执行一次。
一个时间事件主要由以下三个属性组成:
- id:服务器为时间事件创建的全局唯一ID
- when:毫秒精度的UNIX时间戳
- timeProc:时间事件处理器,一个函数
实现
服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件
时间事件应用实例:serverCron函数
它的主要工作包括:
- 更新服务器的各类统计信息
- 清理数据库中的过期键值对
- 关闭和清理连接失效的客户端
- 尝试进行AOF 或 RDB持久化操作
- 如果服务器是主服务器
- 如果处于集群模式,对集群进行定期同步和连接测试
12.3 事件的调度与执行
- aeApiPoll函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,比秒服务器对时间事件进行频繁的轮询,也避免阻塞时间过长
- 因为文件时间是随机出现的,如果等待并处理完一次文件事件之后,仍未有任何时间事件到达,那么服务器继续等待,逐渐逼近时间事件
- 对文件事件和时间事件的处理都是同步、有序、原子地执行的
12.4 重点回顾
chapter13 客户端
服务器为每个客户端建立相应的redis.h/redisClient结构,这个结构保存了客户端当前的状态信息
- 客户端的套接字描述符
- 客户端的名字
- 客户端的标志值
- 只想客户端正在使用的数据库的指针,以及该数据库的号码
- 客户端当前要执行的命令、命令的参数、命令参数的个数,以及指向命令实现函数的指针
- 客户端的输入缓冲区和输出缓冲区
- 客户端的复制状态信息,以及进行符直所需的数据结构
- 客户端执行BRPOP、BLPOP等列表阻塞命令时使用的数据结构
- 客户端的事务状态,以及执行WATCH命令时用到的数据结构
- 客户端执行发布与订阅功能时用到的数据结构
- 客户端的身份验证标志
- 客户端的创建时间,客户端和服务器最后一次通信的时间,以及客户端的输出缓冲区大小超出软性限制的时间
Redis服务器状态结构的clients属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构
13.1 客户端属性
客户端状态包含的属性可以分为两类:
- 一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,它们都要用到这些属性。
- 另外一类是和特定功能相关的属性,比如操作数据结构是需要用到的db属性和dictid属性,执行事务时需要需要用到的mstate属性,以及执行WATCH命令时需要用到的watched_keys属性等等
套接字描述符
如果套接字描述符值为-1,说明这是一个伪客户端,主要用于载入AOF文件并还原数据库状态,或者用于执行Lua脚本中包含的Redis命令
一般的客户端描述符值都是大于-1的整数
名字
默认情况下客户端没有名字,可以通过CLIENT setname
命令设置,通过CLIENT list
查询
标志
客户端的标志属性flags记录了客户端的角色(role),以及客户端目前所处的状态:
- 在主从服务器进行复制操作时,主服务器会成为从服务器的客户端,而从服务器也成为主服务器的客户端。REDI_MASTER标志代表客户端是一个主服务器,REDIS_SLAVE标志表示客户端代表的是一个从服务器。
- REDIS_PRE_PSLAVE标志表示客户端代表的是一个版本低于Redis2.8 的从服务器,主服务器不能使用PSYNC命令与这个从服务器进行同步。这个标志只能在REDIS_SLAVE标志处于打开状态时使用。
- REDIS_LUA_CLIENT标识代表客户端时专门用于处理Lua脚本里面包含的Redis命令的伪客户端
- REDIS_MONITOR标志表示客户端正在执行MONITOR命令
- REDIS_UNIX_SOCKET标志表示服务器使用UNIX套接字连接客户端
- REDIS_BLOCKED标志表示客户端正在被BRPOP、BLPOP等命令阻塞
- REDIS_UNBLOCKED标志表示客户端已经从REDIS_BLOCKET标志所表示的阻塞状态脱离出来,只能在REDIS_BLOCKED标志已经打开时使用
- REDIS_MULTI标志表示客户端正在执行事务
- REDIS_DIRTY_CAS标志表示事务使用WATCH命令监视的数据库键已经被修改,REDIS_DIRTY_EXEC标志表示事务在命令入队时出错,以上两个标志都表示事务的安全性已经被破坏
- REDIS_CLOSE_ASAP标志表示客户端的输出缓冲区大小超出了服务器允许的范围,服务器会在下一次执行serverCron函数时关闭这个客户端,以免服务器的稳定性收到这个客户端影响
- REDIS_CLOSE_AFTER_REPLY标志表示有用户对这个客户端执行了CLENT KILL命令,或者客户端发送了错误的协议内容,服务器将把客户端输出缓冲区的内容发送,之后关闭客户端
- REDIS_ASKING 标志表示客户端向集群节点发送了ASKING命令
- REDIS_FORCE_AOF标志强制服务器将当前执行的命令写入到AOF文件里面,REDIS_FORCE_PEPL标志强制主服务器将当前执行的命令复制给所有从服务器,执行PUBUB命令会使客户端打开REDIS_FORCE_AOF标志,执行SCRIPT LOAD命令回事客户端打开REDIS_FORCE_AOF标志和REDIS_FORCE_REPL标志
- 在主从服务器进行命令传播期间,从服务器需要向主服务器发送PEPLICATION ACK命令,在发送之前从服务器需要打开主服务器对应客户端的REDIS_MASTER_FORCE_REPLY标志,否则发送将被拒绝
输入缓冲区
客户端状态的输入缓冲区用于保存客户端发送的命令请求:
它的大小最大大小不超过1GB,否则服务器将关闭这个客户端
命令和命令参数
chapter14 服务器
14.1 命令请求的执行过程
读取命令请求
命令执行器(1):查找命令实现
在命令表(command table)中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性里面
命令执行器(2):执行预备操作
14.2 serverCron函数
更新服务器时间缓存
struct redisServer{
//...
// 保存了秒级精度的系统当前UNIX时间戳
time_t unixtime;
// 保存了毫秒级精度的系统当前UNIX时间戳
long long mstime;
// 默认10秒更新一次的时间缓存, 用于计算键的空转时间
unsigned lruclock:22;
};
struct redisObject{
//...
// 对象最后一次被命令访问的时间
unsigned lru:22;
//...
};
更新服务器每秒执行命令次数
serverCron函数中的trackOperationsPerSecond函数会以每100毫秒一次的频率执行,这个函数的功能是以抽样计算的方式,估算并记录服务器在最近一秒处理的命令请求数量
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数组的索引值
// 每次抽样后将值自增1
// 在值等于16时重置为0
int ops_sec_idx;
//...
};
更新服务器内存峰值记录
struct redisServer{
//...
// 已使用内存峰值
size_t stat_peaak_memory;
//...
};
处理SIGTERM信号
管理客户端资源
serverCron函数每次执行都会调用clientsCron函数,clientsCron函数会对一定数量的客户端进行以下两个检查:
- 如果客户端与服务器之间的连接已经超时,那么程序释放这个客户端
- 如果客户端在上一次执行命令请求之后,输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区
管理数据库资源
serverCron函数每次执行都会调用databasesCron函数,这个函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时,对字典进行收缩操作
执行被延迟的BGREWRITEAOF
服务器的aof_rewrite_scheduled标识记录了服务器是否延迟了BGREWRITEAOF命令:
struct redisServer{
//...
//如果值为1, 那么标识有 BGREWRITEAOF 命令被延迟了
int aof_rewrite_scheduled;
//...
};
检查持久化操作的运行状态
服务器状态使用rdb_child_pid 属性和 aof_child_pid 属性记录执行BGSAVE命令和 BGREWRITEAOF 命令的子进程的ID
struct redisServer{
//...
// 记录执行BGSAVE命令的子进程的ID
// 如果服务器没有在执行BGSAVE
// 那么这个属性的值为-1
pid_t rdb_child_pid;
// 记录执行BGREWRITEAOF 命令的子进程的ID
// 如果服务器没有在执行BGREWRITEAOF
// 那么这个属性的值为-1
pid_t aof_child_pid;
//...
};
将AOF缓冲区中的内容写入AOF文件
关闭异步客户端
增加cronloops计数器的值
struct redisServer{
//...
// serverCron函数的运行次数计数器
int cronloops;
//...
};
14.3 初始化服务器
void initServerConfig(void) {
int j;
updateCachedTime(1);
// 设置服务器运行id
getRandomHexChars(server.runid,CONFIG_RUN_ID_SIZE);
// 设置默认服务器频率
server.hz = CONFIG_DEFAULT_HZ; /* Initialize it ASAP, even if it may get
updated later after loading the config.
This value may be used before the server
is initialized. */
server.timezone = getTimeZone(); /* Initialized by tzset(). */
// 设置默认配置文件路径
server.configfile = NULL;
// 设置服务器的运行架构
server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
//...
}
载入配置选项
初始化服务器数据结构
initServerConfig函数主要负责初始化一般属性
initServer函数主要负责初始化数据结构
除了初始化数据结构之外,initServer还进行了一些非常重要的设置操作,其中包括:
- 为服务器设置进程信号处理器
- 创建共享对象:这些对象包含Redis服务器经常用到的一些值,比如包含“OK”回复的自负床对象,包含整数1 到 10000 的字符串对象等等,服务器通过重用这些共享对象来避免反复创建相同的对象
- 打开服务器的监听端口,并为监听套接字关联连接应答事件处理器,等待服务器正式运行时接收客户端的连接
- 为serverCron函数创建时间事件,等待服务器正式运行时执行serverCron函数
- 如果AOF持久化功能已经打开,那么打开现有的AOF文件,如果AOF文件不存在,那么创建并打开一个新的AOF文件,为AAOF写入做好准备
- 初始化服务器的后台IO模块,为将来的IO操作做好准备
还原数据库状态
根据服务器是否启用了AOF持久化功能,服务器载入数据时所使用的目标文件会有所不同:
- 如果服务器启用了AOF持久化功能,那么服务器使用AOF文件来还原数据库状态
- 否则使用RDB文件来还原数据库状态
执行事件循环
chapter15 复制
15.1 旧版复制功能的实现
同步
命令传播
旧版复制功能的缺陷
15.3 新版复制功能的实现
Redis2.8开始,使用PSYNC命令替代SYNC命令来执行复制时的同步操作
PSYNC命令具有完整重同步和部分重同步两种模式:
- 完整重同步与SYNC执行步骤基本相同
- 部分重同步用于处理断线后重复制情况,当从服务器在断线后重新连接服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收执行这些命令,实现同步
15.4 部分重同步的实现
- 主服务器的复制偏移量和从服务器的复制偏移量
- 主服务器的复制积压缓冲区
- 服务器的运行ID
复制偏移量
执行复制的双方——主服务器和从服务器会分别维护一直复制偏移量:
- 主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值上+N
- 从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值+N
复制积压缓冲区
一个向从服务器发送数据的队列,如果发送过程中连接中断,重新连接就可以将队列的内容发送出去,实现重同步,它的大小可以参考每次断开时间*服务器每秒处理的写入请求数
服务器运行ID
从服务器可以通过对主服务器的运行ID进行验证,确认连接的服务器是否为原来的服务器
15.5 PSYNC命令的实现
15.6 复制的实现
步骤1:设置主服务器的地址和端口
从服务器的服务器状态中设置:
struct redisServer{
//...
// 主服务器的地址
char *masterhost;
// 主服务器的端口
int masterport;
//...
};
步骤2:建立套接字连接
步骤3:发送PING命令
- 检查套接字的读写状态
- 通过发送PING命令可以检查主服务器能否正常处理命令请求
- 如果主服务器返回一个错误,那么表示主服务器暂时没办法处理从服务器的命令请求,不能继续执行复制工作的后续步骤
- 如果从服务器读取到“PONG”回复,标识主服务器可以正常处理从服务器发送的命令请求
步骤4:身份验证
如果从服务器设置了masterauth选项,则进行身份验证
步骤5:发送端口信息
从服务器向执行命令REPLCONF listening-port <port-number>
主服务器发送端口号
主服务器在客户端状态
struct redisClient{
//...
// 从服务器的监听端口号
int slave_listening_port;
//...
};
步骤6:同步
在同步操作执行之后,主从服务器双方都是对方的客户端,它们可以互相向对方发送命令请求,或者返回回复
正因为主服务器成为了从服务器的客户端,所以主服务器才可以通过发送写命令来改变从服务器的数据库状态,不仅同步操作需要用到这一点,这也是主服务器对从服务器执行命令传播操作的基础
步骤7:命令传播
15.7 心跳检测
在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:
REPLCONF ACK<replication_offset>
对于主服务器有三个作用:
- 检测主从服务器的网络连接状态
- 付诸实现min-slaves选项
- 检测命令失效
检测主从服务器的网络连接状态
INFO replication
lag值显示了从服务器响应的时间,一般这个值在0-1之间
辅助实现min-slaves配置选项
Redis的min-slaces-to-write 和 min-slaces-max-lag 两个选项可以繁殖主服务器在不安全的情况下执行写命令
检测命令丢失
chapter16 Sentinel
16.1 启动并初始化Sentinel
初始化服务器
Sentinel本质上只是运行在特殊模式下的Redis服务器,启动第一步就是初始化一个普通的Redis服务器
使用Sentinel专用代码
使用redis.h/REDIS_SERVERPORT常量值作为服务器端口
使用redis.h/redisCommandTable作为服务器的命令表
// 服务器在 sentinel 模式下可执行的命令
struct redisCommand sentinelcmds[] = {
{"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
{"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
{"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
{"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
{"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
{"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
{"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
{"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
{"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0}
};
struct redisCommand {
// 命令名字
char *name;
// 实现函数
redisCommandProc *proc;
// 参数个数
int arity;
// 字符串表示的 FLAG
char *sflags; /* Flags as string representation, one char per flag. */
// 实际 FLAG
int flags; /* The actual flags, obtained from the 'sflags' field. */
/* Use a function to determine keys arguments in a command line.
* Used for Redis Cluster redirect. */
// 从命令中判断命令的键参数。在 Redis 集群转向时使用。
redisGetKeysProc *getkeys_proc;
/* What keys should be loaded in background when calling this command? */
// 指定哪些参数是 key
int firstkey; /* The first argument that's a key (0 = no keys) */
int lastkey; /* The last argument that's a key */
int keystep; /* The step between first and last key */
// 统计信息
// microseconds 记录了命令执行耗费的总毫微秒数
// calls 是命令被执行的总次数
long long microseconds, calls;
};
初始化Sentinel状态
/* Sentinel 的状态结构 */
struct sentinelState {
// 当前纪元
uint64_t current_epoch; /* Current epoch. */
// 保存了所有被这个 sentinel 监视的主服务器
// 字典的键是主服务器的名字
// 字典的值则是一个指向 sentinelRedisInstance 结构的指针
dict *masters; /* Dictionary of master sentinelRedisInstances.
Key is the instance name, value is the
sentinelRedisInstance structure pointer. */
// 是否进入了 TILT 模式?
int tilt; /* Are we in TILT mode? */
// 目前正在执行的脚本的数量
int running_scripts; /* Number of scripts in execution right now. */
// 进入 TILT 模式的时间
mstime_t tilt_start_time; /* When TITL started. */
// 最后一次执行时间处理器的时间
mstime_t previous_time; /* Last time we ran the time handler. */
// 一个 FIFO 队列,包含了所有需要执行的用户脚本
list *scripts_queue; /* Queue of user scripts to execute. */
} sentinel;
初始化Sentinel状态的masters属性
Sentinel状态中的master字典记录了所有被Sentinel监视的主服务器的相关信息其中:
- 字典的键是被监视主服务器的名字
- 而字典的值是被监视主服务器对应的sentinel.h/sentinelRedisInstance结构
/* A Sentinel Redis Instance object is monitoring. */
/* 每个被监视的 Redis 实例都会创建一个 sentinelRedisInstance 结构
* 而每个结构的 flags 值会是以下常量的一个或多个的并 */
// 实例是一个主服务器
#define SRI_MASTER (1<<0)
// 实例是一个从服务器
#define SRI_SLAVE (1<<1)
// 实例是一个 Sentinel
#define SRI_SENTINEL (1<<2)
// 实例已断线
#define SRI_DISCONNECTED (1<<3)
// 实例已处于 SDOWN 状态
#define SRI_S_DOWN (1<<4) /* Subjectively down (no quorum). */
// 实例已处于 ODOWN 状态
#define SRI_O_DOWN (1<<5) /* Objectively down (confirmed by others). */
// Sentinel 认为主服务器已下线
#define SRI_MASTER_DOWN (1<<6) /* A Sentinel with this flag set thinks that
its master is down. */
// 正在对主服务器进行故障迁移
#define SRI_FAILOVER_IN_PROGRESS (1<<7) /* Failover is in progress for
this master. */
// 实例是被选中的新主服务器(目前仍是从服务器)
#define SRI_PROMOTED (1<<8) /* Slave selected for promotion. */
// 向从服务器发送 SLAVEOF 命令,让它们转向复制新主服务器
#define SRI_RECONF_SENT (1<<9) /* SLAVEOF <newmaster> sent. */
// 从服务器正在与新主服务器进行同步
#define SRI_RECONF_INPROG (1<<10) /* Slave synchronization in progress. */
// 从服务器与新主服务器同步完毕,开始复制新主服务器
#define SRI_RECONF_DONE (1<<11) /* Slave synchronized with new master. */
// 对主服务器强制执行故障迁移操作
#define SRI_FORCE_FAILOVER (1<<12) /* Force failover with master up. */
// 已经对返回 -BUSY 的服务器发送 SCRIPT KILL 命令
#define SRI_SCRIPT_KILL_SENT (1<<13) /* SCRIPT KILL already sent on -BUSY */
// Sentinel 会为每个被监视的 Redis 实例创建相应的 sentinelRedisInstance 实例
// (被监视的实例可以是主服务器、从服务器、或者其他 Sentinel )
typedef struct sentinelRedisInstance {
// 位图,标识值,记录了实例的类型,以及该实例的当前状态
int flags; /* See SRI_... defines */
// 实例的名字
// 主服务器的名字由用户在配置文件中设置
// 从服务器以及 Sentinel 的名字由 Sentinel 自动设置
// 格式为 ip:port ,例如 "127.0.0.1:26379"
char *name; /* Master name from the point of view of this sentinel. */
// 实例的运行 ID
char *runid; /* run ID of this instance. */
// 配置纪元,用于实现故障转移
uint64_t config_epoch; /* Configuration epoch. */
// 实例的地址
sentinelAddr *addr; /* Master host. */
// 用于发送命令的异步连接
redisAsyncContext *cc; /* Hiredis context for commands. */
// 用于执行 SUBSCRIBE 命令、接收频道信息的异步连接
// 仅在实例为主服务器时使用
redisAsyncContext *pc; /* Hiredis context for Pub / Sub. */
// 已发送但尚未回复的命令数量
int pending_commands; /* Number of commands sent waiting for a reply. */
// cc 连接的创建时间
mstime_t cc_conn_time; /* cc connection time. */
// pc 连接的创建时间
mstime_t pc_conn_time; /* pc connection time. */
// 最后一次从这个实例接收信息的时间
mstime_t pc_last_activity; /* Last time we received any message. */
// 实例最后一次返回正确的 PING 命令回复的时间
mstime_t last_avail_time; /* Last time the instance replied to ping with
a reply we consider valid. */
// 实例最后一次发送 PING 命令的时间
mstime_t last_ping_time; /* Last time a pending ping was sent in the
context of the current command connection
with the instance. 0 if still not sent or
if pong already received. */
// 实例最后一次返回 PING 命令的时间,无论内容正确与否
mstime_t last_pong_time; /* Last time the instance replied to ping,
whatever the reply was. That's used to check
if the link is idle and must be reconnected. */
// 最后一次向频道发送问候信息的时间
// 只在当前实例为 sentinel 时使用
mstime_t last_pub_time; /* Last time we sent hello via Pub/Sub. */
// 最后一次接收到这个 sentinel 发来的问候信息的时间
// 只在当前实例为 sentinel 时使用
mstime_t last_hello_time; /* Only used if SRI_SENTINEL is set. Last time
we received a hello from this Sentinel
via Pub/Sub. */
// 最后一次回复 SENTINEL is-master-down-by-addr 命令的时间
// 只在当前实例为 sentinel 时使用
mstime_t last_master_down_reply_time; /* Time of last reply to
SENTINEL is-master-down command. */
// 实例被判断为 SDOWN 状态的时间
mstime_t s_down_since_time; /* Subjectively down since time. */
// 实例被判断为 ODOWN 状态的时间
mstime_t o_down_since_time; /* Objectively down since time. */
// SENTINEL down-after-milliseconds 选项所设定的值
// 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)
mstime_t down_after_period; /* Consider it down after that period. */
// 从实例获取 INFO 命令的回复的时间
mstime_t info_refresh; /* Time at which we received INFO output from it. */
/* Role and the first time we observed it.
* This is useful in order to delay replacing what the instance reports
* with our own configuration. We need to always wait some time in order
* to give a chance to the leader to report the new configuration before
* we do silly things. */
// 实例的角色
int role_reported;
// 角色的更新时间
mstime_t role_reported_time;
// 最后一次从服务器的主服务器地址变更的时间
mstime_t slave_conf_change_time; /* Last time slave master addr changed. */
/* Master specific. */
/* 主服务器实例特有的属性 -------------------------------------------------------------*/
// 其他同样监控这个主服务器的所有 sentinel
dict *sentinels; /* Other sentinels monitoring the same master. */
// 如果这个实例代表的是一个主服务器
// 那么这个字典保存着主服务器属下的从服务器
// 字典的键是从服务器的名字,字典的值是从服务器对应的 sentinelRedisInstance 结构
dict *slaves; /* Slaves for this master instance. */
// SENTINEL monitor <master-name> <IP> <port> <quorum> 选项中的 quorum 参数
// 判断这个实例为客观下线(objectively down)所需的支持投票数量
int quorum; /* Number of sentinels that need to agree on failure. */
// SENTINEL parallel-syncs <master-name> <number> 选项的值
// 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
int parallel_syncs; /* How many slaves to reconfigure at same time. */
// 连接主服务器和从服务器所需的密码
char *auth_pass; /* Password to use for AUTH against master & slaves. */
/* Slave specific. */
/* 从服务器实例特有的属性 -------------------------------------------------------------*/
// 主从服务器连接断开的时间
mstime_t master_link_down_time; /* Slave replication link down time. */
// 从服务器优先级
int slave_priority; /* Slave priority according to its INFO output. */
// 执行故障转移操作时,从服务器发送 SLAVEOF <new-master> 命令的时间
mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF <new> */
// 主服务器的实例(在本实例为从服务器时使用)
struct sentinelRedisInstance *master; /* Master instance if it's slave. */
// INFO 命令的回复中记录的主服务器 IP
char *slave_master_host; /* Master host as reported by INFO */
// INFO 命令的回复中记录的主服务器端口号
int slave_master_port; /* Master port as reported by INFO */
// INFO 命令的回复中记录的主从服务器连接状态
int slave_master_link_status; /* Master link status as reported by INFO */
// 从服务器的复制偏移量
unsigned long long slave_repl_offset; /* Slave replication offset. */
/* Failover */
/* 故障转移相关属性 -------------------------------------------------------------------*/
// 如果这是一个主服务器实例,那么 leader 将是负责进行故障转移的 Sentinel 的运行 ID 。
// 如果这是一个 Sentinel 实例,那么 leader 就是被选举出来的领头 Sentinel 。
// 这个域只在 Sentinel 实例的 flags 属性的 SRI_MASTER_DOWN 标志处于打开状态时才有效。
char *leader; /* If this is a master instance, this is the runid of
the Sentinel that should perform the failover. If
this is a Sentinel, this is the runid of the Sentinel
that this Sentinel voted as leader. */
// 领头的纪元
uint64_t leader_epoch; /* Epoch of the 'leader' field. */
// 当前执行中的故障转移的纪元
uint64_t failover_epoch; /* Epoch of the currently started failover. */
// 故障转移操作的当前状态
int failover_state; /* See SENTINEL_FAILOVER_STATE_* defines. */
// 状态改变的时间
mstime_t failover_state_change_time;
// 最后一次进行故障迁移的时间
mstime_t failover_start_time; /* Last failover attempt start time. */
// SENTINEL failover-timeout <master-name> <ms> 选项的值
// 刷新故障迁移状态的最大时限
mstime_t failover_timeout; /* Max time to refresh failover state. */
mstime_t failover_delay_logged; /* For what failover_start_time value we
logged the failover delay. */
// 指向被提升为新主服务器的从服务器的指针
struct sentinelRedisInstance *promoted_slave; /* Promoted slave instance. */
/* Scripts executed to notify admin or reconfigure clients: when they
* are set to NULL no script is executed. */
// 一个文件路径,保存着 WARNING 级别的事件发生时执行的,
// 用于通知管理员的脚本的地址
char *notification_script;
// 一个文件路径,保存着故障转移执行之前、之后、或者被中止时,
// 需要执行的脚本的地址
char *client_reconfig_script;
} sentinelRedisInstance;
16.2 获取服务器信息
Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息
通过分析主服务器回复的信息,填充主服务器实例结构中的slaves字典,而字典的键为从服务器的名字(addr:port), 值为从服务器的实例结构,如果从服务器的值已经存在,那么更新从服务器实例结构
16.3 获取从服务器信息
在创建命令连接之后,Sentinel默认情况下会以每十秒一次的频率通过命令连接向从服务器发送INFO命令,并获得类似以下内容的回复:
16.4 向主服务器和从服务器发送信息
在默认情况下,Sentinel会以每2秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送以下格式的命令:
PUBLISH __sentinel__:hello "<s_ip>, <s_port>, <s_runid>, <s_epoch>, <m_name>, <m_ip>, <m_prot>, <m_epoch>"
int sentinelSendHello(sentinelRedisInstance *ri) {
char ip[REDIS_IP_STR_LEN];
char payload[REDIS_IP_STR_LEN+1024];
int retval;
// 如果实例是主服务器,那么使用此实例的信息
// 如果实例是从服务器,那么使用这个从服务器的主服务器的信息
sentinelRedisInstance *master = (ri->flags & SRI_MASTER) ? ri : ri->master;
// 获取地址信息
sentinelAddr *master_addr = sentinelGetCurrentMasterAddress(master);
/* Try to obtain our own IP address. */
// 获取实例自身的地址
if (anetSockName(ri->cc->c.fd,ip,sizeof(ip),NULL) == -1) return REDIS_ERR;
if (ri->flags & SRI_DISCONNECTED) return REDIS_ERR;
/* Format and send the Hello message. */
// 格式化信息
snprintf(payload,sizeof(payload),
"%s,%d,%s,%llu," /* Info about this sentinel. */
"%s,%s,%d,%llu", /* Info about current master. */
ip, server.port, server.runid,
(unsigned long long) sentinel.current_epoch,
/* --- */
master->name,master_addr->ip,master_addr->port,
(unsigned long long) master->config_epoch);
// 发送信息
retval = redisAsyncCommand(ri->cc,
sentinelPublishReplyCallback, NULL, "PUBLISH %s %s",
SENTINEL_HELLO_CHANNEL,payload);
if (retval != REDIS_OK) return REDIS_ERR;
ri->pending_commands++;
return REDIS_OK;
}
16.5 接收来自主服务器和从服务器的频道消息
当一个Sentinel从__sentinel__:hello
频道收到一条信息时,Sentinel会对这条信息进行分析,提取出信息中的八个参数,根据这些参数对主服务器的实例结构进行更新
更新sentinals字典
chapter17 集群
17.1 节点
启动节点
Redis服务器启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式
节点会继续使用redisServer结构来保存服务器的状态,使用redisClient结构来保存客户端的状态,至于那些集群模式下才会用到的数据结构,节点将它们保存到了cluster.h/clusterNode结构、cluster.h/clusterLink结构,以及cluster.h/clusterState结构里面
// 节点状态
struct clusterNode {
// 创建节点的时间
mstime_t ctime; /* Node object creation time. */
// 节点的名字,由 40 个十六进制字符组成
// 例如 68eef66df23420a5862208ef5b1a7005b806f2ff
char name[REDIS_CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
// 节点标识
// 使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
// 以及节点目前所处的状态(比如在线或者下线)。
int flags; /* REDIS_NODE_... */
// 节点当前的配置纪元,用于实现故障转移
uint64_t configEpoch; /* Last configEpoch observed for this node */
// 由这个节点负责处理的槽
// 一共有 REDIS_CLUSTER_SLOTS / 8 个字节长
// 每个字节的每个位记录了一个槽的保存状态
// 位的值为 1 表示槽正由本节点处理,值为 0 则表示槽并非本节点处理
// 比如 slots[0] 的第一个位保存了槽 0 的保存情况
// slots[0] 的第二个位保存了槽 1 的保存情况,以此类推
unsigned char slots[REDIS_CLUSTER_SLOTS/8]; /* slots handled by this node */
// 该节点负责处理的槽数量
int numslots; /* Number of slots handled by this node */
// 如果本节点是主节点,那么用这个属性记录从节点的数量
int numslaves; /* Number of slave nodes, if this is a master */
// 指针数组,指向各个从节点
struct clusterNode **slaves; /* pointers to slave nodes */
// 如果这是一个从节点,那么指向主节点
struct clusterNode *slaveof; /* pointer to the master node */
// 最后一次发送 PING 命令的时间
mstime_t ping_sent; /* Unix time we sent latest ping */
// 最后一次接收 PONG 回复的时间戳
mstime_t pong_received; /* Unix time we received the pong */
// 最后一次被设置为 FAIL 状态的时间
mstime_t fail_time; /* Unix time when FAIL flag was set */
// 最后一次给某个从节点投票的时间
mstime_t voted_time; /* Last time we voted for a slave of this master */
// 最后一次从这个节点接收到复制偏移量的时间
mstime_t repl_offset_time; /* Unix time we received offset for this node */
// 这个节点的复制偏移量
long long repl_offset; /* Last known repl offset for this node. */
// 节点的 IP 地址
char ip[REDIS_IP_STR_LEN]; /* Latest known IP address of this node */
// 节点的端口号
int port; /* Latest known port of this node */
// 保存连接节点所需的有关信息
clusterLink *link; /* TCP/IP link with this node */
// 一个链表,记录了所有其他节点对该节点的下线报告
list *fail_reports; /* List of nodes signaling this as failing */
};
typedef struct clusterNode clusterNode;
/* clusterLink encapsulates everything needed to talk with a remote node. */
// clusterLink 包含了与其他节点进行通讯所需的全部信息
typedef struct clusterLink {
// 连接的创建时间
mstime_t ctime; /* Link creation time */
// TCP 套接字描述符
int fd; /* TCP socket file descriptor */
// 输出缓冲区,保存着等待发送给其他节点的消息(message)。
sds sndbuf; /* Packet send buffer */
// 输入缓冲区,保存着从其他节点接收到的消息。
sds rcvbuf; /* Packet reception buffer */
// 与这个连接相关联的节点,如果没有的话就为 NULL
struct clusterNode *node; /* Node related to this link if any, or NULL */
} clusterLink;
// 集群状态,每个节点都保存着一个这样的状态,记录了它们眼中的集群的样子。
// 另外,虽然这个结构主要用于记录集群的属性,但是为了节约资源,
// 有些与节点有关的属性,比如 slots_to_keys 、 failover_auth_count
// 也被放到了这个结构里面。
typedef struct clusterState {
// 指向当前节点的指针
clusterNode *myself; /* This node */
// 集群当前的配置纪元,用于实现故障转移
uint64_t currentEpoch;
// 集群当前的状态:是在线还是下线
int state; /* REDIS_CLUSTER_OK, REDIS_CLUSTER_FAIL, ... */
// 集群中至少处理着一个槽的节点的数量。
int size; /* Num of master nodes with at least one slot */
// 集群节点名单(包括 myself 节点)
// 字典的键为节点的名字,字典的值为 clusterNode 结构
dict *nodes; /* Hash table of name -> clusterNode structures */
// 节点黑名单,用于 CLUSTER FORGET 命令
// 防止被 FORGET 的命令重新被添加到集群里面
// (不过现在似乎没有在使用的样子,已废弃?还是尚未实现?)
dict *nodes_black_list; /* Nodes we don't re-add for a few seconds. */
// 记录要从当前节点迁移到目标节点的槽,以及迁移的目标节点
// migrating_slots_to[i] = NULL 表示槽 i 未被迁移
// migrating_slots_to[i] = clusterNode_A 表示槽 i 要从本节点迁移至节点 A
clusterNode *migrating_slots_to[REDIS_CLUSTER_SLOTS];
// 记录要从源节点迁移到本节点的槽,以及进行迁移的源节点
// importing_slots_from[i] = NULL 表示槽 i 未进行导入
// importing_slots_from[i] = clusterNode_A 表示正从节点 A 中导入槽 i
clusterNode *importing_slots_from[REDIS_CLUSTER_SLOTS];
// 负责处理各个槽的节点
// 例如 slots[i] = clusterNode_A 表示槽 i 由节点 A 处理
clusterNode *slots[REDIS_CLUSTER_SLOTS];
// 跳跃表,表中以槽作为分值,键作为成员,对槽进行有序排序
// 当需要对某些槽进行区间(range)操作时,这个跳跃表可以提供方便
// 具体操作定义在 db.c 里面
zskiplist *slots_to_keys;
/* The following fields are used to take the slave state on elections. */
// 以下这些域被用于进行故障转移选举
// 上次执行选举或者下次执行选举的时间
mstime_t failover_auth_time; /* Time of previous or next election. */
// 节点获得的投票数量
int failover_auth_count; /* Number of votes received so far. */
// 如果值为 1 ,表示本节点已经向其他节点发送了投票请求
int failover_auth_sent; /* True if we already asked for votes. */
int failover_auth_rank; /* This slave rank for current auth request. */
uint64_t failover_auth_epoch; /* Epoch of the current election. */
/* Manual failover state in common. */
/* 共用的手动故障转移状态 */
// 手动故障转移执行的时间限制
mstime_t mf_end; /* Manual failover time limit (ms unixtime).
It is zero if there is no MF in progress. */
/* Manual failover state of master. */
/* 主服务器的手动故障转移状态 */
clusterNode *mf_slave; /* Slave performing the manual failover. */
/* Manual failover state of slave. */
/* 从服务器的手动故障转移状态 */
long long mf_master_offset; /* Master offset the slave needs to start MF
or zero if stil not received. */
// 指示手动故障转移是否可以开始的标志值
// 值为非 0 时表示各个主服务器可以开始投票
int mf_can_start; /* If non-zero signal that the manual failover
can start requesting masters vote. */
/* The followign fields are uesd by masters to take state on elections. */
/* 以下这些域由主服务器使用,用于记录选举时的状态 */
// 集群最后一次进行投票的纪元
uint64_t lastVoteEpoch; /* Epoch of the last vote granted. */
// 在进入下个事件循环之前要做的事情,以各个 flag 来记录
int todo_before_sleep; /* Things to do in clusterBeforeSleep(). */
// 通过 cluster 连接发送的消息数量
long long stats_bus_messages_sent; /* Num of msg sent via cluster bus. */
// 通过 cluster 接收到的消息数量
long long stats_bus_messages_received; /* Num of msg rcvd via cluster bus.*/
} clusterState;
CLUSTER MEET 命令的实现
17.2 槽指派
Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot)
数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽
当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)
记录节点的指派信息
struct clusterNode{
//...
unsigned char slots[16384/8];// 用于刻画节点保存状态的位图,一共16384个位
int numclots;
//...
};
传播节点的槽指派信息
记录集群中所有槽的指派信息
struct clusterState{
//...
clusterNode *slots[16384];
//...
};
CLUSTER ADDSLOTS命令的实现
17.3 在集群中执行命令
计算键属于哪个槽
def slot_number(key):
return CRC16(key) : 16383
验证clusterState.clots[slot_number(key)]是否等于clusterState.myself
- 如果相等,说明该槽点由本节点负责,直接执行key对应命令
- 如果不相等,取出clusterState.clots[slot_number(key)]的clusterNode结构中的ip和port, 向客户端返回MOVED <slot> <ip> <port>错误,指引节点指向正在负责处理key的节点
MOVED 错误
一个集群客户端通常会与集群中的多个节点创建套接字,而所谓的节点转向实际上就是换一个套接字来发送命令
节点数据库的实现
typedef struct clusterState{
//...
// 跳跃表的分值为槽点值,跳跃表的键为键值对的键
zskiplist *slots_to_keys;
//...
}
17.4 重新分片
17.5 ASK错误
17.6 复制与故障转移
设置从节点
向一个节点发送命令CLUSTER REPLICATE <note_id>
struct clusterNode{
//...
// 如果这是一个从节点,那么指向主节点
struct clusterNode *slaveof;
//...
};
故障检测
故障转移
选举新的主节点
17.7 消息
- MEET消息,发送者接到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入发送者当前的集群
- PING消息,集群的每个节点每隔一秒就会从已知节点列表选出5个节点,然后对这5个节点最长时间没有发送过PING消息的节点发送PING消息,对距离上次收到PONG消息时间超过cluster-node-timeout选项设置时长的一半的节点也会发送PING消息
- PONG消息,向发送者确认这条MEET消息或者PING消息已到达,接收者会向发送者返回一条PONG消息,一个节点可以通过发送PONG消息通知其它节点更新对本节点的认知
- FALL消息,当一个节点A判断另一个主节点B已经进入FALL状态时,节点A会向集群广播一条关于节点B的FALL消息,所有收到这条消息的节点都会立即将节点B标记为已下线
- PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令
消息头
// 用来表示集群消息的结构(消息头,header)
typedef struct {
char sig[4]; /* Siganture "RCmb" (Redis Cluster message bus). */
// 消息的长度(包括这个消息头的长度和消息正文的长度)
uint32_t totlen; /* Total length of this message */
uint16_t ver; /* Protocol version, currently set to 0. */
uint16_t notused0; /* 2 bytes not used. */
// 消息的类型
uint16_t type; /* Message type */
// 消息正文包含的节点信息数量
// 只在发送 MEET 、 PING 和 PONG 这三种 Gossip 协议消息时使用
uint16_t count; /* Only used for some kind of messages. */
// 消息发送者的配置纪元
uint64_t currentEpoch; /* The epoch accordingly to the sending node. */
// 如果消息发送者是一个主节点,那么这里记录的是消息发送者的配置纪元
// 如果消息发送者是一个从节点,那么这里记录的是消息发送者正在复制的主节点的配置纪元
uint64_t configEpoch; /* The config epoch if it's a master, or the last
epoch advertised by its master if it is a
slave. */
// 节点的复制偏移量
uint64_t offset; /* Master replication offset if node is a master or
processed replication offset if node is a slave. */
// 消息发送者的名字(ID)
char sender[REDIS_CLUSTER_NAMELEN]; /* Name of the sender node */
// 消息发送者目前的槽指派信息
unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
// 如果消息发送者是一个从节点,那么这里记录的是消息发送者正在复制的主节点的名字
// 如果消息发送者是一个主节点,那么这里记录的是 REDIS_NODE_NULL_NAME
// (一个 40 字节长,值全为 0 的字节数组)
char slaveof[REDIS_CLUSTER_NAMELEN];
char notused1[32]; /* 32 bytes reserved for future usage. */
// 消息发送者的端口号
uint16_t port; /* Sender TCP base port */
// 消息发送者的标识值
uint16_t flags; /* Sender node flags */
// 消息发送者所处集群的状态
unsigned char state; /* Cluster state from the POV of the sender */
// 消息标志
unsigned char mflags[3]; /* Message flags: CLUSTERMSG_FLAG[012]_... */
// 消息的正文(或者说,内容)
union clusterMsgData data;
} clusterMsg;
MEET、PING、PONG 消息的实现
union clusterMsgData {
/* PING, MEET and PONG */
struct {
/* Array of N clusterMsgDataGossip structures */
// 每条消息都包含两个 clusterMsgDataGossip 结构
clusterMsgDataGossip gossip[1];
} ping;
/* FAIL */
struct {
clusterMsgDataFail about;
} fail;
/* PUBLISH */
struct {
clusterMsgDataPublish msg;
} publish;
/* UPDATE */
struct {
clusterMsgDataUpdate nodecfg;
} update;
};
FAIL消息的实现
typedef struct {
// 下线节点的名字
char nodename[REDIS_CLUSTER_NAMELEN];
} clusterMsgDataFail;
集群中的节点通过发送消息来将一个节点标记为下线的过程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TaV854nv-1609686516341)(http://1e-gallery.redisbook.com/_images/graphviz-74e2b6c26627aedbdd10295cfddd6bf6ee29d9ac.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pbb9OOcH-1609686516362)(http://1e-gallery.redisbook.com/_images/graphviz-e2e030e0a517bddc4c8c542b6876efeed2469941.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7wTPrYOB-1609686516363)(http://1e-gallery.redisbook.com/_images/graphviz-c4408a7984bbcfdb233e87165044e609a7846ae2.png)]
PUBLISH 消息的实现
当客户端向集群中的某个节点发送命令:
PUBLISH <channel> <message>
typedef struct {
// 频道名长度
uint32_t channel_len;
// 消息长度
uint32_t message_len;
// 消息内容,格式为 频道名+消息
// bulk_data[0:channel_len-1] 为频道名
// bulk_data[channel_len:channel_len+message_len-1] 为消息
unsigned char bulk_data[8]; /* defined as 8 just for alignment concerns. */
} clusterMsgDataPublish;
17.8 重点回顾
chapter18 发布与订阅
客户端订阅频道。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FXVdjJVP-1609686516367)(http://1e-gallery.redisbook.com/_images/graphviz-25889daa26b8ba11043c7f08f5c96b6a0d7e1173.png)]
客户端向频道发送消息, 消息被传递至各个订阅者。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tSvHQhlM-1609686516369)(http://1e-gallery.redisbook.com/_images/graphviz-0c8815c9d27b05c73c8c67c5e8eb59c6cf5e2e3a.png)]
匹配模式
客户端订阅模式。
客户端向频道发送消息, 消息被传递给正在订阅匹配模式的订阅者。
另一个模式被匹配的例子。
18.1 频道的订阅与退订
struct redisServer{
/* Pubsub */
// 字典,键为频道,值为链表
// 链表中保存了所有订阅某个频道的客户端
// 新客户端总是被添加到链表的表尾
dict *pubsub_channels; /* Map channels to list of subscribed clients */
};
订阅频道
每当客户端执行SUBSCRIBE命令订阅某个或某些频道的时候,服务器都会将客户端与被订阅的频道再pubsub_cannels字典进行关联
退订频道
18.2 模式的订阅与退订
struct redisServer{
// 链表,包含多个 pubsubPattern 结构
// 记录了所有订阅频道的客户端的信息
// 新 pubsubPattern 结构总是被添加到表尾
list *pubsub_patterns; /* patterns a client is interested in (SUBSCRIBE) */
};
/*
* 记录订阅模式的结构
*/
typedef struct pubsubPattern {
// 订阅模式的客户端
redisClient *client;
// 被订阅的模式
robj *pattern;
} pubsubPattern;
订阅模式
退订模式
18.3 发送消息
18.4 查看订阅信息
PUBSB CHANNELS
返回服务器当前被订阅的频道
PUBSUB NUMSUB
PUBSUB NUMSUB [channel-1 channel-2…channel-n] 子命令接受任意多个频道作为输入参数,并返回这些频道的订阅者数量
PUBSUB NUMPAT
PUBSUB NUMPAT 子命令用于返回服务器当前被订阅模式的数量
18.5 重点回顾
chapter19 事务
Redis通过MULTI、EXEC、WATCH等命令来实现事务功能
19.1 事务的实现
事务开始
redis> MULTI
ok
通过切换客户端状态的flag属性的REDIS_MULTI标识来完成
命令入队
事务队列
struct redisServer{
// 事务状态
multiState mstate; /* MULTI/EXEC state */
};
/*
* 事务命令
*/
typedef struct multiCmd {
// 参数
robj **argv;
// 参数数量
int argc;
// 命令指针
struct redisCommand *cmd;
} multiCmd;
/*
* 事务状态
*/
typedef struct multiState {
// 事务队列,FIFO 顺序
multiCmd *commands; /* Array of MULTI commands */
// 已入队命令计数
int count; /* Total number of MULTI commands */
int minreplicas; /* MINREPLICAS for synchronous replication */
time_t minreplicas_timeout; /* MINREPLICAS timeout as unixtime. */
} multiState;
执行事务
19.2 WATCH 命令的实现
WATCH 命令是一个乐观锁吗,它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复
使用WATCH命令监视数据库键
struct redisDb{
// 键正在被 WATCH 命令监视的键,值为客户端链表
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
};
监视机制的触发
所有对数据库进行修改的命令在执行前都会调用multi.c/touchWatchKey函数对wached_keys字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有的话,那么函数会将监视修改键的客户端REDIS_DIRTY_CAS标识打开,标识该客户端的事务安全性已经被破坏。
touchWatchKey函数的定义可以用以下伪代码来描述:
判断事务是否安全
当客户端接收到一个客户端发来的EXEC命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务
一个完整的WATCH事务执行过程
19.3 事务的 ACID 性质
在REDIS中,事务总具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),当Redis运行在某种特定的持久化模式下时,事务也具有耐久性(Durability)
原子性
一致性
chapter20 Lua脚本
Redis从2.6版本开始引入对Lua脚本的支持,通过在服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务器端原子地执行多个Redis命令
20.1 创建并修改Lua环境
创建Lua环境
载入函数库
创建redis全局表格
20.2 Lua环境写作组件
伪客户端
lua_scripts字典
struct redisServer{
/* Scripting */
// Lua 环境
lua_State *lua; /* The Lua interpreter. We use just one for all clients */
// 复制执行 Lua 脚本中的 Redis 命令的伪客户端
redisClient *lua_client; /* The "fake client" to query Redis from Lua */
// 当前正在执行 EVAL 命令的客户端,如果没有就是 NULL
redisClient *lua_caller; /* The client running EVAL right now, or NULL */
// 一个字典,值为 Lua 脚本,键为脚本的 SHA1 校验和
dict *lua_scripts; /* A dictionary of SHA1 -> Lua scripts */
// Lua 脚本的执行时限
mstime_t lua_time_limit; /* Script timeout in milliseconds */
// 脚本开始执行的时间
mstime_t lua_time_start; /* Start time of script, milliseconds time */
// 脚本是否执行过写命令
int lua_write_dirty; /* True if a write command was called during the
execution of the current script. */
// 脚本是否执行过带有随机性质的命令
int lua_random_dirty; /* True if a random command was called during the
execution of the current script. */
// 脚本是否超时
int lua_timedout; /* True if we reached the time limit for script
execution. */
// 是否要杀死脚本
int lua_kill; /* Kill the script if true. */
};
20.3 EVAL命令的实现
定义脚本函数
当客户端向服务器发送EVAL命令,要求执行某个Lua脚本的时候,服务器首先要做的就是在Lua环境中,为传入的脚本定义一个与脚本相对应的Lua函数,其中,Lua函数的名字由f_前缀加上脚本的SHA1校验和(40字符长)组成,而函数的体则是脚本本身
执行脚本函数
[todo]
chapter21 排序
21.1 SORT<key> 命令的实现
// 用于保存被排序值及其权重的结构
typedef struct _redisSortObject {
// 被排序键的值
robj *obj;
// 权重
union {
// 排序数字值时使用
double score;
// 排序字符串时使用
robj *cmpobj;
} u;
} redisSortObject;
21.5 带有ALPHA选项的BY选项的实现
服务器执行 SORT fruits BY *-id ALPHA
时创建的数据结构。
21.6 LIMIT 选项
LIMIT <offset> <count>
返回已排序数组以offset作为起始索引向后count个元素
SORT alphavet ALPHA LIMIT 2 3
返回:“c” “d” “e”
21.7 GET 选项
21.8 STORE 选项的实现
将排序结果依次推入选项值指定的新键中
21.9 多个选项的执行顺序
chapter22 二进制位数组
22.4 BITCOUNT命令的实现
遍历算法
查表算法
variable-precision SWAP算法
chapter23 慢查询日志
Redis的慢查询日志功能用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志来监视和优化查询速度
服务器配置有两个和慢查询相关的选项:
- slowlog-log-slower-than选项指定执行时间超过多少个微秒的命令请求会被记录到日志上
- slowlog-max-len 选项指定服务器最多保存多少条慢查询日志
23.1 慢查询记录的保存
struct redisServer{
/* slowlog */
// 保存了所有慢查询日志的链表
list *slowlog; /* SLOWLOG list of commands */
// 下一条慢查询日志的 ID
long long slowlog_entry_id; /* SLOWLOG current entry ID */
// 服务器配置 slowlog-log-slower-than 选项的值
long long slowlog_log_slower_than; /* SLOWLOG time limit (to get logged) */
// 服务器配置 slowlog-max-len 选项的值
unsigned long slowlog_max_len; /* SLOWLOG max number of items logged */
};
/*
* 慢查询日志
*/
typedef struct slowlogEntry {
// 命令与命令参数
robj **argv;
// 命令与命令参数的数量
int argc;
// 唯一标识符
long long id; /* Unique entry identifier. */
// 执行命令消耗的时间,以微秒为单位
// 注释里说的 nanoseconds 是错误的
long long duration; /* Time spent by the query, in nanoseconds. */
// 命令执行时的时间,格式为 UNIX 时间戳
time_t time; /* Unix time at which the query was executed. */
} slowlogEntry;
服务器状态的 slowlog
属性。
23.2 慢查询日志的阅览和删除
23.3 添加新日志
chapter24 监视器
24.1 成为监视器
客户端执行命令请求:redis> MONITOR
客户端状态的flags字段会被设置
client.flags |= REDIS_MONITOR
服务器状态的monitors字段会被追加该客户端
server.monitors.append(client)
24.2 向监视器发送命令信息
[todo]
chapter21 排序
21.1 SORT<key> 命令的实现
// 用于保存被排序值及其权重的结构
typedef struct _redisSortObject {
// 被排序键的值
robj *obj;
// 权重
union {
// 排序数字值时使用
double score;
// 排序字符串时使用
robj *cmpobj;
} u;
} redisSortObject;
[外链图片转存中…(img-fjf5pjNg-1609686516392)]
21.5 带有ALPHA选项的BY选项的实现
服务器执行 SORT fruits BY *-id ALPHA
时创建的数据结构。
[外链图片转存中…(img-qOm5YqHc-1609686516394)]
[外链图片转存中…(img-XmTJjrbT-1609686516396)]
[外链图片转存中…(img-NRI4DtUr-1609686516397)]
21.6 LIMIT 选项
LIMIT <offset> <count>
返回已排序数组以offset作为起始索引向后count个元素
[外链图片转存中…(img-ussCbXWx-1609686516399)]
SORT alphavet ALPHA LIMIT 2 3
返回:“c” “d” “e”
21.7 GET 选项
[外链图片转存中…(img-h5aOMX95-1609686516400)]
21.8 STORE 选项的实现
将排序结果依次推入选项值指定的新键中
21.9 多个选项的执行顺序
chapter22 二进制位数组
22.4 BITCOUNT命令的实现
遍历算法
查表算法
variable-precision SWAP算法
chapter23 慢查询日志
Redis的慢查询日志功能用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志来监视和优化查询速度
服务器配置有两个和慢查询相关的选项:
- slowlog-log-slower-than选项指定执行时间超过多少个微秒的命令请求会被记录到日志上
- slowlog-max-len 选项指定服务器最多保存多少条慢查询日志
23.1 慢查询记录的保存
struct redisServer{
/* slowlog */
// 保存了所有慢查询日志的链表
list *slowlog; /* SLOWLOG list of commands */
// 下一条慢查询日志的 ID
long long slowlog_entry_id; /* SLOWLOG current entry ID */
// 服务器配置 slowlog-log-slower-than 选项的值
long long slowlog_log_slower_than; /* SLOWLOG time limit (to get logged) */
// 服务器配置 slowlog-max-len 选项的值
unsigned long slowlog_max_len; /* SLOWLOG max number of items logged */
};
/*
* 慢查询日志
*/
typedef struct slowlogEntry {
// 命令与命令参数
robj **argv;
// 命令与命令参数的数量
int argc;
// 唯一标识符
long long id; /* Unique entry identifier. */
// 执行命令消耗的时间,以微秒为单位
// 注释里说的 nanoseconds 是错误的
long long duration; /* Time spent by the query, in nanoseconds. */
// 命令执行时的时间,格式为 UNIX 时间戳
time_t time; /* Unix time at which the query was executed. */
} slowlogEntry;
[外链图片转存中…(img-f4HyCEqZ-1609686516403)]
服务器状态的 slowlog
属性。
[外链图片转存中…(img-8TsgK9K7-1609686516404)]
23.2 慢查询日志的阅览和删除
[外链图片转存中…(img-EuFodNHZ-1609686516408)]
23.3 添加新日志
chapter24 监视器
24.1 成为监视器
客户端执行命令请求:redis> MONITOR
[外链图片转存中…(img-Fzwplhre-1609686516411)]
客户端状态的flags字段会被设置
client.flags |= REDIS_MONITOR
服务器状态的monitors字段会被追加该客户端
server.monitors.append(client)
24.2 向监视器发送命令信息
[外链图片转存中…(img-UwrZ1oL1-1609686516413)]
内容总结
以上是互联网集市为您收集整理的Redis设计与实现全部内容,希望文章能够帮你解决Redis设计与实现所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。