1 Redis服务器结构
Redis服务器将所有数据库都保存在redis.h/redisServer结构中,这里面最重要的一个结构是 redisDb *db,这是一个数组,保存着服务器中的所有数据库。
在redis初始化的时候,程序会根据dbnum属性来决定创建多少个数据库,这个属性由配置文件里的database选项来决定,默认是16,故而redis服务器默认创建16个数据库。
1.1 目标数据库
每个redis客户端都有自己的目标数据库,redis的读写操作都是针对目标数据库的。
默认情况下,redis的目标数据库是0号数据库,用户可以使用select命令来切换目标数据库。SELECT 1
命令即为使目标数据库为1号数据库。
用来存储客户端状态的redisClient对象中的db属性记录了客户端当前的目标数据库,这个指针指向了redisDb数组的某个节点。
Select就是通过改变db属性的指针而切换目标数据库的。
1.2 数据库键空间
上文说过redisDb结构表示一个数据库,其中的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间。我们对键值对的增删改查,都是对这个键空间的操作。
使用字典的好处是,随着数据量的增大,定位到某个键值对的时间复杂度并不会增加太厉害。很适合redis数据库的情况。
1.3 过期字典
要理解过期字典,先知道生存时间这一概念:
1.3.1 设置过期时间
Redis可以设置键的生存时间或过期时间:
- expire命令或者pexpire命令,客户端可以以秒(前者)或者毫秒(后者)精度,为数据库中某个键设置生存时间。
expire命令或者pexpire命令为一个key设置生存时间,注意,这个生存时间设置在key上,除非是用新的k-v来取代这个k-v,否则,只改变其value(可能是链表,集合,哈希等等)的结构,或者只是重命名这个key,或者对key进行自增自减操作,本质上都不会改变key上的生存时间。
- Persist命令可以移除一个键的生存时间设置。
- TTL 命令可以查看一个带有生存时间限制的键的剩余时间,以秒为单位,底层实现是以过期时间戳减去当前时间。(PTTL以毫秒为单位)
1.3.2 过期字典空间
回到我们的redisDb结构,我们可以看到一个dict数组叫做expires
Expires字典保存了数据库中所有键的过期时间,这个键就是过期字典。
- 过期字典的键是一个指针,指向了键空间中的某个键对象(所以才说生存时间是针对key的)
- 过期字典的值是一个long long 类型的整数,这个整数保存了键所指向的键对象的过期时间——以毫秒为精度的一个时间戳(所以底层其实都是用PEXPIREAT命令)
1.3.3 过期键删除策略
Redis使用了惰性删除(在使用到键时才去判断是否过期以及删除)和定期删除(每隔一段时间删除库中的过期键)两种结合的删除策略
- db.c/expireIfNeeded函数负责惰性删除,
- redis.c/activeExpireCycle函数实现定期删除,它在规定的时间内,分多次遍历服务器中的各个数据库,从过期字典中随机检查一部分键的过期时间,并删除过期键。
2 Redis的事务
1 | //我们首先使用MULTI命令告诉Redis: |
Redis的事务和其他的事务一样保证最基本的原子性,只要执行exec命令,事务就会执行,执行过程中,一个命令失败,事务中的命令就全部取消。
redis没有类似关系型数据库的回滚功能,烂摊子只能自己收拾。
3 Redis的锁
Redis的Watch命令具有乐观锁的功能,WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。
监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值)
WATCH只负责 当被监控的键值被修改后 阻止紧随其后的一个事务的执行,而不能保证其他客户端不修改这一键值。
1 | redis > SET key 1 |
4 Redis的持久化
为了防止因为突发情况,导致Redis数据库的数据丢失,我们需要对Redis做持久化。Redis如何做持久化呢?有两种方式:
- RDB(快照)持久化
- AOF
4.1 RDF
RDB(快照)持久化:保存某个时间点的全量数据快照,生成RDB文件在磁盘中。RDB文件是一个压缩过的二进制文件,可以还原为Redis的数据。
4.1.1 触发和载入方式
手动触发方式
- SAVE命令:阻塞Redis的服务器进程,直到RDB文件被创建完毕,阻塞期间服务器不能处理任何命令请求。
- BGSAVE命令:Fork出一个子进程来创建RDB文件,不阻塞服务器进程。lastsave 指令可以查看最近的备份时间。
载入方式
- Redis没有主动载入RDB文件的命令,RDB文件是在服务器启动时自动载入,只要Redis服务器检测到RDB文件的存在,即会载入。且载入过程,服务器也会是阻塞状态。
自动触发方式
根据redis.conf配置里的save m n定时触发(用的是BGSAVE),m表示多少时间内,n表示修改次数。save可以设置多个条件,任意条件达到即会执行BGSAVE命令。
1
2
3save 900 1 //设置条件1,即服务器在900秒内,对数据库进行了至少1次修改,即会触发BGSAVE
save 300 10 //设置条件2,即服务器在300秒内,对数据库进行了至少10次修改,即会触发BGSAVE
save 60 1000 //设置条件3,即服务器在60秒内,对数据库进行了至少1000次修改,即会触发BGSAVEredis如何保存自动触发方式的save配置呢?
- redisServer结构中维护了一个saveParam的数组,数组每个saveParam都存储着一个save条件,如下图:
- 前文所述三个save,其saveParam的数组将会是下图的样子
- redisServer结构中维护了一个saveParam的数组,数组每个saveParam都存储着一个save条件,如下图:
自动触发方式如何实现的呢?
- redisServer结构维护了一个dirty计数器和lastsave属性。
- dirty计数器记录了上次SAVE或者BGSAVE之后,数据库执行了多少次的增删改,当服务器成功执行一个修改命令后,程序就会对该值+1,(对集合操作n个元素,dirty+n)。SAVE或者BGSAVE命令执行后,dirty计数器清零。
- lastsave属性是一个unix时间戳,记录了服务器上次成功执行SAVE或者BGSAVE命令的时间。
- Redis服务器有个周期性操作函数serverCron,默认每100毫秒执行一次,它其中一项工作就是检查saveParam保存的条件,并根据dirty和lastsave字段判断是否有哪一条条件已经被满足。
4.1.2 RDB文件的结构
不加赘述,详见让你彻底了解RDB存储结构
4.2 AOF
除了RDB持久化以外,Redis还提供了AOF(append only file)持久化功能,和RDB通过保存数据库的键值对来记录状态不同,AOF持久化是通过保存Reids服务器所执行的写命令来记录数据库状态的。
4.2.1 AOF持久化的实现
AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤。
- 命令追加
- 当AOF持久化功能处于打开状态时,服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓存区的末尾。
- AOF文件的写入和同步
- Redis的服务器进程就是一个事件循环。
- 每次结束一个事件循环之前,都会调用flushAppendOnlyFile函数,考虑是否将缓冲区的内容写入和保存到AOF文件里面。
- flushAppendOnlyFile函数根据配置项appendsync的不同选值有不同的同步策略。
4.2.2 AOF文件的载入
Redis读取AOF文件并还原数据库状态的详细步骤如下:
- 服务器创建一个不带网络连接的伪客户端(fake client)(因为Redis的命令只能在客户端上下文中执行);
- 从AOF文件中分析并读取出一条写命令。
- 执行命令。
- 一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止。
4.2.3 AOF重写
体积过大的AOF文件很可能对Redis服务器、甚至整个宿主计算机造成影响,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。
为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能。
通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多。
我们称新的AOF文件为AOF重写文件,AOF重写文件不是像AOF一样记录每一条的写命令,也不是对AOF文件的简单复制和压缩。AOF重写是通过读取当前Redis数据库状态来实现的。
比如一个animals键,我们有如下操作:
在AOF中,我们要保存四条写命令,而在AOF重写文件中,我们使用一条SADD animals "Dog" "Panda" "Tiger" "Lion" "Cat"
来替代四条命令。
从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,即直接从结果反推命令,这就是AOF重写功能的实现原理。(比如连续6条RPUSH命令会被整合成1条)
在实际中,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量(默认为64)的值,那么重写程序将使用多条命令来记录键的值,而不单单使用一条命令。例如如果SADD后面加入的元素为90条,那么会分成两条SADD,第一条SADD 64个元素,第二条SADD 36个元素。
4.2.3 AOF后台重写
Redis服务器是单线程,如果由服务器发起AOF重写,那么服务器将阻塞。为了防止这个情况,Redis使用子进程进行AOF重写,在这同时主进程仍然在接受并处理客户端的请求。
因为在子线程重写的过程中,主线程也在处理请求导致数据库状态变化,为了解决数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用。
在子线程重写的过程中,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区。
这样一来可以保证:
- AOF缓冲区的内容会定期被写入和同步到AOF文件,对现有AOF文件的处理工作会如常进行。
- 从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面。
当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:
将AOF重写缓冲区中的所有内容写入到AOF重写文件中,这时AOF重写文件所保存的数据库状态将和服务器当前的数据库状态一致。
对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换。
在整个AOF后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低。