二丶数据库的实现

单机数据库的实现


struct redisServer {
  //...
  redisDb *db;//一个数组,保存服务器的所有数据库
  int dbnum;//数据库的数量
  long dirty;//修改计数器
  time_t lastsave;//上一次执行保存的时间
  sds aof_buf;//AOF缓冲区
  list *clients;//客户端状态
  //...
};

typedef struct redisClient {
  //...
  redisDb *db;//记录正在使用的数据库
  int fd;//套接字描述符.伪客户端时fd为-1,普通客户端大于-1
  robj *name;
  sds querybuf;//输入缓冲区(默认1GB)
  robj **argv;//命令参数
  int argc;//命令个数
  //...
} redisClient;

typedef struct redisDb {
  dict *dict;//数据库键空间,保存数据库中所有的键值对
  dict *expires;//过期字典,保存着过期时间
} redisDb;


设置过期时间


#生存时间ttl秒
EXPIRE <key> <ttl> 
#生存时间ttl毫秒
PEXPIRE <key> <ttl> 
#过期时间设置为ttl秒指定的时间戳
EXPIREAT <key> <ttl> 
#过期时间设置为ttl毫秒指定的时间戳
PEXPIREAT <key> <ttl>


EXPIRE转换为PEXPIRE转换为PEXPIREAT。EXPIREAT转换为PEXPIREAT。最终都转换成PEXPIREAT 命令来执行。

过期键策略

  • 定时删除:设置键的过期时间的同时有一个定时器,让定时器在键的过期时间来临时,立即对键进行删除。
  • 惰性删除:放任键过期不管,但每次获取键时都检查键是否过期,如果过期就删除该键,没过期就返回该键。
  • 定期删除:每隔一段时间,就对数据库检查,删除里面的过期键。删除键数量与检查数据库数量由算法决定。

定时删除对CPU不友好,在过期键比较多的情况下,删除行为会占用相当一部分CPU时间。惰性删除对内存不友好,不会占用过多的CPU时间;且非常多的过期键又恰好没有被访问到时,会造成内存泄漏。定期删除是一种整合与折中。

Redis的过期删除策略

实际使用的是惰性删除和定期删除。

AOF、RDB和复制功能对过期键的处理

生成RDB文件(在执行SAVE和BGSAVE命令创建一个RDB文件)时已过期的键不会保存到文件中。

载入RDB文件,主服务器运行,未过期的会载入到数据库,以过期的会忽略。从服务器运行,会全部载入到数据库,不过在向主服务器同步数据时,从服务器会被清空。

AOF对于键以过期,但未被删除时,不会产生影响。待键删除后,会向AOF文件追加(append)一条DEL命令。

AOF重写,会忽略已过期的键。

复制,从服务器处理读命令时,即使碰到过期键也不会删除过期键,待接到主服务器的DEL命令时,才会删除过期键。

数据库的通知

空间通知:某个键执行什么命令

事件通知:某个命令被什么键执行

notify-keyspace-events决定服务器发送的通知类型

RDB持久化

有两个命令可以用于生成RDB文件,一个是SAVE,一个是BGSAVE。

SAVE会阻塞Redis服务器进程,知道文件创建完成为止,阻塞期间服务器不能处理任何请求。

BGSAVE会派生出一个子进程,然后由子进程负责创建RDB文件,父进程继续处理服务器命令。

RDB文件的载入工作是在服务器启动时自动执行的。由于AOF文件的更新频率比RDB要高,如果开启了AOF持久化功能,会优先使用AOF文件来还原数据库状态,只有AOF持久化功能关闭时,才会使用RDB文件来还原数据库状态。

在BGSAVE执行期间,客户端发送SAVE或BGASAVE会被拒绝,以防发生竞争。发送BGREWRITEAOF会被延迟到BGSAVE后执行。

在BGREWRITEAOF执行期间,客户端发送BGASAVE会被拒绝。

BGREWRITEAOF和BGASAVE都在子进程中执行,性能考虑,避免同时执行大量磁盘的写入操作。

AOF持久化

AOF持久化功能分为命令追加、文件写入、文件同步三个步骤。

appendfsync选项:

  • always 总是
  • everysec 每秒一次
  • no 不同步aof文件,由系统调用

AOF文件的载入:创建一个不带网络连接的伪客户端来执行AOF文件保存的写命令。

AOF重写(每条命令的元素数量最大为64):子进程建立后,会创建一个AOF重写缓冲区,执行重写期间的命令,会追加到缓冲区和重写缓冲区,缓冲区会定期同步到AOF文件,待重写完成,重写缓冲区会将所有内容写入到新的AOF文件并替换原文件。

事件

文件事件

Redis服务器通过套接字与客户端进行连接,而文件事件就是对套接字的抽象。

文件事件处理器使用I/O多路复用来同时监听多个套接字,并根据套接字执行的任务关联不同的事件处理器。I/O多路复用底层实现包括select、epoll、evport、kqueue。

时间事件

Redis服务器中的一些操作(serverCron)需要在给定时间执行,而时间事件就是对这类任务的抽象。

服务器将所有的时间事件都放在一个无序链表中,每当时间事件运行时,就遍历整个链表,并调用相应的事件处理器。(正常模式下服务器只有一个serverCorn时间事件,在benchmark模式下,使用两个时间事件,此时无序链表退化成一个指针使用)

serverCorn:更新服务器信息。清除过期键值对。关闭失效连接。RDB或AOF持久化操作。主服务器,对从服务器定期同步。集群模式,对集群同步和连接测试。

serverCron每100毫秒执行一次更新系统时间缓存。每10秒更新LRU时钟。执行延时的BGREWRITEAOF。

多机数据库的实现

复制

SLAVEOF 127.0.0.1 6379(成为127.0.0.1 6379的从服务器)

  1. 从服务器向主服务器发送SYNC命令
  2. 收到SYNC命令的主服务器执行BGSAVE命令,生成一个RDB文件,并使用一个缓冲区记录从现在开始所有的写命令
  3. RDB文件生成完毕,将文件发送给从服务器,从服务器开始载入文件
  4. 主服务器将缓冲区的所有命令发给从服务器,从服务器开始执行这些命令,将自己的数据库状态更新为当前所处的状态

新版本复制功能:SYNC是非常耗费资源的操作,2.8版本开始,使用PSYNC代替SYNC,PSYNC包括完整同步和部分同步。

部分同步包括:

主服务器复制偏移量和从服务器复制偏移量。

主服务器复制积压缓冲区。(固定长度1MB先进先出队列,命令传播时会保存进队列。可修改repl-backlog-size修改缓冲区大小)

服务器运行id。

Sentinel

redis-sentinel sentinel.conf

redis-server sentinel.conf --sentinel

Sentinel本质上是一个特殊模式下的Redis服务器,对于每个被监视的服务器来说,Sentinel会建立一个命令连接,用于向服务器发送命令。一个订阅连接服务器的_sentinel_ :hello频道接收服务器的频道信息来发现未知的Sentinel。

选举领头Sentinel

每次选举都会使纪元+1,每个sentinel向其他sentinel发送SENTINEL is-master-down-by-addr,如果接收到命令的sentinel还未设置局部领头的话,就将收到的运行id设置为自己的局部领头,超过半数确认领头sentinel。

故障转移

  1. 在已下线的主服务器的从服务器中选出一个从服务器将其转换为主服务器
  2. 让其他从服务器改为复制新主服务器
  3. 旧主服务器重新上线,将它设置为新主服务器的从服务器

集群

Redis集群是Redis的分布式数据库方案,通过分片来进行数据共享,并提供复制和故障转移功能。

redis-cli -c -p 7000 //连上集群

redis.conf中的cluster-enable设置为yes开启集群模式

节点

CLUSTER MEET 127.0.0.1 6379 //加入节点

槽指派

集群的整个数据库被分为16384个槽(slot),如果数据库中16384个槽都有节点在处理就是处于上线状态。相反,有任何一个槽没有得到处理,集群就处于下线状态。

CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000 //指定槽给节点

指派好所有的槽后,就可以执行命令了,如果键所在的槽正好指派了当前节点,就直接执行,如果不是当前节点,那么节点就会向客户端返回一个MOVED错误,指引客户端转向(redirect)正确的节点。

重新分片

重新分片由Redis的集群管理软件redis-trib负责。

  1. 让目标节点准备导入属于槽的键值对
  2. 让源节点准备将槽的键值对迁移至目标节点
  3. 获取最多count个属于槽的键名
  4. 将键名的节点迁移至目标节点,直到所有迁移完全为止
  5. 同步槽信息给其他节点