Redis概述
Redis是基于内存的高性能Key-Value数据库,是非关系型数据库(NoSQL),它主要是基于内存的数据库,并提供一定的持久化功能,并且支持多种数据结构。
Redis的特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
redis 是单线程的,是基于内存的操作,CPU不是redis的瓶颈,redis的瓶颈是根据机器的内存和网络带宽。
redis是将所有的数据全部放在内存中的,所以使用单线程效率最高,多线程会CPU上下切换,会耗时;
对于内存的系统来说,没有上下文切换,多次读写都是在一个cpu里面,也就是单线程就是效率最高的。
redis默认有16个数据库,可以把数据存储在不同的数据库中,默认是使用第一个数据库。
> select 3 # 切换数据库
> DBSIZE # 查看数据库大小
> flushdb # 清除当前数据库
> keys * # 查看所有的key
> FLUSHALL # 情况所有数据库
传统的关系型数据库如Mysql已经不能适用所有的场景了,比如秒杀的库存扣减,APP首页的访问流量高峰等等,都很容易把数据库打崩,所以引入了缓存中间件,目前市面上比较常用的缓存中间件有Redis和Memcached。
Redis 基本命令
1. 启动/关闭
命令 | 概述 | |
---|---|---|
brew services list | 查看brew安装的服务列表 | |
brew services start redis | 启动redis,关闭teminal后服务不会停止 | |
brew services stop redis | 关闭redis | |
redis-server /usr/local/etc/redis.conf | 启动redis | |
brew uninstall redis # 卸载redis | 卸载服务 | |
Shut down | 关闭服务 |
2. 连接redis
redis-cli -h 127.0.0.1
dokcer 使用redis
# 下载redis 镜像
docker pull redis
# 启动redis:-d 表示镜像的名称 -p 表示映射的端口 --name 表示运行的名称
# redis-server –appendonly yes : 在容器执行redis-server启动命令,并打开redis持久化配置
# --requirepass "123456" 启动密码
# –restart=always 随docker 的启动而启动
docker run --name=myredis -p 6379:6379 -v /root/docker/redis/config/redis.conf:/etc/redis/redis.conf -v /root/docker/redis/data:/data -d --restart=always redis redis-server --appendonly yes --requirepass "123456"
redis:5.0.5 redis-server /etc/redis/redis.conf
-d
# 进入redis 客户端
docker exec -it 容器ID redis-cli -a 'password'
3. 性能测试
# 测试本地100个并发服务,对10万个请求进行写入测试
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
配置文件在/usr/local/bin/
Redis 数据结构与对象
数据库
Redis 服务器将所有数据库都保存在服务器状态 redis.h/redisServer结构的db的数组中,会根据dbnum属性来决定创建多少个数据库,默认创建16个。
Redis客户端 状态redisClient结构的db 指针,指向了客户端当前使用的目标数据库,指针指向redisServer.db中的一个元素,可以使用select 切换目标数据库。
struct redisServer{
// 数组,保存服务器中的所有数据库
redisDb *db;
int dbnum;// 服务器数据库数量
// 记录了保存条件的数组,服务器执行BGSAVE的条件数组
struct saveparam *saveparams;
// 修改计数器,记录上一次执行BGSAVE成功后,进行了多少次修改
long long dirty;
};
struct redisDb{
// 数据库键空间,保存数据库中所有的键值对
dict *dict;
dict *expires;// 过期字典,保存键的过期时间。
} redisDb;
// redisClient.db指针指向redisServer.db数组的其中一个元素,而被指向的元素就是客户端的目标数据库。
typedef struct redisClient{
// 记录客户端当前正在使用的数据库
redisDb *db;
} redisClient;
select命令可以用来切换数据库,通过修改redisClient.db指针,让它指向服务器中的不同数据库,从而实现切换目标数据库的功能。
1. 键空间
Redis是键值对数据库服务器,每个数据库都是一个redis.h/redisDb结构表示,其中结构里面的dict字典保存数据库所有的键值对,这个字典称为键空间
typedef struct redisDb{
// 数据库键空间,保存着数据库中的所有键值对
dict *dict;
// 过期字典,保存着键的过期时间
dict *expires;
}
通过EXPIRE 或者 PEXPIRE命令 ,客户端可以以秒或者毫秒给数据库中的某个键设置生存时间(TTL),
SETEX 命令可以在设置字符串键的同时,创建过期时间
redisDb结构的expires 字典保存了所有键的过期时间,称为过期字典。过期字典的键是一个指针,指向键空间的某个键对象,值是一个long long 类型的整数,保存的是过期时间。
2. 过期键删除策略
- 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,对键进行删除操作。
- 惰性删除:每次从键空间获取键时,都检查取的键是否过期,如果过期就删除键,如果没有过期就返回键。
- 定期删除:每隔一段时间,程序就对数据库进行检查,删除里面的过期键。
第一种和第三种为主动删除策略,第二种为被动删除策略。
优缺点分析:
-
定时删除:
- 优点:对内存是最友好的,使用定时器会保证过期键尽快被删除,并释放占用的内存
- 缺点:对CPU时间是不友好的,过期键比较多的话,删除会消耗大量的CPU时间,在CPU紧张,但内存不紧张的情况下,把CPU用在删除键上面,会对服务器性能造成影响。
-
惰性删除:
- 优点:对CPU是有好的,只会在读取键的时候才删除。只有在非做不可的情况下才删除键,并且只会删除当前处理的键,不会删除其他无关的键。
- 缺点:对内存最不友好的,如果键过期了,还是会保存在内存中,只要不删除,内存就不会被释放,如果这个过期键没有被访问到的话,有可能永远不被删除
-
定期删除
- 如果定期操作时长太频繁的话,就会退化成定时删除,CPU被过多的消费在删除键上面
- 如果操作时长太短,或者执行太少,又会和惰性删除一样,占用大量内存。
Redis 通过配合使用定期删除和惰性删除来合理使用CPU时间和内存消费。
3. AOF、RDB 和复杂功能对过期键的处理
RDB持久化时,过期的键是不会被保存到新建的RDB文件中(SAVE、BGSAVE命令)。
在加载RDB文件时:
- 如果服务器是主服务器模式运行:过期的键是不会被载入到服务器中
- 如果服务器是从服务器模式运行:所有的键都会被载入到数据库中,
AOF 文件写入:
当服务器以AOF持久化模式运行时,如果某个键过期了,但还没有被删除,那么AOF文件不会有任何影响。
如果过期键被删除了,就会想AOF文件追加DEL命令。
AOF重写时,过期的键是不会被写入的。
复制:
如果服务器运行在复制模式下,从服务器的过期键删除动作由主服务器控制:
- 主服务器删除一个过期键后,会显示的向所有从服务器发送一个DEL命令,通知从服务器删除这个过期键
- 从服务器在执行客户端发送的读命令时,不会处理过期键。
- 从服务器只有当接受到主服务器发送的DEL命令才会删除键。
可以保证主从服务器的一致性。
总结
- Redis 服务器所有数据库保存在redisServer.db数组中。
- 客户端修改目标数据库指针,让他指向服务器中不同的数据库。
- 数据库由dict 键空间和 expires过期字典
- Redis采用惰性删除和定期删除两种策略来删除过期键。
- SAVE和BGSAVE 产生的新RDB文件不会包含过期的键
- BGREWRITEAOF 重写AOF文件不会包含过期的键。
- 当删除一个键时,会追加一条DEL命令到AOF文件末尾。
- 当主服务器删除一个过期键时,会显示的向所有的从服务器发送一个DEL命令删除过期键。
持久化
Redis是内存数据库,如果不把内存中的数据库状态保存到磁盘中,那么一旦服务器进程退出,服务器中数据库的状态也会丢失,因此Redis提出了RDB、AOF持久化功能
RDB持久化
RDB持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中。RDB持久化生成的RDB文件是经过压缩的二进制文件,通过该文件可以将某个时间点上的数据库状态保存到一个RDB文件中。
1. RDB文件创建与载入
创建:
SVAE 和BGSAVE命令可以用来生成RDB文件:
- SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕,在阻塞期间,服务器不能处理任何命令请求。
- BGSAVE命令会派生出一个子进程,然后由子进程创建RDB文件,服务器进程继续处理命令请求。
载入
Redis没有专门用于载入RDB文件的命令,在服务器启动时,只要检测到RDB文件存在,就会自动载入RDB文件。
在RDB文件载入期间,服务器处于阻塞状态,直到载入工作完成。
如果AOF持久化功能开启后,服务器会优先使用AOF文件来还原数据库。只有AOF持久化处于关闭状态,才会使用RDB文件。
在BGSAVE执行期间,服务器处理SAVE、BGSAVE、BGREWRITEAOF命令不同:
- 客户端发送的SAVE命令会被拒绝,禁止SAVE和BGSAVE同时存在。是为了避免服务器进程和子进程同时执行rdbSave,产生竞争条件。
- 客户端发送的BGSAVE命令也会被拒绝。
- BGREWRITEAOF 和 BGSAVE 命令不能同时执行:
- BGSAVE在执行,BGREWRITEAOF就会在BGSAVE执行完成后执行
- BGREWRITEAOF在执行,客户端发送的BGSAVE就会被禁止。
BGREWRITEAOF和BGSAVE 都是由子进程执行
自动间隔性保存
可以通过save选项设置多个保存条件,让服务器每隔一段时间就自动执行BGSAVE命令。只要一个条件满足就会被执行。
save 900 1 // 如果服务器在900秒之内,进行了至少一次修改
save 300 10// 如果服务器在300秒之内,进行了至少10次修改
save 60 10000// 如果服务器在60秒之内,进行了至少1000次修改
serverCron 默认每100豪秒执行一次,其中有一项功能就是检查save选择设置的保存时间是否满足。
AOF持久化
Redis 也提供了AOF (Append Only File)持久化功能,与RDB保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。
struct redisServer{
sda aof_buffer; // AOF缓冲区
}
命令会先保存到AOF缓冲区里面,之后在定期写入并同步到AOF文件。
AOF持久化的实现
AOF 持久化可以分为命令追加(append)、文件写入、文件同步(sync)。
-
命令追加
当AOF持久化功能开启后,服务器在执行一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。 -
AOF文件的写入与同步
Redis的服务器进程就是一个事件循环(loop),这个循环中的:
- 文件事件负责接受客户端的命令请求,以及向客户端发送命令回复;
- 时间事件负责运行定时执行的函数。
服务器在每次结束完一个事件循环之前,会调用flushAppendOnlyFile函数,考虑是否将aof_buf缓冲区的内容写入和保存到AOF文件里面。
flushAppendOnlyFile由服务器配置的appendfsync的值来决定:
appendfsync 选项的值 |
flushAppendOnlyFile 函数的行为 |
---|---|
always |
将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件。 |
everysec (默认) |
将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 如果上次同步 AOF 文件的时间距离现在超过一秒钟, 那么再次对 AOF 文件进行同步, 并且这个同步操作是由一个线程专门负责执行的。 |
no |
将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 但并不对 AOF 文件进行同步, 何时同步由操作系统来决定。 |
PS:文件的写入和同步
为了提高文件的写入效率,在操作系统汇总,当用户调用write函数将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满、或者超过指定的时限之后,才会真正的将缓冲区的数据写入到磁盘里面。
存在计算机发生停机,保存在内存缓冲区里面的写入数据将丢失的问题。s s s s s s s
AOF 持久化的效率和安全性
服务器配置 appendfsync
选项的值直接决定 AOF 持久化功能的效率和安全性。
当 appendfsync
的值为 always
时, 服务器在每个事件循环都要将 aof_buf
缓冲区中的所有内容写入到 AOF 文件, 并且同步 AOF 文件, 所以 always
的效率是 appendfsync
选项三个值当中最慢的一个, 但从安全性来说, always
也是最安全的, 因为即使出现故障停机, AOF 持久化也只会丢失一个事件循环中所产生的命令数据。
当 appendfsync
的值为 everysec
时, 服务器在每个事件循环都要将 aof_buf
缓冲区中的所有内容写入到 AOF 文件, 并且每隔超过一秒就要在子线程中对 AOF 文件进行一次同步: 从效率上来讲, everysec
模式足够快, 并且就算出现故障停机, 数据库也只丢失一秒钟的命令数据。
当 appendfsync
的值为 no
时, 服务器在每个事件循环都要将 aof_buf
缓冲区中的所有内容写入到 AOF 文件, 至于何时对 AOF 文件进行同步, 则由操作系统控制。
因为处于 no
模式下的 flushAppendOnlyFile
调用无须执行同步操作, 所以该模式下的 AOF 文件写入速度总是最快的, 不过因为这种模式会在系统缓存中积累一段时间的写入数据, 所以该模式的单次同步时长通常是三种模式中时间最长的: 从平摊操作的角度来看, no
模式和 everysec
模式的效率类似, 当出现故障停机时, 使用 no
模式的服务器将丢失上次同步 AOF 文件之后的所有写命令数据。
AOF文件的载入与数据还原
服务器只需要读入并重新执行一遍AOF文件里面保存的写命令就可以还原到关闭之前的状态。
步骤如下:
- 创建一个伪客户端(没有网络连接的客户端),
- 从AOF文件中分析并读取一条写命令
- 使用伪客户端执行被读出的写命令。
- 重复步骤2和3,直到AOF文件所有的写命令被处理完毕。
AOF 重写
随着服务器运行时间变长,AOF文件中的内容会越来越多,文件体积会越来越大,如果不处理的话,对严重影响redis的性能,而且AOF文件体积越大,AOF文件来进行数据还原的时间就越多。
AOF重写通过创建一个新的AOF文件来代替现有的AOF文件,新旧两个文件保存的数据库状态相同,但是新文件里面不会包含任何浪费空间的冗余命令。
1. AOF重写的实现
AOF通过读取当前的服务器状态来完成AOF重写。重写命令是BGREWRITEAOF。
AOF的重写是在子进程里面执行的,父进程(服务器进程)可以继续处理命令请求。在子进程重写期间,服务器会使用一个AOF重写缓冲区,记录服务器执行所有的命令,当Redis服务器执行一个写命令之后,会同步这个写命令到AOF缓冲区 和 AOF重写缓冲区。 子进程完成重写后会给服务器发送一个信号,父进程接受到信号后,会调用信号处理函数,这个函数会把AOF重写缓冲区里面的内容写入到新的AOF文件中,这样就可以保证AOF重写期间的数据一致性。
AOF重写缓冲区负责在子进程创建新的AOF文件期间,记录服务器执行的所有写命令,当子进程完成创建新的AOF文件工作之后,服务器会将重写缓冲区中的所有内容追加到新的AOF文件末尾。
客户端与服务器
客户端
Redis服务器是一对多的服务器程序,一个服务器可以与多个客户端建立网络连接。通过I/O多路复用技术来实现的文件事件处理器,Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。
- 服务器状态结构使用
clients
链表连接起多个客户端状态, 新添加的客户端状态会被放到链表的末尾。 - 客户端状态的
flags
属性使用不同标志来表示客户端的角色, 以及客户端当前所处的状态。 - 输入缓冲区记录了客户端发送的命令请求, 这个缓冲区的大小不能超过 1 GB 。
- 命令的参数和参数个数会被记录在客户端状态的
argv
和argc
属性里面, 而cmd
属性则记录了客户端要执行命令的实现函数。 - 客户端有固定大小缓冲区和可变大小缓冲区两种缓冲区可用, 其中固定大小缓冲区的最大大小为 16 KB , 而可变大小缓冲区的最大大小不能超过服务器设置的硬性限制值。
- 输出缓冲区限制值有两种, 如果输出缓冲区的大小超过了服务器设置的硬性限制, 那么客户端会被立即关闭; 除此之外, 如果客户端在一定时间内, 一直超过服务器设置的软性限制, 那么客户端也会被关闭。
- 当一个客户端通过网络连接连上服务器时, 服务器会为这个客户端创建相应的客户端状态。 网络连接关闭、 发送了不合协议格式的命令请求、 成为 CLIENT_KILL 命令的目标、 空转时间超时、 输出缓冲区的大小超出限制, 以上这些原因都会造成客户端被关闭。
- 处理 Lua 脚本的伪客户端在服务器初始化时创建, 这个客户端会一直存在, 直到服务器关闭。
- 载入 AOF 文件时使用的伪客户端在载入工作开始时动态创建, 载入工作完毕之后关闭。
服务器
- 一个命令请求从发送到完成主要包括以下步骤: 1. 客户端将命令请求发送给服务器; 2. 服务器读取命令请求,并分析出命令参数; 3. 命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复; 4. 服务器将命令回复返回给客户端。
serverCron
函数默认每隔100
毫秒执行一次, 它的工作主要包括更新服务器状态信息, 处理服务器接收的SIGTERM
信号, 管理客户端资源和数据库状态, 检查并执行持久化操作, 等等。- 服务器从启动到能够处理客户端的命令请求需要执行以下步骤: 1. 初始化服务器状态; 2. 载入服务器配置; 3. 初始化服务器数据结构; 4. 还原数据库状态; 5. 执行事件循环。
复制
在Redis中,用户可以通过执行SLAVEOF命令,让一个服务器去复制另一个服务器,被复制的服务器称为主服务器,对主服务器进行复制的服务器称为从服务器。
SLAVEOF 127.0.0.1 6379 # 成为127.0.0.1 6379的从服务器
1. 旧版的复制功能
Redis的复制分为同步(sync)和命令传播 两个操作:
- 同步操作用于将从服务的数据库状态更新至主服务器当前的状态
- 命令传播操作用于在主服务器状态被修改,导致主从服务器状态不一致时,让主从服务器达到一致状态。
1.1 同步
当客户端向从服务器发送 SLAVEOF 命令, 要求从服务器复制主服务器时, 从服务器首先需要执行同步操作, 也即是, 将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
SYNC 命令的执行步骤:
- 从服务器向主服务器发送 SYNC 命令。
- 收到 SYNC 命令的主服务器执行 BGSAVE 命令, 在后台生成一个 RDB 文件, 并使用一个缓冲区记录从现在开始执行的所有写命令。
- 当主服务器的 BGSAVE 命令执行完毕时, 主服务器会将 BGSAVE 命令生成的 RDB 文件发送给从服务器, 从服务器接收并载入这个 RDB 文件, 将自己的数据库状态更新至主服务器执行 BGSAVE 命令时的数据库状态。
- 主服务器将记录在缓冲区里面的所有写命令发送给从服务器, 从服务器执行这些写命令, 将自己的数据库状态更新至主服务器数据库当前所处的状态。
1.2 命令传播
当主服务器执行客户端发送的写命令时, 主服务器的数据库就有可能会被修改, 并导致主从服务器状态不再一致。
主服务器会将自己执行的写命令 —— 也即是造成主从服务器不一致的那条写命令 —— 发送给从服务器执行, 当从服务器执行了相同的写命令之后, 主从服务器将再次回到一致状态。
1.3 复制功能的实现
- 初次复制:从服务器以前没有复制过任何主服务器,或者从服务器当前要复制的主服务器和上一次复制的主服务器不同
- 断线后重复制:处于命令传播阶段的主从服务器因为网络原因中断了复制,但从服务器通过自动重连上了主服务器,并继续复制主服务器。
对于断线重连,旧版的复制功能在连接上后会向主服务器重新发送SYNC命令,主服务器会重新生成RDB文件,然后发送给从服务器,这一步非常耗费资源,其实只需要复制断线后主服务器执行的命令即可。
2. 新版复制功能
使用PSYNC来代替SYNC效率低下的问题,PSYNC命令具有完整重同步和部分重同步两种模式:
- 完整重同步跟SYNC一样。
- 部分重同步用于处理断线后重复制的情况,在从服务器断线重连到主服务器后,如果条件允许,主服务将断线期间执行的写命令发送给从服务器,从服务器只需要执行这些写命令就可以更新到主服务器当前的状态。
2.1 部分重同步的实现
部分重同步由三个部分组成:主从服务器的复制偏移量、主服务器的复制积压缓冲区、服务器的运行ID
复制偏移量
主从服务器都会维护一个复制偏移量,主服务每次向从服务器传播数据时就将自己的复制偏移量加上N,从服务每次接受到主服务的数据时,也将自己的复制偏移量加上N。
通过对比主从服务器的复制偏移量就可以知道主从服务器是否处于一致状态。
复制积压缓冲区
主服务器会维护一个固定长度的FIFO队列。复制积压缓冲区:
主服务器进行命令传播时,不仅会将写命令发送给所有从服务器,还会将写命令复制到积压缓冲区里面。所以积压缓冲区保存着一部分最近传播的写命令,也会记录相应的复制偏移量。
当从服务器重连上主服务器后,会通过PSYNC命令将自己的复制偏移量发送给主服务器:
- 如果偏移量之后的数据处于复制积压缓冲区里面,主服务器执行部分重同步操作
- 如果不存在则执行完整重同步操作。
服务器运行ID
每个Redis服务器都有自己的运行ID,初次复制时,主服务器会把自己的ID发给从服务器,从服务器就会保存这个ID,当断线重连后,从服务器就会发送这个ID给主服务器,如果从服务器保存的运行ID和当前运行的主服务器的ID相同的话,说明断线之前复制的就是当前连接的主服务器;否则就说明不是同一个服务器,则执行完整重复制。
主从服务器之间互为客户端,所以主服务才可以对从服务器执行写操作。
心跳检测
在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令(REPLCONF),这个命令对与主服务器有三个作用:(REPLCONF ACK <replication_offset>)
- 检测主从服务器的网络连接状态
- 辅助实现min-slaves选项
- 检测命令丢失。(检查replication_offset 偏移量是否正确)
哨兵与集群
Sentinel
Sentinel(哨兵) 是Redis 的高可用性解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器下的所有从服务器,并在被监听的主服务器下线时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。
下线的主服务再次上线后,成为当前的被监听的主服务器的从服务器。
1. 启动或初始化
启动一个 Sentinel 可以使用命令:
$ redis-sentinel /path/to/your/sentinel.conf
$ redis-server /path/to/your/sentinel.conf --sentinel
当一个 Sentinel 启动时, 它需要执行以下步骤:
- 初始化服务器。
- 将普通 Redis 服务器使用的代码替换成 Sentinel 专用代码。
- 初始化 Sentinel 状态。
- 根据给定的配置文件, 初始化 Sentinel 的监视主服务器列表。
- 创建连向主服务器的网络连接。
首先, 因为 Sentinel 本质上只是一个运行在特殊模式下的 Redis 服务器, 所以启动 Sentinel 的第一步, 就是初始化一个普通的 Redis 服务器,不过, 因为 Sentinel 执行的工作和普通 Redis 服务器执行的工作不同, 所以 Sentinel 的初始化过程和普通 Redis 服务器的初始化过程并不完全相同。
初始化状态:
struct sentinelState {
// 当前纪元,用于实现故障转移
uint64_t current_epoch;
// 保存了所有被这个 sentinel 监视的主服务器
// 字典的键是主服务器的名字
// 字典的值则是一个指向 sentinelRedisInstance 结构的指针
dict *masters;
// 是否进入了 TILT 模式?
int tilt;
// 目前正在执行的脚本的数量
int running_scripts;
// 进入 TILT 模式的时间
mstime_t tilt_start_time;
// 最后一次执行时间处理器的时间
mstime_t previous_time;
// 一个 FIFO 队列,包含了所有需要执行的用户脚本
list *scripts_queue;
} sentinel;
创建连向主服务器的网络连接
初始化 Sentinel 的最后一步是创建连向被监视主服务器的网络连接: Sentinel 将成为主服务器的客户端, 它可以向主服务器发送命令, 并从命令回复中获取相关的信息。
对于每个被 Sentinel 监视的主服务器来说, Sentinel 会创建两个连向主服务器的异步网络连接:
- 一个是命令连接, 这个连接专门用于向主服务器发送命令, 并接收命令回复。
- 另一个是订阅连接, 这个连接专门用于订阅主服务器的
__sentinel__:hello
频道。
2. 获取主服务器信息
Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO信息,通过分析INFO命令的回复来获取主服务器的当前信息。
3. 获取从服务器信息
当Sentinel发送主服务有新的从服务器出现时,Sentinel除了会为这个新的从服务器创建相应的实例结构外,Sentinel还会创建连接到从服务器的命令连接和订阅连接。
4. 检测主观下线
Sentinel会以每秒一次的频率向所有建立连接的实例发送ping命令请求,并根据回复来判断是否在线。根据配置文件设置的down-after-milliseconds 来判断是否下线,断线时长超过这个时间过后就会被判断为主观下线。
多个sentinel设置的主观下线时长可能不同。
主观下线是指,某个Sentinel发现主服务器出现故障不可达,但是这个不可达有可能是自身网络的原因。
5. 检测客观下线
当一个sentinel将一个主服务器判断为主观下线之后,为了确定这个主服务器是否真的下线,会向监视这个主服务的其他sentinel进行询问,看他们是否也认为主服务器已经进入了下线状态,当sentinel从其他sentinel接受到足够的下线判断后,sentinel就会将从服务器判断为客观下线,并进行故障转移。
6. 选举领头sentinel
当一个服务器被判断为客观下线后,监视这个服务器的各个sentinel会进行协商,选择出一个领头sentinel,并由领头sentinel进行故障转移:
选择领头sentinel步骤和方法:
- 所有的sentinel 都有机会被选中的资格
- 每个发现主服务器客观下线的sentinel,都会要求其他sentinel将自己选择为局部领头sentinel
- sentinel选择的规则是先到先的,最先向目标sentinel发送设置要求的源sentinel将成为目标sentinel的局部领头,而之后接受的其他设置要求会被目标sentinel拒绝。
- 如果超过半数以上的sentinel被设置成局部领头,那么这个sentinel将成为领头sentinel。
7. 故障转移
当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器执行故障转移操作
。包括三个步骤:
- 在已下线的主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转移成主服务器。
- 让已下线的主服务器属下所有的从服务器改成复制新的主服务器。
- 将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。
选择新的主服务
- 删除列表里面所有处于下线状态或者断线状态的从服务器
- 删除列表里面5秒内没有回复过领头Sentinel 的info命令的从服务器。
- 根据从服务器的优先级选择优先级最高的从服务器作为新的主服务器,选择复制偏移量最大、运行ID最小的从服务器。
多个 sentinel 有可能在同一个时间段内一起发现某个 master 客观下线,如果多个 sentinel 同时执行故障转移,有可能会乱套,也可能出现“脑裂”现象,所以在一个集群里,多个 sentinel 需要通过投票选出一个代表,由代表去执行故障转移。
脑裂: 经过故障转移后,
产生多个主服务
,会出现读写不一致的情况,(2 个哨兵可能出现问题)
设置sentinel 个数为奇数,少数服从多数,不会出现两个票数一样的代表同时被选上,进行故障转移。
Sentinel系统选举领头的方法是对Raft算法的领头选举方法的实现
集群
Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能
。
1. 节点
一个 Redis 集群通常由多个节点(node)组成, 在刚开始的时候, 每个节点都是相互独立的, 它们都处于一个只包含自己的集群当中, 要组建一个真正可工作的集群, 我们必须将各个独立的节点连接起来, 构成一个包含多个节点的集群。
连接各个节点的命令:
CLUSTER MEET <ip> <port>
向一个节点 node
发送 CLUSTER MEET 命令, 可以让 node
节点与 ip
和 port
所指定的节点进行握手(handshake), 当握手成功时, node
节点就会将 ip
和 port
所指定的节点添加到 node
节点当前所在的集群中。
一个节点就是一个运行在集群模式下的 Redis 服务器,Redis 服务器在启动时会根据 cluster-enabled
配置选项的是否为 yes
来决定是否开启服务器的集群模式,还是成为一个普通的Redis服务器。
集群数据结构
clusterNode
结构保存了一个节点的当前状态, 比如节点的创建时间, 节点的名字, 节点当前的配置纪元, 节点的 IP 和地址, 等等。
每个节点都会使用一个 clusterNode
结构来记录自己的状态, 并为集群中的所有其他节点(包括主节点和从节点)都创建一个相应的 clusterNode
结构, 以此来记录其他节点的状态:
struct clusterNode {
// 创建节点的时间
mstime_t ctime;
// 节点的名字,由 40 个十六进制字符组成
// 例如 68eef66df23420a5862208ef5b1a7005b806f2ff
char name[REDIS_CLUSTER_NAMELEN];
// 节点标识
// 使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
// 以及节点目前所处的状态(比如在线或者下线)。
int flags;
// 节点当前的配置纪元,用于实现故障转移
uint64_t configEpoch;
// 节点的 IP 地址
char ip[REDIS_IP_STR_LEN];
// 节点的端口号
int port;
// 保存连接节点所需的有关信息,比如套接字描述符, 输入缓冲区和输出缓冲区:
clusterLink *link;
// 用于记录节点负责处理那些槽。
unsigned char slots[16384/8];
int numslots;
......
};
CLUSTER MEET命令的实现
通过向结点A发送CLUSTER MEET命令,客户端可以让接收命令的结点A将另一个结点B添加到结点A所在的集群里面,收到命令后A与B进行握手,来确定彼此的存在
:
- A为结点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。
- 结点A根据CLUSTER MEET命令给定的ip和端口,向B发送meet消息。
- 结点B收到消息后,也为A创建clusterNode结构,并将添加到自己clusterState.nodes。
- 然后B回复给A一个pong消息;A收到后再回复pong给B。
2. 槽指派
Redis集群通过分片的方式保存数据库中的键值对,集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽中的一个,集群的每个节点可以处理0个或最多16384个槽
。
当数据库中16384个槽都有节点在处理时,集群处于上线状态(ok)
;相反的,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)。
一个节点会将自己负责处理的槽记录再clusterNode结构的slots属性和numslots属性之外,还会将自己的slots数组通过消息发送给集群中的其他结点
。
每个结点都会处理自己的槽,并记录其他结点负责处理的槽,只有所有的槽都被指派后集群才会上线
CLUSTER ADDSLOTS <slot> [slot ..] // 用于将槽指派给某个节点
3. 在集群中执行命令
在对数据库中16384个槽都进行了指派后,集群就进入了上线状态,这时客户端就可以向集群中的节点发送数据命令了,当客户端向节点发送和数据库键相关的命令时,接受命令的节点会计算出命令要处理的数据库键属于那一个槽,并检查这个槽是否指派给了自己
:
- 如果键所在的槽就刚好指派了给当前节点,那么节点直接执行这个命令
如果键所在的槽没有指派给当前节点,那么节点就会想客户端返回一个MOVED错误,指引客户端转向正确的节点,并再次发送之前想要的命令
。
4. 重新分片
Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽 改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点
。
ASK错误
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一个情况,属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面
。
当客户端向源节点发送一个与数据库有关的命令,并且命令要处理的数据库键恰好属于正在被迁移的槽时:
- 源节点会先在自己的数据库里面查找指定的键,如果找到的话就直接执行客户端发送的命令。
- 如果源节点没能在自己的数据库里面找到指定的键,那么这个键可能被迁移到了目标节点,
源节点将向客户点返回一个ASK错误
,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。
总结:
- 节点通过握手来将其他节点添加到自己所处的集群当中。
- 集群中的
16384
个槽可以分别指派给集群中的各个节点, 每个节点都会记录哪些槽指派给了自己, 而哪些槽又被指派给了其他节点。 - 节点在接到一个命令请求时, 会先检查这个命令请求要处理的键所在的槽是否由自己负责, 如果不是的话, 节点将向客户端返回一个
MOVED
错误,MOVED
错误携带的信息可以指引客户端转向至正在负责相关槽的节点。 - 对 Redis 集群的重新分片工作是由客户端执行的, 重新分片的关键是将属于某个槽的所有键值对从一个节点转移至另一个节点。
- 如果节点 A 正在迁移槽
i
至节点 B , 那么当节点 A 没能在自己的数据库中找到命令指定的数据库键时, 节点 A 会向客户端返回一个ASK
错误, 指引客户端到节点 B 继续查找指定的数据库键。 MOVED
错误表示槽的负责权已经从一个节点转移到了另一个节点, 而ASK
错误只是两个节点在迁移槽的过程中使用的一种临时措施。- 集群里的从节点用于复制主节点, 并在主节点下线时, 代替主节点继续处理命令请求。
- 集群中的节点通过发送和接收消息来进行通讯, 常见的消息包括
MEET
、PING
、PONG
、PUBLISH
、FAIL
五种。
发布与订阅
Redis的发布与订阅功能由PUBLISH、SUBSCRIBE、PSUBSCRIBE 等命令组成:
通过执行SUBSCRIBE命令,客户端可以订阅一个或多个频道,从而成为这些频道的订阅者:每当有其他客户端向被订阅的频道发送消息时,频道的所有订阅者都会接收到这条消息
。(UNSUBSCRIBE命令用于退订频道)
客户端通过执行PSUBSCRIBE命令订阅一个或多个符合给定模式的频道,从而成为这些模式的订阅者
:每当有其他客户端向某个频道发送消息时,消息不仅会被发送给这个频道的所有订阅者,它还会被发送给所有与这个频道相匹配的模式的订阅者。(PUNSUBSCRIBE命令用来退订模式)。
PUBLISH 命令用来发布消息。
// 向 “news.it” 频道发送消息“hello”
PUBLISH “news.it” "hello"
PUBSUB命令可以用来查看频道或者模式的相关信息。
总结
- 服务器状态在
pubsub_channels
字典保存了所有频道的订阅关系: SUBSCRIBE 命令负责将客户端和被订阅的频道关联到这个字典里面, 而 UNSUBSCRIBE 命令则负责解除客户端和被退订频道之间的关联。 - 服务器状态在
pubsub_patterns
链表保存了所有模式的订阅关系: PSUBSCRIBE 命令负责将客户端和被订阅的模式记录到这个链表中, 而 UNSUBSCRIBE 命令则负责移除客户端和被退订模式在链表中的记录。 - PUBLISH 命令通过访问
pubsub_channels
字典来向频道的所有订阅者发送消息, 通过访问pubsub_patterns
链表来向所有匹配频道的模式的订阅者发送消息。 - PUBSUB 命令的三个子命令都是通过读取
pubsub_channels
字典和pubsub_patterns
链表中的信息来实现的。
事务
Redis 通过MULTI、EXEC、WATCH等命令来实现事务功能
。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并在事务执行期间,服务器不会中断事务而去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
事务的实现
一个事务从开始到结束通常会经历以下三个阶段:事务开始、命令入队、事务执行。
MULTI 命令可以将执行该命令的客户端从非事务状态切换至事务状态, 这一切换是通过在客户端状态的 flags
属性中打开 REDIS_MULTI
标识来完成的。
当一个客户端处于非事务状态时, 这个客户端发送的命令会立即被服务器执行。当一个客户端切换到事务状态之后, 服务器会根据这个客户端发来的不同命令执行不同的操作:
- 如果客户端发送的命令为 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令的其中一个, 那么服务器立即执行这个命令。
- 与此相反, 如果客户端发送的命令是 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令以外的其他命令, 那么服务器并不立即执行这个命令, 而是将这个命令放入一个事务队列里面, 然后向客户端返回
QUEUED
回复。
每个 Redis 客户端都有自己的事务状态, 这个事务状态保存在客户端状态的 mstate
属性里面:
typedef struct redisClient {
// ...
// 事务状态
multiState mstate; /* MULTI/EXEC state */
// ...
} redisClient;
//事务状态包含一个事务队列, 以及一个已入队命令的计数器 (也可以说是事务队列的长度)
typedef struct multiState {
// 事务队列,FIFO 顺序
multiCmd *commands;
// 已入队命令计数
int count;
} multiState;
当一个处于事务状态的客户端向服务器发送 EXEC 命令时, 这个 EXEC 命令将立即被服务器执行: 服务器会遍历这个客户端的事务队列, 执行队列中保存的所有命令, 最后将执行命令所得的结果全部返回给客户端。
WATCH命令的实现
WATCH命令是一个乐观锁
,它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话则拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
demo:
> WATCH "name" // 监视name键
> MULTI
> SET "name" "peter"
> EXEC
Nil //事务执行失败,因为有另外一个客户端在EXEC命令之前执行了SET “name” “john”
每个Redis数据库都保存一个watched_key字典,这个字典的键是某个被WATCH命令监视的数据库键,值是一个链表(记录监视相应数据库键的客户端)
所有对数据库进行修改的命令,如SET、LPUSH、SADD等,在执行完后都会调用multi.c/touchWatchKey函数 对watched_keys字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有的话那么监视被修改键的所有客户端的REDIS_DIRTY_CAS标识被打开,表示客户端的事务安全性已经被破坏。
服务器通过检查 监视键的客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务 。
悲观锁则是把所有的命令都加上锁,性能比较低。
事务的ACID性质
事务具有:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
原子性
数据库将事务中的多个操作当成一个整体来执行,服务器要么执行事务中的所有操作,要么一个也不执行。
- 如果在命令入队时出错而被服务器拒绝,那么事务中的所有命令都不会被执行。
- 如果在命令执行期间出错,事务后续的命令也会继续执行下去.
Redis的事务不支持回滚机制
。
持久性
当一个事务执行完毕后,执行这个事务所得的结果立马保存到硬盘里面,即使服务哇宕机,执行事务的结果也不会丢失。
只有运行才AOF持久化模式下,并且appendfsync的值为always时,命令数据才会马上保存到硬盘里面
。
总结
- 事务提供了一种将多个命令打包, 然后一次性、有序地执行的机制。
- 多个命令会被入队到事务队列中, 然后按先进先出(FIFO)的顺序执行。
- 事务在执行过程中不会被中断, 当事务队列中的所有命令都被执行完毕之后, 事务才会结束。
- 带有 WATCH 命令的事务会将客户端和被监视的键在数据库的
watched_keys
字典中进行关联, 当键被修改时, 程序会将所有监视被修改键的客户端的REDIS_DIRTY_CAS
标志打开。 - 只有在客户端的
REDIS_DIRTY_CAS
标志未被打开时, 服务器才会执行客户端提交的事务, 否则的话, 服务器将拒绝执行客户端提交的事务。 - Redis 的事务总是保证 ACID 中的原子性、一致性和隔离性, 当服务器运行在 AOF 持久化模式下, 并且
appendfsync
选项的值为always
时, 事务也具有耐久性。