单机数据库的实现
- 数据库
- 持久化 RDB,AOF
- 事件
- 客户端
- 服务器
数据库
redis服务器将所有的数据库都保存在服务器状态 redis,h/redisServer结构的db数组中
struct redisServer{
// 一个数组,保存服务器中所有数据库
redisDb *db;
int dbnum;
}
初始化服务器中,程序会根据服务器状态的dbnum属性来决定创建多少个数据库
切换数据库
默认情况下,redis客户端的目标数据库为0号数据库,客户端可以通过执行select命令来切换目标数据库
例如:在0号数据库 set message 在0号数据库可以获取到这个message,在其他数据库不会保存,为nil
客户端状态redisClient结构的db属性记录了客户端当前目标数据库,这个属性是一个指向redisDb结构的指针
typedef struct redisClient{
redisDb *db
}redisClient;
通过修改redisClient,db指针 ,让它指向服务器中不同的数据库,从而实现切换目标数据库的功能-这就是Select命令的实现原理
数据库键空间
服务器中每个数据库都由一个redis,h/redisDb 结构表示,其中redisDb结构的dict字典保存了数据库中所有的键值对 我们将这个字典称为键空间
typedef struct redisDb{
dict *dict
}
键空间和用户所见的数据库是直接对应的
- 键空间的键也就是数据库的键,每个键都是一个字符串对象
- 键空间的值也就是数据库的值,可以是字符串对象,列表对象,哈希表对象,集合对象和有序集合对象中任意一种redis对象
- 添加一个键,也就是将一个键值对添加到键空间字典里
- 删除数据库中的一个键,实际上就是在键空间里面删除键所对应的键值对对象
- 对一个数据库键进行更新,也就是对键空间的所对应的值对象进行更新
- 对键取值,也就是在键空间中取出键所对应的值对象
- FLUSHDB,也就是通过删除键空间的所有键值对来实现的
- DBSIZE 就是返回通过键空间中包含键值对的数量来实现的
读写键空间时的维护操作
当时同redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作
- 读取一个键之后服务器会根据键是否命中 ,来更新服务器的键空间命中次数和键空间不命中次数 ,可使用命令keyspace_hits和keyspace_misses进行查看
- 读取一个键之后,服务器会更新键的LRU(最后一次使用)时间
- 如果在服务器读取一个键时发现这个键已经过期,那么服务器会先删除这个键,然后才执行余下的其他操作
- 如果客户端使用了watch命令监视了某个键,如果服务器对监视的键进行修改之后,会将这个键标记为脏dirty,从而让事务注意到这个键已经被修改过
- 服务器每次修改一个键后,都会对脏键计数器的值增1,这个计数器会触发服务器的持久化以及复制操作
设置键的生存时间或过期时间
保存过期时间:redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典
- 过期字典的键是一个指针,指向键空间的某个键对象
- 过期字典的值是一个long long 类型的整数,保存了键所指向的数据库键的过期时间
计算并返回剩余生存空间
TTL:以秒为单位返回键的生存时间
PTTL: 以毫秒为单位返回键的剩余生存时间
- 键不存在数据库 返回-2
- 尝试取得键的过期时间,如果没有设置键的过期时间,返回为none
- 如果返回的时间为none,说明没有设置过期时间,返回-1
- 获取当前时间
- 过期时间减去当前时间,当前的差就是键的剩余生存时间
过期键的判定:
- 获取键的过期时间
- 判断键有没有设置过期时间
- 获取当前时间的unix时间戳
- 检查当前时间是否大于键的过期时间
- 大于:键以过期 小于:还没过期
过期键的删除策略
定时删除
惰性删除
定期删除
惰性删除策略的实现
过期间的惰性删除策略是由db,c/expireIfNeeded函数实现,所有读写数据库的命令在执行之前都会调用expireIfNeeded函数对输入键进行检查
-
如果输入键已经过期,那么expireIfNeeded函数对输入键从数据库删除
-
如果输入键还没过期,expireIfNeeded不做动作
-
当键存在时,命令按照键存在的情况执行
-
当键不存在时,或者因为过期而被函数删除,命令按照键不存在的情况执行
定期删除策略的实现
过期键的定期删除有redis,c/activeExpireCycle函数实现,每当redis的服务器周期性操作redis,c/serverCron函数执行时,acticeExpireCycle函数就被调用
- 函数每次运行时,都从一定数量的数据库取出一定数量的随机键进行检查,并删除其中的过期键
- 全局变量current_db记录了当前activeExpireCycle函数的检查进度,并在下一次activeExpireCycle函数调用时,接着上一次的进度进行处理
- 随着函数不断执行,服务器中所有的数据库都会被检查一遍,这时调用current_db变量重置为0,然后再次开始新一轮的检查工作
AOf和EDB对过期键的处理
-
生成rdb文件时: 在执行save命令或者bgsave命令创建一个新的rdb文件时,程序会对数据库中键进行检查,已过期的键不会保存到新建的rdb文件中
-
载入rdb文件: 主服务器不会载入过期键,从服务器模式会载入所有的键到数据库,不过,主从服务器进行数据同步时,从服务器的数据库会被清空,过期键的载入rdb文件的从服务器也不会造成影响
-
aof文件写入,当 过期键被惰性删除或定期删除之后,程序会想aof文件追加一条del命令,来显示记录该键已经被删除,例如客户端使用get message 命令试图访问过期键message时
- 从数据库删除message键
- 追加一条del message命令到aof问囧
- 向执行get命令的客户端返回空回复
-
aof重写:过期键不会保存到重写后的aof文件中
-
复制: 从服务器的过期键删除动作由主服务器控制
-
主服务器删除一个过期键后,会显式地向所有从服务器发送一个del命令,搞知从服务器删除这个过期键
-
从服务器在执行客户端发送的读命令时,即使碰到过期键也不会删除过期键,而是像处理未过期的键一样处理过期键
-
从服务器只有在接收到主服务器发来的DEL命令后,才会删除过期键
-
主服务器控制从服务器统一的删除过期键,保证了主从服务器数据的一致性
当客户端向从服务器发送命令 get message 时,从服务器发现message已经过期 ,但从服务器不会删除message键,而是继续将message键的值返回给客户端
如果客户端向主服务器发送get message,主服务器会发现键message已经过期,主服务器会删除message,向客户端返回空回复,并向从服务器发送del message命令
从服务器在接收到主服务器发来的del message命令之后,也会从数据库中删除message键,这样 ,主从服务器都不会保存过期键message了
RDB持久化
因为aof文件的更新频率通常比rdb文件的更新频率高
- 如果服务器开启了aof持久化功能,那么服务器会优先使用aof文件还原数据库状态
- 只有在aof持久化功能处于关闭状态时,服务器才会使用rdb文件来还原数据库状态
save命令:redis服务器会被阻塞,当save执行时,客户端发送的所有请求都会被阻塞,只有当服务器执行完save命令,重新开始接收命令请求之后,客户端发送的命令才会被处理
bgsave:bgsave命令保存工作由子进程执行,所以子进程创建rdb文件过程中,redis服务器仍然可以继续处理客户端的命令请求。
在bgsave命令执行期键,服务器处理save,bgsave,bgrewriteaof 三个命令和平时有所不同
- save命令被拒绝:防止父进程和子进程同时执行两个rdbsave调用,防止产生竞争条件
- bgsave被拒绝,同时执行两个bgsave也会产生竞争条件
- bgrewriteaof延迟到bgsave执行完毕后执行
- 执行bgrewriteaof时,客户端发送的bgsave命令会被 服务器拒绝
rdb文件载入期键,服务器会一直居于阻塞状态
save的默认条件
- save 900 1 900秒之内,对数据库进行了至少一次修改
- save 300 10 300s之内 , 对数据库进行了至少10次修改
- save 60 10000 在60s之内,进行了至少10000次修改
dirty计数器: 记录距离上一次成功执行save命令或者bgsave命令之后,服务器对数据库状态进行了多少次修改
redis 服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,检查save命令的保存条件是否满足,满足就执行bgsave命令
AOF持久化
redis服务器通过保存redis服务器所执行的写命令来执行数据库状态的
持久化的实现
- 命令追加
- 文件写入
- 文件同步
###AOF持久化
- 命令追加。服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾
- 将缓冲区的内容写入aof文件的三种方式
- always
- everysec 默认
- no
AOF文件重写
AOF重写并不需要对现有的AOF文件进行任何读取,分析和写入操作,这个功能时根据通过读取服务器当前的数据库状态来实现的
使用子进程重写aof文件
AOF重写缓冲区在服务器创建子进程之后开始使用
在服务器创建子进程时,开始使用aof重写缓冲区,当redis服务器执行完一个写命令之后吗,它会同时将这个写命令发送给aof缓冲区和aof重写缓冲区
- 执行客户端发来的命令
- 将执行后的写命令追加到aof缓冲区
- 将执行后的写命令追加到aof重写缓冲区
子进程重写aof后,它会向父进程发送一个信号,父进程在接收到该信号之后,会调用一个信号处理函数,执行以下工作
- 将aof重写缓冲区的内容写入到新aof文件中,这时aof文件所保存的数据库状态将和服务区当前的数据库状态一致
- 对新的aof文件进行改名,原子地覆盖现有的aof文件,完成新旧两个aof文件的替换
多机数据库的实现
- 主从复制
- 哨兵(sentinel)
- 集群
- 分布式锁
主从复制
从服务器->slaveof->主服务器
进行复制中的主从服务器双方的数据库保存相同的数据,称为数据库状态一致
复制功能
- 同步操作(sync)
- 将从服务器的数据库状态更新至主服务器当前所处的数据库状态
- 从服务器发送sync至主数据库
- 收到sync命令的主服务器执行bgsave命令,后台生成rdb文件,并使用一个缓冲区记录从现在开始执行的所有写命令
- 当主服务器的bgsave的命令执行完,主服务器将生成的rdb文件发送给从服务器,从服务器接收并加载这个rdb文件,将从服务器的数据库状态更新至主服务器执行bgsave命令时的数据库状态
- 主服务器将缓冲区的所有写命令发送给从服务器,将自己的数据库状态更新至主服务器数据库当前所处的状态
- 命令传播(command propagate)
- 主服务器的数据库状态被修改,导致主从服务器的数据库状态不一致,让主从服务的数据库状态重新回到一致状态
- 当主服务器数据库状态被修改,主服务器将造成主从数据库不一致的写命令发送给从服务器执行,主从服务器数据库状态再次回到一致状态
旧版复制功能和新版复制功能
- 旧版复制功能
- 效率低,例如断线重连时会将主服务器数据库的全部数据重新加载过来
- 新版复制功能
- PSYNC 完整重同步 部分重同步
- 完整重同步:初次复制情况,与sync命令执行步骤基本一样
- 部分重同步:用与处理断线后重复制情况
- psync仅发送断线后从服务器缺少的写命令
部分重同步的实现
- 主服务器的复制偏移量和从服务器的复制偏移量
-
执行复制的双方,主服务器和从服务器会分别维护一个复制偏移量
-
主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N
-
从服务器每次收到主服务器传播来的N个字节的数据时,九江自己的复制偏移量加上N
-
如果主从服务器处于一致状态,那么主从服务器的复制偏移量(offset)总是相同的
-
如果主从服务器的偏移量并不相同,那么主从服务器并未处于一致状态
-
- 主服务器的复制积压缓冲区
- 主服务器维护的一个固定长度,先进先出FIFO队列,大小为1MB
- 主服务器进行命令传播时,不光将写命令发送给所有从服务器,并且将写命令入队到复制积压缓冲区中。因此主服务器中的复制积压缓冲区中会保存一部分最近传播的写命令,复制积压缓冲区并为队列中的每个字节记录相应的复制偏移量
- 当从服务器通过psync命令将自己的复制偏移量发送给主服务器,主服务器会根据这个复制偏移量来判断进行何种同步操作
- 如果offset偏移量之后的数据仍然存在复制积压缓冲区中,主服务器对从服务器进行部分重同步操作+continue
- offset偏移量之后的数据不存在复制积压缓冲区中,主服务器对从服务器执行完整重同步操作
- 服务器的运行ID
- 初次复制时,主服务器将自己的运行ID发送从服务器,从服务器保存这个id
- 断线重连时,从服务器将向当前连接的主服务器发送之前保存的运行ID
- 保存ID和当前连接的主服务器运行ID相同,说明从服务器断线前复制的就是当前连接的这个主服务器,主服务器可以继续尝试执行部分重同步操作
- 如果ID不同,说明从服务器断线前复制的主服务器并不是当前连接的这个主服务器,主服务器会对从服务器执行完整重同步操作
哨兵机制(Sentinel)
由一个或多个sentinel实例组成的sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线1的主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求
sentinel本质知识一个运行在特殊模式下的redis服务器,初始化sentinel时不会载入rdb或者aof文件
- 初始化服务器
- 使用sentinel专用代码
- 初始化sentinel状态
- 初始化sentinel状态的nasters属性
- sentinel状态的masters字典记录了所有被sentine监视的主服务器的相关信息
- 创建连向主服务器的网络连接
- 命令连接
- 订阅连接
sentinel默认以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令回复来获取主服务器的当前信息
检测主观下线状态
默认情况下,sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(主服务器,从服务器,其他sentinel)发送ping命令,并通过实例返回的ping命令回复判断实例是否在线
sentinel询问其他sentinel是否认为服务器下线,当认为主服务器已经进入下线状态的sentinel参数的值,那么该sentinel会认为主服务器已经进入客观下线的状态
选举领头sentinel
故障转移
- 在以下线得主服务器属下得所有从服务器中,挑选一个从服务器,并将其转换为主服务器
- 将已下线得主服务器下的所有从服务器改为复制新的主服务器
- 已下线得主服务器设置为新主服务器得从服务器,当这个旧的主服务器重新上线时,他就会成为新的主服务器的从服务器