Redis设计与实现读书笔记-AOF,RDB,复制
Redis
Redis特性:
1.Redis是一个键值对数据库服务器, 服务器中通常包含着任意个非空数据库, 而每个非空数据库中又可以包含任意个键值对,为了方便起见, 我们将服务器中的非空数据库以及它们的键值对统称为数据库状态。
2.Redis是内存数据库,它将自己的数据库状态储存在内存里面,一旦服务器进程退出, 服务器中的数据库状态也会消失不见。所以redis有两种机制可以将数据库状态保存到磁盘里面:RDB,AOF。
RDB
1. RDB两种执行方式:
a) RDB持久化既可以手动执行
b) RDB持久化可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个ROB文件中
2.RDB持久化功能:
a)所生成的ROB文件是一个经过压缩的二进制文件,
b)通过该文件可以还原生成ROB文件时的数据库状态。
ROB文件的创建与载入
创建
一、手动创建
有两个Redis命令可以用于生成ROB文件, 一个是SAVE, 另一个是BGSAVE。
SAVE:命令会阻塞Redis服务器进程,直到ROB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
BGSAVE :和SAVE命令直接阻塞服务器进程的做法不同,命令会派生出一个子进程, 然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。
在BGSAVE命令执行期间, 服务器处理SAVE、 BGSAVE、 BGREWRITEAOF三个命令的方式会和平时有所不同。
1. SAVE命令会被服务器拒绝,服务器禁止SAVE命令和BGSAVE命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个rdbSave调用,防止产生竞争条件。(进程竞争)
2.BGSAVE命令会被服务器拒绝,因为同时执行两个BGSAVE命令也会产生竞争条件。(进程竞争)
3. BGREWRITEAOF:BGREWRITEAOF和BGSAVE两个命令的实际工作都由子进程执行,所以这两个命令在操作方面并没有什么冲突的地方,不能同时执行它们只是一个性能方面的考虑一并发出两个子进程, 并且这两个子进程都同时执行大量的磁盘写入操作, 这怎么想都不会是一个好主意。(无进程竞争问题,但两个进程工作量都很大)
二、自动创建
因为BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。
用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。
dirty 计数器和 lastsave 属性
dirty计数器:记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器 对 数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、 删除、 更新等操作)。
lastsave属性:是一个UNIX时间戳, 记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间。
载入
服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。
和使用SAVE命令或者BGSAVE命令创建RDB文件不同,RDB文件的载人工作是在服务器启动时自动执行的,所以Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件
另外值得一提的是,因为AOF文件的更新频率通常比ROB文件的更新频率高,所以:
1.如果服务器开启了AOF持久化功能,那么服务器会优先使用 AOF文件来还原数据 库状态。
2.只有在AOF持久化功能处于关闭状态时, 服务器才会使用RDB文件来还原数据库状态。 服务器判断该用哪个文件来还原数据库状态的流程如下图所示。
载入ROB文件的实际工作由rdb.c/rdbLoad函数完成, 这个函数和rdbSave函数之间的关系可以下图表示
AOF
AOF持久化是通过保存 Redis服务器所执行的写命令来记录数据库状态的, 如下图所示
命令追加
当AOF持久化功能处于打开状态时, 服务器在执行完一个写命令之后, 会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
文件写入与同步
Redis的服务器进程就是一个事件循环(loop), 这个循环中的文件事件负责接收客户端 的命令请求, 以及向客户端发送命令回复。
服务器每次结束一个事件循环之前, 它都会调用flushAppendOnlyFile函 数, 考虑是否需要将aof_buf缓冲区中的内容写人和保存到AOF文件里面
flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值(默认everysec)来决定,各个不同值产生的行为如下表所示。
文件载入与还原
因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读人并重新执行一遍AOF文件里面保存的写命令, 就可以还原服务器关闭之前的数据库状态。
Redis读取AOF文件并还原数据库状态的详细步骤如下:
1) 创建一个不带网络连接的伪客户端(fake client)。
2) 从AOF文件中分析并读取出一条写命令。
3) 使用伪客户端执行被读出的写命令。
4) 一直执行步骤2和步骤3, 直到AOF文件中的所有写命令都被处理完毕为止。
载入过程如下图:
AOF重写
aof_rewrite(单进程):读取服务器当前数据库状态,将命令写入AOF文件中,使用新的AOF文件替换旧的AOF文件(缺点,单进程,重写执行了大量写入操作,会使函数的线程被长时间阻塞,无法处理客户端发来的命令请求)
BGREWRITEAOF(后台重写):服务器fork出子进程执行重写工作,但在重写期间,新的写命令会被丢失,为解决数据不一致的问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用。
在子进程执 行AOF重写期间,服务器进程需要执行以下三个工作:
1)执行客户端发来的命令。
2)将执行后的写命令追加到AOF缓冲区。
3)将执行后的写命令追加到AOF重写缓冲区。
当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:
1)将AOF重写缓冲区中的所有内容写人到新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
2)对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两 个AOF文件的替换。
这个信号处理函数执行完毕之后,父进程就可以继续像往常一样接受命令请求了。
在整个AOF后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞(在同步重写缓冲区的内容到aof文件中时),在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低。
复制
在Redis中, 用户可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制(replicate)另一个服务器。
主服务器(master):被复制的服务器为
从服务器(slave):对主服务器进行复制的服务器
数据库状态一致:进行复制中的主从服务器双方的数据库将保存相同的数据
旧版复制功能的实现
Redis的复制功能分为同步(sync)和命令传播(command propagate)两个操作:
同步操作:用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
命令传播操作:用于在主服务器的数据库状态被修改,导致主从服务器的数据库状
态出现不一致时,让主从服务器的数据库重新回到一致状态。
同步
从服务器对主服务器的同步操作需要通过向主服务器发送SYNC命令来完成, 以下是SYNC命令的执行步骤:
1)从服务器向主服务器发送SYNC命令。
2)收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。
3)当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB 文件发送给从服务器,从服务器接收并载人这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。
4)主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。
命令传播
在同步操作执行完毕之后, 主从服务器两者的数据库将达到一致状态,但这种一致并不是一成不变的, 每当主服务器执行客户端发送的写命令时, 主服务器的数据库就有可能会被修改, 并导致主从服务器状态不再一致。
为了让主从服务器再次回到一致状态, 主服务器需要对从服务器执行命令传播操作:
- 主服务器会将自己执行的写命令, 也即是造成主从服务器不一致的那条写命令,发送给从服务器执行
- 从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态。
也就是说。命令传播是在同步完成后,主服务器将每次执行的写命令发送给从服务器执行,以此达到主从一致
旧版复制功能的缺陷
在Redis中,从服务器对主服务器的复制可以分为以下两种情况:
初次复制:从服务器以前没有复制过任何主服务器,或者从服务器当前要复制的主 服务器和上一次复制的主服务器不同。
断线后重复制:处千命令传播阶段的主从服务器因为网络原因而中断了复制,但从服务器通过自动重连接重新连上了主服务器,并继续复制主服务器。
缺陷:初次复制可很好地完成任务,但对于断线后重复制来说,旧版复制功能虽然也能让主从服务器重新回到一致状态,但效率却非常低-为了执行断电期间少量的写命令,让主服务器重新执行一次sync命令,非常耗资源
新版复制功能的实现
为了解决旧版复制功能在处理断线重复制情况时的低效问题,Redis从2.8版本开始,使用PSYNC命令代替SYNC命令来执行复制时的同步操作。
PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partialresynchronization) 两种模式。
完整重同步:用于处理初次复制情况,执行步骤和SYNC命令的执行步骤基本一样,它们都是通过让主服务器创建并发送RDB文件,以及向从服务器 发送保存在缓冲区里面的写命令来进行同步。
部分重同步:则用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务 器当前所处的状态。
部分重同步的实现
部分重同步功能由以下三个部分构成:
1.主服务器的复制偏移量(replication offset) 和从服务器的复制偏移量。
2.主服务器的复制积压缓冲区(replication backlog)。
3.服务器的运行ID (run ID)。
复制偏移量
执行复制的双方一主服务器和从服务器会分别维护一个复制偏移量:
主服务器:每次向从服务器传播N 个字节的数据时,就将自己的复制偏移量的值加上N。
从服务器:每次收到主服务器传播来的N个字节的数据时, 就将自己的复制偏移量的值加上N。
通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态,如果主从服务器处于一致状态,那么主从服务器两者的偏移量总是相同的。
相反,如果主从服务器两者的偏移量并不相同, 那么说明主从服务器并未处于一致状态。
复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列, 默认大小为1MB。
当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到 复制积压缓冲区里面
因此,主服务器的复制积压缓冲区里面会保存着一部分最近传播的写命令,并且复制积
压缓冲区会为队列中的每个字节记录相应的复制偏移量。
当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量 offset发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作:
1.如果offset偏移量之后的数据(也即是偏移量offset+1开始的数据)仍然存在于复制积压缓冲区里面,主服务器向从服务器发送+CONTINUE回复,那么主服务器将对从服务器执行部分重同步操作。
2.相反,如果offset偏移量之后的数据已经不存在于复制积压缓冲区, 那么主服务器将对从服务器执行完整重同步操作。
3. 接着主服务器会将复制积压缓冲区 10086 偏移址之后的所有数据(偏移量为 10087 至 10119) 都发送给从服务器。
复制积压缓冲区的最小大小可以根据公式second * write_size_per_second 来估算:
其中second为从服务器断线后重新连接上主服务器所需的平均时间(以秒计算)。
而write_size_per_second则是主服务器平均每秒产生的写命令数据量(协议格式的写命令的长度总和)。
服务器运行ID
每个Redis服务器, 不论主服务器还是从服务,都会有自己的运行1D。
运行1D在服务器启动时自动生成,由40个随机的十六进制字符组成,例如53b9b 28df8042fdc9ab5e3fcbbbabffld5dce2b3。
- 当从服务器对主服务器进行初次复制时,主服务器会将自己的运行1D传送给从服务器, 而从服务器则会将这个运行1D保存起来。
- 当从服务器断线并重新连上 个主服务器时,从服务器将向当前连接的主服务器发送之前保存的运行ID。
- 如果从服务器保存的运行ID和当前连接的主服务器的运行ID相同,那么说明从服务器断线之前复制的就是当前连接的这个主服务器,主服务器可以继续尝试执行部分重同步操作。相反地, 如果从服务器保存的运行1D和当前连接的主服务器的运行1D并不相同,那么说明从服务器断线之前复制的主服务器并不是当前连接的这个主服务器,主服务器将对从服务器执行完整重同步操作。
PSYNC命令的实现
PSYNC命令的调用方法有两种:
1.如果从服务器以前没有复制过任何主服务器, 或者之前执行过SLAVEOF no one命令, 那么从服务器在开始一次新的复制时将向主服务器发送PSYNC ? -1命令, 主动请求主服务器进行完整重同步(因为这时不可能执行部分重同步)。
2.相反地,如果从服务器已经复制过某个主服务器,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC <runid> <offset>命令:其中runid是上一次复制的主服务器的运行ID,而offset则是从服务器当前的复制偏移量,接收到这个命令的主服务器会通过这两个参数来判断应该对从服务器执行哪种同步操作 。
根据情况,接收到PSYNC命令的主服务器会向从服务器返回以下三种回复的其中一种:
- 如果主服务器返回+FULLRESYNC <runid> <offset>回复,那么表示主服务器将与从服务器执行完整重同步操作:其中runid是这个主服务器的运行ID, 从服务器会将这个ID保存起来,在下一次发送PSYNC命令时使用;而offse七则是主服务器当前的复制偏移量,从服务器会将这个值作为自己的初始化偏移量。
- 如果主服务器返回+CONTINUE 回复,那么表示主服务器将与从服务器执行部分重同步操作,从服务器只要等着主服务器将自己缺少的那部分数据发送过来就可以了。
- 如果主服务器返回-ERR 回复,那么表示主服务器的版本低千Redis2.8, 它识别不了PSYNC命令,从服务器将向主服务器发送SYNC命令,并与主服务器执行完整同步操作。
复制的实现
Step1:设置宁服务器的地址和端口
客户端向从服务器发送SLAVEOF命令,从服务器首先要做的就是将客户端给定的主服务器IP地址127.0.0.1 以及端口6379保存到服务器状态的masterhost属性和masterport属性里面。SLAVEOF命令是一个异步命令, 在完成masterhost属性和masterport属性的设
置工作之后,从服务器将向发送SLAVEOF命令的客户端返回OK, 表示复制指令已经被接
收, 而实际的复制工作将在OK返回之后才真正开始执行。
Step2: 建立套接字连接
在SLAVEOF命令执行之后,从服务器将根据命令所设置的IP地址和端口,创建连向主服务器的套接字连接,如下图所示。
如果从服务器创建的套接字能成功连接(connect)到主服务器,那么
从服务器将为这个套接字关联一个专门用于处理复制工作的文件事件处理器,这个处理器将负责执行后续的复制工作,比如接收RDB文件,以及接收主服务器传播来的写命令, 诸如此类。
主服务器在接受(accept)从服务器的套接字连接之后,将为该套接字创建相应的客户端状态,并将从服务器看作是一个连接到主服务器的客户端来对待, 这时从服务器将同时具有服务器(server)和客户端(client)两个身份:从服务器可以向主服务器发送命令请求,而主服务器则会向从服务器返回命令回复。
Step3: 发送PING命令
从服务器成为主服务器的客户端之后,做的第一件事就是向主服务器发送一个PING命令。
这个PING命令有两个作用:
1.虽然主从服务器成功建立起了套接字连接,但双方井未使用该套接字进行过任何通信,通过发送PING命令可以检查套接字的读写状态是否正常。
2.因为复制工作接下来的几个步骤都必须在主服务器可以正常处理命令请求的状态下 才能进行,通过发送PING命令可以检查主服务器能否正常处理命令请求。
从服务器在发送PING命令之后将遇到以下三种情况的其中一种:
1.如果主服务器向从服务器返回了一个命令回复,但从服务器却不能在规定的时限(timeout)内读取出命令回复的内容,那么表示主从服务器之间的网络连接状态不 佳,不能继续执行复制工作的后续步骤。 当出现这种情况时,从服务器断开并重新创建连向主服务器的套接字。
2.如果主服务器向从服务器返回一个错误,那么表示主服务器暂时没办法处理从服务器的命令请求,不能继续执行复制工作的后续步骤。 当出现这种情况时,从服务器断开并重新创建连向主服务器的套接字。 比如说, 如果主服务器正在处理一个超时运行的脚本, 那么当从服务器向主服务器发送PING命令时,从服务器将收到主服务器返回的BUSY Redisis busy running a scrip七. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. 错误。
3.如果从服务器读取到"PONG" 回复, 那么表示主从服务器之间的网络连接状态正常, 并且主服务器可以正常处理从服务器(客户端)发送的命令请求, 在这种情况下, 从服务器可以继续执行复制工作的下个步骤。
Step4: 身份验证
1.如果从服务器设置了masterauth选项,那么进行身份验证。
2.如果从服务器没有设置masterauth选项,那么不进行身份验证。
在需要进行身份验证的情况下,从服务器将向主服务器发送一条AUTH命令,命令的参数为从服务器masterau吐选项的值。
从服务器在身份验证阶段可能遇到的情况有以下几种:
1.如果主服务器没有设置requirepass选项,并且从服务器也没有设置masterauth 选项, 那么主服务器将继续执行从服务器发送的命令, 复制工作可以继续进行。
2.如果从服务器通过AUTH命令发送的密码和主服务器requirepass选项所设置的密码相同, 那么主服务器将继续执行从服务器发送的命令, 复制工作可以继续进行。与此相反, 如果主从服务器设置的密码不相同,那么主服务器将返回一个 invalid password错误。
3.如果主服务器设置了requirepass选项,但从服务器却没有设置masterauth选项,name主服务器将返回一个NOAUTH错误。另一方面, 如果主服务器没有设置requirepass选项, 但从服务器却设置了masterauth选项, 那么主服务器将返回一个no password is set错误。
Step5发送端口信息
在身份验证步骤之后,从服务器将执行命令REPLCONF listening-por 七<portnumber>,
向主服务器发送从服务器的监听端口号。
主服务器在接收到这个命令之后, 会将端口号记录在从服务器所对应的客户端状态的
slave_listening_port属性中。
Step6:同步
在这一步,从服务器将向主服务器发送PSYNC命令,执行同步操作,并将自己的数据库更新至主服务器数据库当前所处的状态。
值得一提的是,在同步操作执行之前,只有从服务器是主服务器的客户端,但是在执行同步操作之后, 主服务器也会成为从服务器的客户端:
1.如果PSYNC命令执行的是完整重同步操作,那么主服务器需要成为从服务器的客户端,才能将保存在缓冲区里面的写命令发送给从服务器执行。
2.如果PSYNC命令执行的是部分重同步操作,那么主服务器需要成为从服务器的客户端,才能向从服务器发送保存在复制积压缓冲区里面的写命令。
Step7:命令传播
当完成了同步之后, 主从服务器就会进人命令传播阶段, 这时主服务器只要执行的写命令发送给从服务器, 而从服务器只要一直接收主服务器发来的命令,就可以保证主从一致了
心跳检测
在命令传播阶段, 从服务器默认会以每秒一次的频率, 向主服务器发送命令: REPLCONF ACK <replication_offset>
其中 replication_offset 是从服务器当前的复制偏移量。
发送REPLCONFACK命令对于主从服务器有三个作用:
1.检测主从服务器的网络连接状态。
2.辅助实现 min-slaves 选项。
3.检测命令丢失。