redis设计与实现学习---(二)单机数据库的实现

一 数据库

注意:下面所有serverCron的操作的定时都是都是默认100毫秒一次,可以通过hz(默认为10 )配置
1 服务器状态
初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库,
dbnum可以通过配置文件中的database决定,默认16。
在这里插入图片描述

2 客户端状态
每个客户端中间有一个数据库指针,通过select index可以切换指向的数据库
在这里插入图片描述

3 数据库
其中数据库键空间中所有的键都是字符串键对象,值是不同数据结构对象。
过期时间中键与键空间共享同一个键对象,值存的是一个long类型的时间戳。
在这里插入图片描述

4 键空间的维护操作
除了对键值对的CRUD本身操作之外,还会在做操作的同时进行键空间的维护操作。
① 读取一个键之后会根据键是否存在来 更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数,这两个值可以在INFO stats命令的key space_hits属性和key space_misses属性中查看。
② 在读取一个键之后,服务器会更新键的LRU(最后一次使用)时间,这个值可以用于计 算键的闲置时间,使用OBJECT idletime命令可以查看键key的闲置时间。
③ 如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键,然后 才执行余下的其他操作
④ ·如果有客户端使用WATCH命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务程序注意到这个键已经被修改过。服务器每次修改一个键之后,都会对脏(dirty)键计数器的值增1,这个计数器会触发服务器的持久化以及复制操作
⑤ 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知(notify-keyspace-events属性配置)

5 键的过期操作
EXPIRE / PEXPIRE 设置精确的秒/毫秒事件后过期
EXPIREAT / PEXPIREAT 设置精确的某个秒或者毫秒时间戳后过期
上面的四个操作实际上都是按照PEXPIREAT根据当前时间计算出某个毫秒级的时间戳并设置redisDb.expires中的键以及过期时间。
PERSIST表示移除某个KEY的过期时间,实际操作就是删除redisDb.expires中的KEY。
TTL以及PTTL表示根据当前时间-redisDb.expires.KEY过期时间计算出来的剩余秒数或毫秒数,如果在redisDb.expires中没有找到对应的KEY指针,则表示这个KEY没有设置过期时间,默认返回-1,如果在redisDb.dict中也没有这个KEY则表示不存在,返回-2。

6 过期键的删除策略
① 定时删除,实时删除数据,但对CPU不友好,尤其是当内存空间足够时并不需要浪费CPU去做删除。
② 惰性删除,只在使用时才会检测是否过期,对于访问频率比较低的KEY可能导致内存泄漏。
③ 定期删除,定时与惰性的折中,CPU隔一段时间检测是否有过期数据,并且在使用KEY时对KEY做校验。

7 redis定期删除的实现
①惰性删除:在对每个KEY做操作时都会调用expireIfNeeded函数判断KEY是否过期,如果过期则删除,没有过期则无动作。
②定期删除:redis周期操作的总函数serverCron中调用activeExpireCycle函数。
函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
全局变量current_db会记录当前activeExpireCycle函数检查的进度,并在下一次 activeExpireCycle函数调用时,接着上一次的进度进行处理。比如说,如果当前 activeExpireCy cle函数在遍历10号数据库时返回了,那么下次activeExpireCycle函数执行时,将从11号数据库开始查找并删除过期键。
随着activeExpireCycle函数的不断执行,服务器中的所有数据库都会被检查一遍,这时函数将current_db变量重置为0,然后再次开始新一轮的检查工作。

8 redis内存不足时KEY的处理策略
① noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
② allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
③ allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
④ volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
⑤ volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
⑥ volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

9 RDB对过期KEY的处理
① 生成RDB文件时,过期的KEY不会写入文件
② 读取RDB文件时,如果redis进程属于主服务器模式,则在载入RDB文件时会忽略过期的KEY。如果redis进程属于从服务器模式,则在载入RDB文件时,不论是否过期,KEY都会载入。不过主从服务器进行数据同步的时候,从服务器数据都会被清空,所以也不影响。

10 AOF对过期KEY的处理
① AOF文件写入,在服务器以AOF持久化运行时,因为只会记录每次写入的操作,所以在检测到某个KEY过期时,只会追加一条DEL KEY的命令。
② AOF文件重写,再重写的过程中会对数据库的KEY进行检查,已经过期的KEY不会被记录在新的AOF文件中。
③ 当服务器运行在复制模式时,从服务器读到一个过期的KEY不会做任何操作,只有当主服务器发现KEY过期时,会向所有从服务器发送一条DEL命令,这时从服务器才会执行命令删除过期KEY。

11 数据库通知
数据库的通知操作分为两种,一种是key-space notification(用来获取某个KEY的具体操作),一种是key-event notification(用来获取某个redis指令对哪些KEY做了操作)。通知采用了redis的发布订阅模式发送消息,默认通知时关闭状态,需要在配置文件中配置notify-keyspace-events参数(K或者E是必要的)。
客户端监听采用
subscribe keyevent@0:redis指令
subscribe keyspace@0:key
在这里插入图片描述

二 RDB持久化

1 RDB文件的创建与载入
RDB保存的是二进制文件,生成RDB文件的命令包括SAVE以及BGSAVE。
SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。
SAVE与BGSAVE都会调用rdb.c/rdbSave函数来创建RDB文件。
在这里插入图片描述

在RDB载入时服务器处于阻塞状态,直到全部载入才可以正常对外服务。

2 BGSAVE与其他SAVE执行时的冲突
在BGSAVE命令执行期间,客户端发送的SAVE命令会被服务器拒绝,服务器禁止 SAVE命令和BGSAVE命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个 rdbSave调用,防止产生竞争条件。
在BGSAVE命令执行期间,客户端发送的BGSAVE命令会被服务器拒绝,因为同时执行两个BGSAVE命令也会产生竞争条件。
BGREWRITEAOF(重写AOF,也是子进程执行的)和BGSAVE两个命令不能同时执行(本身没有冲突,但为了防止两个进程同时进行大量IO所以也不能同时执行):
如果BGSAVE命令正在执行,那么客户端发送的BGREWRITEAOF命令会被延迟到BGSAVE命令执行完毕之后执行。
如果BGREWRITEAOF命令正在执行,那么客户端发送的BGSAVE命令会被服务器拒绝。

3 BGSAVE自动触发的实现方式
可以在配置文件中通过设置save来触发,默认为save 900 1, save 300 10, save 60 10000。
代码实现方式:
serverCron默认每100毫秒执行一次,根据saveParam的值以及dirty以及lastsave来判断是否需要执行BGSAVE。
在这里插入图片描述

4 RDB文件结构
RDB保存的是二进制数据,直接打开会乱码,可以使用命令od -cx dump.rdb查看(下面的结构只是db_version第六版的,每个版本可能中间有加的内容,但都有这几个字段)。
在这里插入图片描述

① 开头固定为5个字符 R E D I S,表示这是一个rdb文件
② db_version固定4个字节,表示这是哪一个版本的rdb文件
③ databases部分包含着零个或任意多个数据库,以及各个数据库中的键值对数据
④ EOF常量的长度为1字节,这个常量标志着RDB文件正文内容的结束,当读入程序遇到这 个值的时候,它知道所有数据库的所有键值对都已经载入完毕了。
⑤ check_sum是一个8字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对REDIS、db_version、databases、EOF四个部分的内容进行计算得出的。用来检查文件是否损坏

下面是每个database中包含的字段:
在这里插入图片描述

① SELECTDB常量的长度为1字节,当读入程序遇到这个值的时候,它知道接下来要读入 的将是一个数据库号码。
② db_number保存着一个数据库号码,根据号码的大小不同,这个部分的长度可以是1字 节、2字节或者5字节。当程序读入db_number部分之后,服务器会调用SELECT命令,根据读 入的数据库号码进行数据库切换,使得之后读入的键值对可以载入到正确的数据库中。
③ key_value_pairs部分保存了数据库中的所有键值对数据,如果键值对带有过期时间,那么过期时间也会和键值对保存在一起。根据键值对的数量、类型、内容以及是否有过期时间 等条件的不同,key_value_pairs部分的长度也会有所不同(这个每个键值还会记录类型,编码等,可以参考redisObject的结构)。

三 AOF持久化

1 AOF持久化的实现
AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三 个步骤。
命令追加:
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
在这里插入图片描述

文件写入与同步:
Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面。
在这里插入图片描述

由于调用的是操作系统的write函数,只会把文件存入内存缓冲区,所以可能会存在丢失数据的风险,需要进行文件的同步,即同步写入磁盘文件。为此,系统提供了fsync和fdatasync两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面,从而确保写入数据的安全性。

2 AOF持久化的效率和安全性
AOF_FSYNC_NO :不保存。
AOF_FSYNC_EVERYSEC :每一秒钟保存一次。
AOF_FSYNC_ALWAYS :每执行一个命令保存一次。

WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。
在这里插入图片描述

AOF_FSYNC_NO:
在这种模式下,每次调用 flushAppendOnlyFile 函数,WRITE都会被执行, 但SAVE 会被略过。
在这种模式下,SAVE只会在以下任意一种情况中被执行:
Redis 被关闭
AOF功能被关闭
系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)
这三种情况下的 SAVE 操作都会引起 Redis 主进程阻塞。

AOF_FSYNC_EVERYSEC :
在这种模式中, SAVE原则上每隔一秒钟就会执行一次,这种情况 SAVE操作是由后台子线程调用的,所以它不会引起服务器主进程阻塞。
根据下图说明可以知道,在“每一秒钟保存一次”模式下,如果在情况1中发生故障停机,那么用户最多损失小于2秒内所产生的所有数据。
如果在情况2中发生故障停机,那么用户损失的数据是可以超过2秒的。
Redis 官网上所说的,AOF在“每一秒钟保存一次”时发生故障,只丢失1秒钟数据的说法,实际上并不准确。
在这里插入图片描述

AOF_FSYNC_ALWAYS:
在这种模式下,每次执行完一个命令之后,WRITE和SAVE都会被执行。
另外,因为SAVE是由Redis主进程执行的,所以在SAVE执行期间,主进程会被阻塞,不能接受命令请求。

3 AOF载入
在开启AOF的情况下,如果有AOF与RGB文件,那么会优先载入AOF文件,因为AOF相对比较实时。
由于AOF文件记录的是明文命令,所以可以直接读取,首先创建一个不带网络链接的伪客户端(lua脚本链接执行同样采用的是伪客户端),然后依次读取并执行命令。

4 AOF重写原理
BGREWRITEAOF重写并不会读取解析旧的AOF文件,而是重新获取当前数据库的最新数据写入一个新的文件然后替换原文件,例如当前有一个集合animals “Dog” “Panda” “Tiger”,会把当前集合的最新数据抽取为命令SADD animals “Dog” “Panda” "Tiger"记录。(这里注意,当集合元素多于redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD=64并不会全堆到一条命令里,而是拆分成多条命令记录)。
在执行AOF重写时,除了fork子进程抽取当前库里的数据之外,由于数据还在源源不断地写入数据库,所以此时服务器会额外记录一个重写缓冲区,每次写入除了原始的aof写入缓冲区之外,还需要把数据写入aof重写缓冲区,等子进程结束之后,会把aof重写缓冲区内的数据一并写入子进程的重写文件内,并清空aof重写缓冲区。同时原子性的替换旧的aof文件。

四 redis文件事件

1 redis事件的分类
Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:
① 文件事件(file event):Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器) 的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
② 时间事件(time event):Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。

2 文件事件
Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器。
文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并根据 套接字目前执行的任务来为套接字关联不同的事件处理器。
文件事件处理器以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同 样以单线程方式运行的模块进行对接

3 文件事件处理器的构成
文件事件处理器的四个组成分别是套接字、I/O多路复用程序、 文件事件分派器(dispatcher),以及事件处理器。
在这里插入图片描述
在这里插入图片描述

4 IO多路复用的类型以及事件类型
I/0多路复用程序可以监听多个套接字的ae.h/AE_READABLE事件和 ae.h/AE_WRITABLE事件,这两类事件和套接字操作之间的对应关系如下:
当套接字变得可读时(客户端对套接字执行write操作,或者执行close操作),或者有 新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行connect操 作),套接字产生AE_READABLE事件。
当套接字变得可写时(客户端对套接字执行read操作),套接字产生AE_WRITABLE事 件。
如果一个套接字又可读又可写的话,那么服务器将先读套接字,后写套接字。
在这里插入图片描述

5 文件事件处理器类型
① 连接应答处理器
② 命令请求处理器
③ 命令回复处理器

6 完整的客户端与服务器链接过程
①假设一个Redis服务器正在运作,那么这个服务器的监听套接字的AE_READABLE事件应该正处于监听状态之下,而该事件所对应的处理器为连接应答处理器。
②如果这时有一个Redis客户端向服务器发起连接,那么监听套接字将产生AE_READABLE事件,触发连接应答处理器执行。处理器会对客户端的连接请求进行应答, 然后创建客户端套接字,以及客户端状态,并将客户端套接字的AE_READABLE事件与命令 请求处理器进行关联,使得客户端可以向主服务器发送命令请求。
③之后,假设客户端向主服务器发送一个命令请求,那么客户端套接字将产生 AE_READABLE事件,引发命令请求处理器执行,处理器读取客户端的命令内容,然后传给 相关程序去执行。
④执行命令将产生相应的命令回复,为了将这些命令回复传送回客户端,服务器会将客户 端套接字的AE_WRITABLE事件与命令回复处理器进行关联。当客户端尝试读取命令回复的 时候,客户端套接字将产生AE_WRITABLE事件,触发命令回复处理器执行,当命令回复处 理器将命令回复全部写入到套接字之后,服务器就会解除客户端套接字的AE_WRITABLE事 件与命令回复处理器之间的关联。

五 redis时间事件

1 时间事件的分类
定时事件:让一段程序在指定的时间之后执行一次。
周期性事件:让一段程序每隔指定时间就执行一次。
一个时间事件主要由以下三个属性组成:
id:服务器为时间事件创建的全局唯一ID(标识号)。ID号按从小到大的顺序递增,新事件的ID号比旧事件的ID号要大。
when:毫秒精度的UNIX时间戳,记录了时间事件的到达(arrive)时间。
timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件(返回值如果是AE_NOMORE则是定时事件,如果是具体的整数则表示在这个整数事件之后再次执行)

2 时间事件的实现
服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历 整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。
因为新的时间事件总是插入到链表的表头,所以时间事件是按ID逆序排序,而不是按when即执行时间排序。

3 serverCron函数
主要工作:
① 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。
② 清理数据库中的过期键值对。 ·关闭和清理连接失效的客户端。
③ 尝试进行AOF或RDB持久化操作。
④ 如果服务器是主服务器,那么对从服务器进行定期同步。
⑤ 如果处于集群模式,对集群进行定期同步和连接测试。
Redis服务器以周期性事件的方式来运行serverCron函数,在服务器运行期间,每隔一段时间,serverCron就会执行一次,直到服务器关闭为止。用户可以通过修改hz选项来调整serverCron的每秒执行次数

六 事件的调度与执行

1 事件流程
在这里插入图片描述
在这里插入图片描述

2 事件执行规则
1)aeApiPoll函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,这个方 法既可以避免服务器对时间事件进行频繁的轮询(忙等待),也可以确保aeApiPoll函数不会 阻塞过长时间。
2)因为文件事件是随机出现的,如果等待并处理完一次文件事件之后,仍未有任何时 间事件到达,那么服务器将再次等待并处理文件事件。随着文件事件的不断执行,时间会逐 渐向时间事件所设置的到达时间逼近,并最终来到到达时间,这时服务器就可以开始处理到 达的时间事件了。
3)对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中 断事件处理,也不会对事件进行抢占,因此,不管是文件事件的处理器,还是时间事件的处 理器,它们都会尽可地减少程序的阻塞时间,并在有需要时主动让出执行权,从而降低造成 事件饥饿的可能性。比如说,在命令回复处理器将一个命令回复写入到客户端套接字时,如 果写入字节数超过了一个预设常量的话,命令回复处理器就会主动用break跳出写入循环,将 余下的数据留到下次再写;另外,时间事件也会将非常耗时的持久化操作放到子线程或者子 进程执行。
4)因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的 实际处理时间,通常会比时间事件设定的到达时间稍晚一些。

七 客户端

1 重点摘要
服务器状态结构使用clients链表连接起多个客户端状态,新添加的客户端状态会被放到 链表的末尾。
客户端状态的flags属性使用不同标志来表示客户端的角色,以及客户端当前所处的状 态。
输入缓冲区记录了客户端发送的命令请求,这个缓冲区的大小不能超过1GB。
命令的参数和参数个数会被记录在客户端状态的argv和argc属性里面,而cmd属性则记 录了客户端要执行命令的实现函数。
客户端有固定大小缓冲区和可变大小缓冲区两种缓冲区可用,其中固定大小缓冲区的最 大大小为16KB,而可变大小缓冲区的最大大小不能超过服务器设置的硬性限制值。
输出缓冲区限制值有两种,如果输出缓冲区的大小超过了服务器设置的硬性限制,那么 客户端会被立即关闭;除此之外,如果客户端在一定时间内,一直超过服务器设置的软性限 制,那么客户端也会被关闭。
当一个客户端通过网络连接连上服务器时,服务器会为这个客户端创建相应的客户端状 态。网络连接关闭、发送了不合协议格式的命令请求、成为CLIENT KILL命令的目标、空转 时间超时、输出缓冲区的大小超出限制,以上这些原因都会造成客户端被关闭。
处理Lua脚本的伪客户端在服务器初始化时创建,这个客户端会一直存在,直到服务器 关闭。
载入AOF文件时使用的伪客户端在载入工作开始时动态创建,载入工作完毕之后关闭。

八 服务器

1 重点摘要
一个命令请求从发送到完成主要包括以下步骤:1)客户端将命令请求发送给服务器; 2)服务器读取命令请求,并分析出命令参数;3)命令执行器根据参数查找命令的实现函 数,然后执行实现函数并得出命令回复;4)服务器将命令回复返回给客户端。
serverCron函数默认每隔100毫秒执行一次,它的工作主要包括更新服务器状态信息, 处理服务器接收的SIGTERM信号,管理客户端资源和数据库状态,检查并执行持久化操作等等。
服务器从启动到能够处理客户端的命令请求需要执行以下步骤:1)初始化服务器状 态;2)载入服务器配置;3)初始化服务器数据结构;4)还原数据库状态;5)执行事件循环。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值