一、数据库
1.1 数据库概念
Redis中通过数据库的物理模型存储数据:
struct redisServer {
// ...
// 一个数组,保存着服务器中的所有数据库
redisDb *db;
// 服务器的数据库数量
int dbnum;
// ...
};
dbnum属性的值由服务器配置的database选项决定,默认情况下,该选项的值为16,所以Redis服务器默认会创建16个数据库
客户端与Redis建立连接后会默认选择0号数据库,不过可以随时使用SELECT命令更换数据库。
redis> SET msg "hello world"
OK
redis> GET msg
"hello world"
redis> SELECT 2
OK
redis[2]> GET msg
(nil)
redis[2]> SET msg"another world"
OK
redis[2]> GET msg
"another world"
1.2 数据库的作用
Redis实例默认建立了16个db,由于不支持自主进行数据库命名所以以dbX的方式命名。默认数据库数量可以修改配置文件的database值来设定。对于db正确的理解应为“命名空间”,多个应用程序不应使用同一个Redis不同库,而一个应用程序对应一个Redis实例,不同的数据库可用于存储不同环境的数据。
因此,数据库是为了应对当只有单机实例时,多个应用使用同一个实例应进行环境隔离。
而在集群的情况下不支持使用select命令来切换db,因为Redis集群模式下只有一个db0。
二、键淘汰策略
2.1 什么是淘汰策略
当我们设置了一个带有超时时间的Key,例如:
redis>expire key_Test 60
1
redis>ttl key_Test
58
当时间过了60S之后,该key的有效期已经过期,正常来说当我们第80S再次访问应该访问不到。
但是,这个Key是什么时候被物理删除的呢?是第60S的时候Redis的定时器准时帮我们删除的?还是是第70S的时候Redis的定时任务帮我们删除的?又或者是是当我们访问的时候Redis发现这个Key已经过期了,第80S的时候删除的?
其实Redis的淘汰策略就存在这三个问题之中:
- 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
2.2 定时删除
定时删除是每次有设置过期键的时候就启动一个定时器,定时器一直轮询查看该Key是否过期,一但发现过期就直接删除。毫无疑问,该方式对内存及其友好,因为过期的Key会基本上立马删除。但是对CPU也及其不友好,假如我们设置了一个长达一年的过期时间,CPU就需要一直轮询一年查看是否过期,一旦数据量过大,CPU的使用率肯定会飙升。
2.3 定期删除
定期删除使用折中策略,比如每隔1S,就抽查一批Key查看其是否过期。这种方式能够避免定时删除中的不停轮询消耗CPU的问题。
2.4 惰性删除
当Key被访问时,会先查看该Key是否过期,如果过期的话就返回空,并且此时再将此Key物理删除。毫无疑问的是该方式对CPU时及其友好的,因为基本上不会存在耗费CPU的操作,同时对内存也是及其不友好的。如果一批Key长时间一直未被访问,那么会一直占用内存空间,极其浪费空间存储效率。
2.5 常用的淘汰策略
Redis使用的淘汰策略是定期删除+惰性删除,通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。
三、RDB持久化
什么是持久化?持久化就是当系统关机或宕机之后,再次启动能够恢复到上一次关机之前的状态。Redis的持久化机制有两种:RDB与AOF。
3.1 RDB命令
我们1.1中讲到,Redis中数据都存在于数据库,而想要做到持久化,只需要将数据库中的所有数据保存下来即可。当Redis再次启动时,只需要将数据重新写入数据库即可完成持久化机制。因此,也可以将RDB简单的理解为存储快照。
RDB命令有两种:SAVE和 BGSAVE,两者的区别是阻塞和非阻塞的。
SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。因此我们生产中一般不会使用此命令。
和SAVE命令直接阻塞服务器进程的做法不同,BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。
def BGSAVE():
# 创建子进程
pid = fork()
if pid == 0:
# 子进程负责创建RDB文件
rdbSave()
# 完成之后向父进程发送信号
signal_parent()
elif pid > 0:
# 父进程继续处理命令请求,并通过轮询等待子进程的信号
handle_request_and_wait_signal()
else:
# 处理出错情况
handle_fork_error()
此外,Redis服务器会优先使用AOF的方式来回滚数据,这是因为AOF的数据更新频率比RDB快。只有当AOF关闭时才会使用RDB。
3.2 RDB文件结构
本章内容将讲述RDB的文件结构,看一下数据是怎么存储在RDB文件中的。
RDB文件主要有以下五个部分:
- REDIS,一个常量字符串,用于标识该文件为RDB文件。
- db_version长度为4字节,它的值是一个字符串表示的整数,这个整数记录了RDB文件的版本号,比如"0006"就代表RDB文件的版本为第六版。
- databases,用于存储数据库中的数据。
- EOD,也为常量,标识数据库部分已经结束。
- check_sum是一个8字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对REDIS、db_version、databases、EOF四个部分的内容进行计算得出的。服务器在载入RDB文件时,会将载入数据所计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或者损坏的情况出现。
3.2.1 databases
databases中可以保存多个数据库,每个数据库的格式为:
其中:
- SELECTDB常量的长度为1字节,当读入程序遇到这个值的时候,它知道接下来要读入的将是一个数据库号码。
- db_number用于标识该数据库的编号。
- key_value_pairs部分保存了数据库中的所有键值对数据,如果键值对带有过期时间,那么过期时间也会和键值对保存在一起。
3.2.2 key_value_pairs
key_value_pairs保存了多个键值对,即我们存入Redis的数据。
其中:
- TYPE记录了value的类型,长度为1字节,一般是以下几种的一种:
REDIS_RDB_TYPE_STRING
REDIS_RDB_TYPE_LIST
REDIS_RDB_TYPE_SET
REDIS_RDB_TYPE_ZSET
REDIS_RDB_TYPE_HASH
......
- key是一个字符串对象,即我们存入的key
- value为具体的值
带有过期时间的键值对:
其中
- EXPIRETIME_MS常量的长度为1字节,它告知读入程序,接下来要读入的将是一个以毫秒为单位的过期时间。
- ms为8位的UNIX时间戳,标识该key的过期时间。
四、AOF持久化
我们与Redis的交互都是通过命令的方式实现的,试想一下,我们除了可以保存数据库的状态,是不是也可以保存所有执行过的命令呢?我们只需要在启动时执行所有执行过的命令,最终数据库的状态也可以达到关机之前的状态。
Redis 服务在启动之后会陷入一个巨大的 while 循环,不停地执行 processEvents 方法处理
文件事件和时间事件,每次循环中执行完上述的两次事件之后,都会执行AOF操作。一次完整的循环。即文件时间+时间时间+AOF操作称之为一次时间循环。
4.1 AOF实现
Redis会开辟一块缓冲区,在每有新的修改类型命令执行时,会同时将该命令写入该缓冲区。此后,每当开始执行AOF操作时,会将该缓冲区的命令通过操作系统写入到硬盘上的AOF文件中。
其中写入硬盘的时机有三种模式:
- always,每次事件循环都会将新的命令写入到磁盘上的AOF文件中。该操作效率最慢,因为需要等待将数据写入磁盘中,但是安全度最高。
- everysec,会判断上次写入操作距今是否已经超过1S,是的话再继续写入。换句话说,每间隔至少一秒钟写入一次磁盘。
- no,不主动写入磁盘,等待操作系统择机写入。该操作效率最高,但安全性较低。
4.2 AOF读取
因为AOF文件中已经包含了所有要执行的命令,因此只需要执行文件中所有的命令即可恢复原来的数据库状态。
Redis的命令只能在客户端执行,因此Redis服务器会创建一个不带网络连接的伪客户端,由该客户端执行这些所有的AOF命令,服务端像正常的服务器一样接受这些命令即可。
4.3 AOF重写
4.3.1 重写实现
随着服务器的运行时长累加,AOF文件的大小肯定会快速膨胀,因此我们需要对AOF文件进行优化重写。
例如:
redis> RPUSH list "A" "B" // ["A", "B"]
(integer) 2
redis> RPUSH list "C" // ["A", "B", "C"]
(integer) 3
redis> RPUSH list "D" "E" // ["A", "B", "C", "D", "E"]
(integer) 5
redis> LPOP list // ["B", "C", "D", "E"]
"A"
redis> LPOP list // ["C", "D", "E"]
"B"
redis> RPUSH list "F" "G" // ["C", "D", "E", "F", "G"]
(integer) 5
经过以上六条命令,数据库中最终只剩下 ["C", "D", "E", "F", "G"],正常情况下如果不对文件进行重写,那么AOF文件中肯定是有6条命令的。但是很明显,这些命令是可以进行优化的,我们可以简单的优化为:
RPUSH list "C" "D" "E" "F" "G"
那么这个优化的过程就是对AOF重写的过程。
然而,Redis并没有对AOF文件进行读取、分析、修改的操作,而是采用了直接读取数据库数据的操作。
例如上述的情况,Redis只需要读取数据库中的 ["C", "D", "E", "F", "G"],并根据类型重新拼装成 RPUSH list "C" "D" "E" "F" "G"这条命令即可。
这是因为,直接读取数据库的操作远比对AOF文件命令进行分析简单的多,因为可能两条对相同Key操作的命令相差很远,此外计算也会比较耗时。
4.3.2 后台重写
我们之前提到,RDB因为阻塞性质的原因,可以通过fork子进程的方式解决此问题。
同样的,AOF也可以通过子进程的形式在后台进行重写。
但是此时会带来一个问题:
子进程在重写期间,主进程还会对数据进行处理,如果不解决此问题,就会造成新的数据丢失的情况。
如果不对K2 K3 K4进行补偿处理,则会造成数据丢失。
因此Redis创建了一个AOF重写缓冲区,在子进程开始执行重写操作时,服务器进程把新写入的命令存入此AOF重写缓冲区,以达到保存数据的效果。等待子进程重写完成之后,再将此缓冲区内的所有命令添加到子进程重新的AOF文件尾部,从而达到了保存新数据的效果。
Q:既然已经有了AOF重写的功能,那一开始的AOF是不是就没必要了?