数据库
多数据库结构
一个Redis实例可以支持多个数据库,当客户端与服务端连接并指定到某个数据库时,两者的结构如下图所示:
redisServer
和redisClient
是两个结构体,前者拥有数据库数组。当客户端使用SELECT
命令选择数据库是,会把客户端的db
指针指向数据库数组中的一个对象。
数据库键空间
Redis的每个数据库都用redisDb
的结构体表示:
dict
属性是一个字典,存储所有的键值对,叫做键空间。每个键都是一个字符串对象,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象当中的一种。
键空间操作
在对数据库进行读写操作时,Redis还会执行一些额外的操作来记录一些指标,比如命中率,可以通过INFO
命令查看。
键的过期时间
当用EXPIRE
/PEXPIRE
/EXPIREAT
/PEXPIREAT
给键设置过期时间时,过期时间会保存在redisDb
对象的expires
字典中:
此处dict
和expires
中值相同的键其实是同一个对象,只是为了简化说明所以划成两份。
TTL
命令和PTTL
命令会计算当前时间和expires
中存储的时间的差值然后输出剩下的生存时间。
过期键的删除策略
Redis删除过期键的策略结合了惰性删除和定期删除两种。
惰性删除
当对键进行读取命令时,Redis会判断该键是否过期,如果过期那么就删除,然后继续执行命令。缺点是,如果一个键一直没有被访问,那么就永远停留在内存里,造成资源浪费,因此Redis还采用了定期删除的策略。
定期删除
当定期删除被触发时,Redis会遍历各个数据库,然后从expires
字典中随机检查一部分键的过期时间,如果过期了那么就删除过期的键。
AOF、RDB和复制功能对过期键的处理
生成RDB文件以及AOF重写
当Redis生成RDB文件或者进行AOF重写时,会对键进行检查,过期的键不会写入到文件中。
载入RDB文件
如果服务器以主服务器模式运行,那么过期的键不会被载入内存;如果服务器以从服务器模式运行,那么过期的键会载入内存。
AOF文件写入
当服务器以AOF持久化模式运行时,如果某个键已经过期但还没有被删除,那么不会有任何影响。当过期键被删除后,Redis会向AOF文件追加一条DEL
命令,显示记录键已被删除。
复制
当服务器运行在主从复制模式下运行时,当主服务器删除一个键后会向从服务器发送一个DEL
命令;当客户端在从服务器读取时即使碰到过期的键也不会删除,只有收到主服务器发来的DEL
命令后才会把键删除。
RDB持久化
Redis提供了RDB持久化功能可以将数据存储在硬盘里,我们可以使用SAVE
或者BGSAVE
命令创建RDB文件,两者的区别是SAVE
命令由服务器进程执行,执行时会阻塞进程,在此期间Redis会阻塞来自客户端的请求,直到RDB文件创建完毕;BGSAVE
命令由子进程执行,执行完毕后会通知服务器进程,因此不会阻塞客户端的请求。
自动创建RDB文件
处了上述两个命令之外,我们还可以通过配置来让Redis自动创建RDB文件,其本质是自动执行BGSAVE
命令。
#自动执行BGSAVE命令的默认配置
save 900 1
save 300 10
save 60 10000
如果你进行了上面的配置,那么只要满足下面三个条件的任意一个,服务器就会执行BGSAVE
命令:
- 900秒内数据库进行了至少1次修改
- 300秒内数据库进行了至少10次修改
- 60秒内数据库进行了至少10000次修改
自动保存RDB文件的配置存储在redisServer
结构体的saveparams
数组中,其数据结构如图所示:
redisServer
还有一个dirty
属性记录了数据库被修改次数,执行SAVE
或BGSAVE
命令成功后会自动归零。Redis通过一个周期性执行的函数,默认每100毫秒对自动配置以及dirty
属性进行检查,如果达到触发条件就执行BGSAVE
命令。
RDB文件结构
RDB文件(版本6)结构如下图所示,图中的层次只是为了展示方便,实际并不存在。
value的编码
RDB文件中的TYPE
属性决定了value的存储格式,下面依次介绍。
字符串对象 [TYPE=REDIS_RDB_TYPE_STRING]
如果字符串对象的编码是REDIS_ENCODING_INT
,那么会以下面的结构存储:
比如:
如果对象的编码是REDIS_ENCODING_RAW
,那么有两种情况:
-
字符串长度小于等于20字节,其存储结构如下:
-
如果字符串长度大于20字节,那么字符串会经过压缩再存储,其结构如下:
REDIS_RDB_ENC_LZF
表示经过LZF算法压缩。
列表对象 [TYPE=REDIS_RDB_TYPE_LIST]
对应的value的编码是REDIS_ENCODING_LINKEDLIST
,其存储结构如图:
集合对象 [TYPE=REDIS_RDB_TYPE_SET]
对应的value的编码是REDIS_ENCODING_HT
,其存储结构类似列表对象:
INTSET编码的集合对象 [TYPE=REDIS_RDB_TYPE_SET_INTSET]
value保存的是一个整数集合,Redis会把整数集合转换为一个字符串对象,然后把转换后出的字符串对象写入RDB文件。
哈希表对象 [TYPE=REDIS_RDB_TYPE_HASH]
对应的value的编码是REDIS_ENCODING_HT
,其存储结构如图:
有序集合对象 [TYPE=REDIS_RDB_TYPE_ZSET]
对应的value的编码是REDIS_ENCODING_SKIPLIST
,其存储结构如图:
ZIPLIST编码的列表、哈希表或有序集合 [TYPE=REDIS_RDB_TYPE_LIST_ZIPLIST,REDIS_RDB_TYPE_HASH_ZIPLIST,REDIS_RDB_TYPE_ZSET_ZIPLIST]
value保存的是一个压缩列表对象,Redis会把它转换为一个字符串对象,然后把转换后出的字符串对象写入RDB文件。
AOF持久化
RDB持久化功能是把数据库的键值对存储到文件中,AOF持久化功能则是把服务器执行的写命令以Redis的命令请求协议格式存储到文件中。
在了解AOF持久化过程前,我们需要先了解Redis的进程模型,其本质是一个事件循环模型,流程图如下:
当执行完一个写命令后,服务器会把命令以协议格式追加到AOF缓冲区的末尾,在每一个事件循环结束前会将AOF缓冲区中的内容写到AOF文件。
需要注意的是,写入文件不代表写入磁盘,现代操作系统中为提高效率,通常在写文件时会把数据写入内存缓冲区,等到缓冲区满时或者超过一定时间后才会真正写入磁盘,这一操作称为同步。
虽然每次事件循环都会写入AOF文件,但是并不是每次都会执行同步操作,Redis提供了appendfsync
配置项,有以下3个选项:
-
always
每一次事件循环都进行同步 -
everysec
每隔一秒进行同步 -
no
由操作系统决定是否同步
还原数据时,Redis会创建一个没有网络连接的伪客户端,依次执行AOF文件中的命令。执行完毕后数据库就恢复了之前的状态。
AOF重写
如果AOF文件中存储的命令不断增长下去,那么AOF文件会越来越大,载入时间会越来越长。Redis提供了一种重写AOF文件的方法,可以把多条命令合并成一条(或几条)生成一个新的AOF文件,并替换原文件。
RPUSH list "A" "B"
RPUSH list "C"
RPUSH list "D" "E"
LPOP list
LPOP list
RPUSH list "F" "G"
上面的5条语句经过重写后可以合并成下面一行命令:
RPUSH list "C" "D" "E" "F" "G"
Redis通过读取数据库当前键值对的方式进行AOF文件重写,而不是分析原来的AOF文件。
重写过程中会产生大量的写操作,为了不阻塞进程,Redis在子进程中进行重写工作。这样导致的一个问题是,当AOF重写正在进行时,可能会有新的命令被服务器执行,导致新的AOF文件和数据库状态不一致。Redis的解决方法是:除了AOF缓冲区外再增加一个AOF重写缓冲区,这个缓冲区只在开启子进程后使用,服务器把执行的写命令同时追加到这两个缓冲区的末尾,当子进程完成重写后通知服务器进程,服务器把重写缓冲区中的内容全部写入新的AOF文件,然后原子地替换原来的AOF文件。
自动AOF重写
当serverCron函数执行时,它会检查以下条件是否全部满足,如果全部满足的话,就触发自动的AOF重写操作:
- 没有RDB持久化或AOF持久化在执行;
- 没有AOF重写在进行;
- 当前AOF文件大小要大于
server.aof_rewrite_min_size
(默认为1MB),或者在redis.conf配置了auto-aof-rewrite-min-size
大小; - 当前AOF文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比(在配置文件设置了
auto-aof-rewrite-percentage
参数,不设置默认为100%)
客户端
在网络连接部分,Redis使用了I/O多路复用技术,用单线程接收和响应客户端的请求。每个客户端对应一个redisClient
类型的结构体,多个客户端以链表的形式存储在redisServer
对象中。可以使用CLIENT list
命令查看所有的客户端信息,其中age
属性是客户端连接到服务器的秒数。
id=2 addr=127.0.0.1:35400 fd=6 name= age=8 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
可以使用CLIENT SETNAME name
为当前连接的客户端设置一个名字。
id=2 addr=127.0.0.1:35400 fd=6 name=hello age=603 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
标志
redisClient
结构体中有一个重要的属性flags
,主要有以下几种标志常量:
-
REDIS_MASTER
表示客户端代表的是一个主服务器 -
REDIS_SLAVE
表示客户端代表是的一个从服务器 -
REDIS_LUA_CLIENT
表示客户端是专门处理Lua脚本的伪客户端 -
REDIS_BLOCKED
表示客户端正在被BRPOP
、BLPOP
等命令阻塞 -
REDIS_MULTI
表示客户端正在执行事务 -
REDIS_FORCE_AOF
强制服务器将当前命令写入AOF文件 -
REDIS_FORCE_REPL
强制主服务器将当前命令复制给所有从服务器
flags
属性可以是多个常量的组合,比如REDIS_LUA_CLIENT|REDIS_FORCE_AOF
服务器
当服务器接收到客户端发来的命令后,会在命令表中查找对应的命令,其数据结构如图所示:
每一个命令对应一个
redisCommand
类型的对象,其中:
-
name
表示命令的名称 -
proc
指向具体的实现函数 -
arity
命令的参数个数,命令本身也是参数。如果该值是负数,如-N,则表示参数的数量大于等于N -
sflags
命令的属性,比如是不是可写命令,是不是只读命令每种属性由一个字符表示,比如wm
表示这是一个可写命令(w
),并且可能需要占用大量内存,服务器需要进行内存空间的检查(m
),SET
命令的属性就是wm
服务器查找命令时是忽略大小写的。
命令执行器
在执行命令前,服务器会执行一些检查工作,比如命令参数个数是否正确、客户端是否通过了身份验证、如果服务器开启了maxmemory
功能,那么还要检查内存占用情况并按需回收等步骤。
在命令执行后,服务器也会执行一些后续工作,以下列出部分:
- 如果开启了慢日志查询功能,服务器会检查是否要为刚才执行的命令添加一条慢查询日志
- 如果开启了AOF功能,那么就写入到AOF缓冲区
- 如果由其他从服务器正在复制当前这个服务器,那么服务器会把刚才执行的命令传播给所有从服务器
serverCron函数
serverCron函数每100毫秒执行一次,负责管理服务器的资源,主要有以下几个任务:
- 更新时间缓存
为了减少系统调用的次数,在redisServer
对象中有两个字段分别缓存了秒级和毫秒级精度的UNIX时间戳,由于精度不高,所以时间缓存仅用于打印日志、更新服务器的LRU时钟、决定是否执行持久化任务、计算服务器uptime这类对时间精度不高的功能上。对于设置过期时间等需要高精度时间的功能还是会通过系统调用。 - 更新LRU时钟
redisServer
的lruclock
属性缓存了服务器的LRU时钟,serverCron函数每10秒更新该属性的值。Redis通过lruclock
减去每个redisObject
中的lru
属性的值就可以得到键的空转时间。 - 更新服务器每次执行命令次数
Redis通过抽样计算的方式把多次抽样结果存储在一个数组中,通过遍历数组计算平均数的方式,得到过去1秒的平均执行命令次数,每100毫秒进行一次抽样。 - 更新内存峰值记录
每次serverCron函数执行时都会对比之前记录的内存峰值,如果比之前的大,就设置为新的内存峰值。 - 处理SIGTERM信号
当Redis收到SIGTERM
信号时会把redisServer
对象的shutdown_asap
属性设置为1,表示关闭服务器,serverCron函数每次都会检查这个属性,如果要关闭服务器,则执行RDB持久化,然后关闭服务器。 - 管理客户端资源
serverCron函数每次执行会对一定数量的客户端进行以下检查: - 如果客户端在很长一段时间里都没有跟服务端互动,那么程序就释放连接
- 如果客户端在上一次执行命令请求后输入缓冲区的大小超过了一定的长度,那么程序就释放这块内存并重新创建一个默认大小的输入缓冲区
- 管理数据库资源
检查数据库,删除过期的键,在需要时也会对字典进行收缩操作。 - 执行被延迟的BGREWRITEAOF
在服务器执行BGSAVE
命令期间,如果客户端发来BGREWRITEAOF
命令,那么服务器会在BGSAVE
命令执行完毕后延迟执行BGREWRITEAOF
命令。当redisServer
的aof_rewrite_scheduled
值为1时,表示有延迟的BGREWRITEAOF
命令需要被执行。 -
检查持久化操作的运行状态
serverCron函数会检查当前是否有持久化任务正在执行,如果没有,那么会走下面的流程:
- 将AOF缓冲区中的内容写入AOF文件
- 关闭输出缓冲区超过大小限制的客户端
发布订阅
发布订阅功能分为两种,一种是精确匹配SUBSCRIBE
/UNSUBSCRIBE
,一种是模式匹配PSUBSCRIBE
/PUNSUBSCRIBE
,这两种订阅状态分别存储在redisServer
结构体的pubsub_channels
和pubsub_patterns
字段中。
当客户端向服务器发送PUBLISH
消息时,服务器会遍历pubsub_channels
中某个频道下的客户端发送消息,然后再遍历pubsub_patterns
,找出符合条件的客户端发送消息。
在集群模式中,当客户端向某个节点发送
PUBLISH
消息后,服务器会向整个集群广播这条消息,每一个节点都会执行PUBLISH
消息,这样,连接在其它节点上的客户端也能收到这条消息。
事务
Redis的事务主要有三个步骤:
- 事务开始
MULTI
命令标志事务的开始,服务器会打开客户端状态的事务标识,表示客户端已进入事务模式。 - 命令入队
当服务器接收到处于事务模式的客户端发来的消息后,除非是EXEC
、DISCARD
、WATCH
、MULTI
中的一个命令,否则就把命令放入队列,并且向客户端回复QUEUED
消息。 - 事务执行
服务器依次执行队列中的消息,清空客户端的事务状态,并把执行命令的全部结果返回给客户端。
Redis的事务具有原子性。事务中的命令要么全部执行,要么一个都不执行(比如监控的键被修改),但是Redis的事务即使全部执行不代表所有的命令都正确执行,比如某条命令参数错误导致执行失败,Redis仍然会执行完接下来的命令,对已经执行的命令不会有任何影响。
作者认为事务中出现错误通常是编程错误,比如少写了一个参数。
WATCH命令
服务器会把当前所有正在被监控的键以及客户端的状态存在redisDb
结构体的watched_keys
字段中。
当执行对数据库的修改后(如SET
、LPUSH
等命令),会遍历watched_keys
字段,如果被修改的键存在字典中,那么就打开该键关联的所有客户端的REDIS_DIRTY_CAS
标识,当服务器执行EXEC
时,它会先检查客户端的REDIS_DIRTY_CAS
标识是否打开,如果已经打开,那么就拒绝执行事务。
参考/图片出处:
1. 机械工业出版社 -《Redis设计与实现》