Redis
NoSQL
概述
NoSQL(Not-Only SQL):泛指非关系型的数据库,作为关系型数据库的补充
MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力
作用:应对基于海量用户和海量数据前提下的数据处理问题
特征:
- 可扩容,可伸缩,SQL 数据关系过于复杂,Nosql 不存关系,只存数据
- 大数据量下高性能,数据不存取在磁盘 IO,存取在内存
- 灵活的数据模型,设计了一些数据存储格式,能保证效率上的提高
- 高可用,集群
常见的 NoSQL:Redis、memcache、HBase、MongoDB
参考书籍:https://book.douban.com/subject/25900156/
参考视频:https://www.bilibili.com/video/BV1CJ411m7Gc
Redis
Redis (REmote DIctionary Server) : 用 C 语言开发的一个开源的高性能键值对(key-value)数据库
特征:
- 数据间没有必然的关联关系,不存关系,只存数据
- 数据存储在内存,存取速度快,解决了磁盘 IO 速度慢的问题
- 内部采用单线程机制进行工作
- 高性能,官方测试数据,50 个并发执行 100000 个请求,读的速度是 110000 次/s,写的速度是 81000 次/s
- 多数据类型支持
- 字符串类型:string(String)
- 列表类型:list(LinkedList)
- 散列类型:hash(HashMap)
- 集合类型:set(HashSet)
- 有序集合类型:zset/sorted_set(TreeSet)
- 支持持久化,可以进行数据灾难恢复
安装启动
安装:
-
Redis 5.0 被包含在默认的 Ubuntu 20.04 软件源中
sudo apt updatesudo apt install redis-server -
检查 Redis 状态
sudo systemctl status redis-server
启动:
-
启动服务器——参数启动
redis-server [--port port]#redis-server --port 6379 -
启动服务器——配置文件启动
redis-server config_file_name#redis-server /etc/redis/conf/redis-6397.conf -
启动客户端:
redis-cli [-h host] [-p port]#redis-cli -h 192.168.2.185 -p 6397注意:服务器启动指定端口使用的是--port,客户端启动指定端口使用的是-p
基本配置
系统目录
-
创建文件结构
创建配置文件存储目录
mkdir conf创建服务器文件存储目录(包含日志、数据、临时配置文件等)
mkdir data -
创建配置文件副本放入 conf 目录,Ubuntu 系统配置文件 redis.conf 在目录
/etc/redis中cat redis.conf | grep -v "#" | grep -v "^$" -> /conf/redis-6379.conf去除配置文件的注释和空格,输出到新的文件,命令方式采用 redis-port.conf
服务器
-
设置服务器以守护进程的方式运行,关闭后服务器控制台中将打印服务器运行信息(同日志内容相同):
daemonize yes|no -
绑定主机地址,绑定本地IP地址,否则SSH无法访问:
bind ip -
设置服务器端口:
port port -
设置服务器文件保存地址:
dir path -
设置数据库的数量:
databases 16 -
多服务器快捷配置:
导入并加载指定配置文件信息,用于快速创建 redis 公共配置较多的 redis 实例配置文件,便于维护
include /path/conf_name.conf
客户端
-
服务器允许客户端连接最大数量,默认 0,表示无限制,当客户端连接到达上限后,Redis 会拒绝新的连接:
maxclients count -
客户端闲置等待最大时长,达到最大值后关闭对应连接,如需关闭该功能,设置为 0:
timeout seconds
日志配置
设置日志记录
-
设置服务器以指定日志记录级别
loglevel debug|verbose|notice|warning -
日志记录文件名
logfile filename
注意:日志级别开发期设置为 verbose 即可,生产环境中配置为 notice,简化日志输出量,降低写日志 IO 的频度
配置文件:
bind 192.168.2.185
port 6379
#timeout 0
daemonize no
logfile /etc/redis/data/redis-6379.log
dir /etc/redis/data
dbfilename "dump-6379.rdb"
基本指令
帮助信息:
-
获取命令帮助文档
help [command]#help set -
获取组中所有命令信息名称
help [@group-name]#help @string
退出服务
-
退出客户端:
quitexit -
退出客户端服务器快捷键:
Ctrl+C
数据库
服务器
Redis 服务器将所有数据库保存在服务器状态 redisServer 结构的 db 数组中,数组的每一项都是 redisDb 结构,代表一个数据库,每个数据库之间相互独立,**共用 **Redis 内存,不区分大小。在初始化服务器时,根据 dbnum 属性决定创建数据库的数量,该属性由服务器配置的 database 选项决定,默认 16
struct redisServer {
// 保存服务器所有的数据库
redisDB *db;
// 服务器数据库的数量
int dbnum;
};

在服务器内部,客户端状态 redisClient 结构的 db 属性记录了目标数据库,是一个指向 redisDb 结构的指针
struct redisClient {
// 记录客户端正在使用的数据库,指向 redisServer.db 数组中的某一个 db
redisDB *db;
};
每个 Redis 客户端都有目标数据库,执行数据库读写命令时目标数据库就会成为这些命令的操作对象,默认情况下 Redis 客户端的目标数据库为 0 号数据库,客户端可以执行 SELECT 命令切换目标数据库,原理是通过修改 redisClient.db 指针指向服务器中不同数据库
命令操作:
select index #切换数据库,index从0-15取值
move key db #数据移动到指定数据库,db是数据库编号
ping #测试数据库是否连接正常,返回PONG
echo message #控制台输出信息
Redis 没有可以返回客户端目标数据库的命令,但是 redis-cli 客户端旁边会提示当前所使用的目标数据库
redis> SELECT 1
OK
redis[1]>
键空间
key space
Redis 是一个键值对(key-value pair)数据库服务器,每个数据库都由一个 redisDb 结构表示,redisDb.dict 字典中保存了数据库的所有键值对,将这个字典称为键空间(key space)
typedef struct redisDB {
// 数据库键空间,保存所有键值对
dict *dict
} redisDB;
键空间和用户所见的数据库是直接对应的:
- 键空间的键就是数据库的键,每个键都是一个字符串对象
- 键空间的值就是数据库的值,每个值可以是任意一种 Redis 对象

当使用 Redis 命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会进行一些维护操作:
- 在读取一个键后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中 hit 次数或键空间不命中 miss 次数,这两个值可以在
INFO stats命令的 keyspace_hits 属性和 keyspace_misses 属性中查看 - 更新键的 LRU(最后使用)时间,该值可以用于计算键的闲置时间,使用
OBJECT idletime key查看键 key 的闲置时间 - 如果在读取一个键时发现该键已经过期,服务器会先删除过期键,再执行其他操作
- 如果客户端使用 WATCH 命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务注意到这个键已经被修改过
- 服务器每次修改一个键之后,都会对 dirty 键计数器的值增1,该计数器会触发服务器的持久化以及复制操作
- 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知
读写指令
常见键操作指令:
-
增加指令
set key value #添加一个字符串类型的键值对 -
删除指令
del key #删除指定keyunlink key #非阻塞删除key,真正的删除会在后续异步操作 -
更新指令
rename key newkey #改名renamenx key newkey #改名值得更新需要参看具体得 Redis 对象得操作方式,比如字符串对象执行
SET key value就可以完成修改 -
查询指令
exists key #获取key是否存在randomkey #随机返回一个键keys pattern #查询keyKEYS 命令需要遍历存储的键值对,操作延时高,一般不被建议用于生产环境中
查询模式规则:* 匹配任意数量的任意符号、? 配合一个任意符号、[] 匹配一个指定符号
keys * #查询所有keykeys aa* #查询所有以aa开头keys *bb #查询所有以bb结尾keys ??cc #查询所有前面两个字符任意,后面以cc结尾keys user:? #查询所有以user:开头,最后一个字符任意keys u[st]er:1 #查询所有以u开头,以er:1结尾,中间包含一个字母,s或t -
其他指令
type key #获取key的类型dbsize #获取当前数据库的数据总量,即key的个数flushdb #清除当前数据库的所有数据(慎用)flushall #清除所有数据(慎用)在执行 FLUSHDB 这样的危险命令之前,最好先执行一个 SELECT 命令,保证当前所操作的数据库是目标数据库
时效设置
客户端可以以秒或毫秒的精度为数据库中的某个键设置生存时间(TimeTo Live, TTL),在经过指定时间之后,服务器就会自动删除生存时间为 0 的键;也可以以 UNIX 时间戳的方式设置过期时间(expire time),当键的过期时间到达,服务器会自动删除这个键
expire key seconds #为指定key设置生存时间,单位为秒
pexpire key milliseconds #为指定key设置生存时间,单位为毫秒
expireat key timestamp #为指定key设置过期时间,单位为时间戳
pexpireat key mil-timestamp #为指定key设置过期时间,单位为毫秒时间戳
- 实际上 EXPIRE、EXPIRE、EXPIREAT 三个命令底层都是转换为 PEXPIREAT 命令来实现的
- SETEX 命令可以在设置一个字符串键的同时为键设置过期时间,但是该命令是一个类型限定命令
redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,字典称为过期字典:
- 键是一个指针,指向键空间中的某个键对象(复用键空间的对象,不会产生内存浪费)
- 值是一个 long long 类型的整数,保存了键的过期时间,是一个毫秒精度的 UNIX 时间戳
typedef struct redisDB {
// 过期字典,保存所有键的过期时间
dict *expires
} redisDB;
客户端执行 PEXPIREAT 命令,服务器会在数据库的过期字典中关联给定的数据库键和过期时间:
def PEXPIREAT(key, expire_time_in_ms):
# 如果给定的键不存在于键空间,那么不能设置过期时间
if key not in redisDb.dict:
return 0
# 在过期字典中关联键和过期时间
redisDB.expires[key] = expire_time_in_ms
# 过期时间设置成功
return 1
时效状态
TTL 和 PTTL 命令通过计算键的过期时间和当前时间之间的差,返回这个键的剩余生存时间
- 返回正数代表该数据在内 存中还能存活的时间
- 返回 -1 代表永久性,返回 -2 代表键不存在
ttl key #获取key的剩余时间,每次获取会自动变化(减小),类似于倒计时
pttl key #获取key的剩余时间,单位是毫秒,每次获取会自动变化(减小)
PERSIST 是 PEXPIREAT 命令的反操作,在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联
persist key #切换key从时效性转换为永久性
Redis 通过过期字典可以检查一个给定键是否过期:
- 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间
- 检查当前 UNIX 时间戳是否大于键的过期时间:如果是那么键已经过期,否则键未过 期
补充:AOF、RDB 和复制功能对过期键的处理
- RDB :
- 生成 RDB 文件,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的 RDB 文件中
- 载入 RDB 文件,如果服务器以主服务器模式运行,那么在载入时会对键进行检查,过期键会被忽略;如果服务器以从服务器模式运行,会载入所有键,包括过期键,但是主从服务器进行数据同步时就会删除这些键
- AOF:
- 写入 AOF 文件,如果数据库中的某个键已经过期,但还没有被删除,那么 AOF 文件不会因为这个过期键而产生任何影响;当该过期键被删除,程序会向 AOF 文件追加一条 DEL 命令,显式的删除该键
- AOF 重写,会对数据库中的键进行检查,忽略已经过期的键
- 复制:当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制
- 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键
- 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,会当作未过期键处理,只有在接到主服务器发来的 DEL 命令之后,才会删除过期键(数据不一致)
过期删除
删除策略
删除策略就是针对已过期数据的处理策略,已过期的数据不一定被立即删除,在不同的场景下使用不同的删除方式会有不同效果,在内存占用与 CPU 占用之间寻找一种平衡,顾此失彼都会造成整体 Redis 性能的下降,甚至引发服务器宕机或内存泄露
针对过期数据有三种删除策略:
- 定时删除
- 惰性删除(被动删除)
- 定期删除
Redis 采用惰性删除和定期删除策略的结合使用
定时删除
在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间到达时,立即执行对键的删除操作
- 优点:节约内存,到时就删除,快速释放掉不必要的内存占用
- 缺点:对 CPU 不友好,无论 CPU 此时负载多高均占用 CPU,会影响 Redis 服务器响应时间和指令吞吐量
- 总结:用处理器性能换取存储空间(拿时间换空间)
创建一个定时器需要用到 Redis 服务器中的时间事件,而时间事件的实现方式是无序链表,查找一个事件的时间复杂度为 O(N),并不能高效地处理大量时间事件,所以采用这种方式并不现实
惰性删除
数据到达过期时间不做处理,等下次访问到该数据时执行 expireIfNeeded() 判断:
- 如果输入键已经过期,那么 expireIfNeeded 函数将输入键从数据库中删除,接着访问就会返回空
- 如果输入键未过期,那么 expireIfNeeded 函数不做动作
所有的 Redis 读写命令在执行前都会调用 expireIfNeeded 函数进行检查,该函数就像一个过滤器,在命令真正执行之前过滤掉过期键
惰性删除的特点:
- 优点:节约 CPU 性能,删除的目标仅限于当前处理的键,不会在删除其他无关的过期键上花费任何 CPU 时间
- 缺点:内存压力很大,出现长期占用内存的数据,如果过期键永远不被访问,这种情况相当于内存泄漏
- 总结:用存储空间换取处理器性能(拿空间换时间)
定期删除
定期删除策略是每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响
- 如果删除操作执行得太频繁,或者执行时间太长,就会退化成定时删除策略,将 CPU 时间过多地消耗在删除过期键上
- 如果删除操作执行得太少,或者执行时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况
定期删除是周期性轮询 Redis 库中的时效性数据,从过期字典中随机抽取一部分键检查,利用过期数据占比的方式控制删除频度
-
Redis 启动服务器初始化时,读取配置 server.hz 的值,默认为 10,执行指令 info server 可以查看,每秒钟执行 server.hz 次
serverCron() → activeExpireCycle() -
activeExpireCycle() 对某个数据库中的每个 expires 进行检测,工作模式:
-
轮询每个数据库,从数据库中取出一定数量的随机键进行检查,并删除其中的过期键,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒
-
全局变量 current_db 用于记录 activeExpireCycle() 的检查进度(哪一个数据库),下一次调用时接着该进度处理
-
随着函数的不断执行,服务器中的所有数据库都会被检查一遍,这时将 current_db 重置为 0,然后再次开始新一轮的检查
-
定期删除特点:
- CPU 性能占用设置有峰值,检测频度可自定义设置
- 内存压力不是很大,长期占用内存的冷数据会被持续清理
- 周期性抽查存储空间(随机抽查,重点抽查)
数据淘汰
逐出算法
数据淘汰策略:当新数据进入 Redis 时,在执行每一个命令前,会调用 freeMemoryIfNeeded() 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,Redis 要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为逐出算法
逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行,当对所有数据尝试完毕,如不能达到内存清理的要求,出现 Redis 内存打满异常:
(error) OOM command not allowed when used memory >'maxmemory'
策略配置
Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 64 位操作系统下不限制内存大小,在 32 位操作系统默认为 3GB 内存,一般推荐设置 Redis 内存为最大物理内存的四分之三
内存配置方式:
-
通过修改文件配置(永久生效):修改配置文件 maxmemory 字段,单位为字节
-
通过命令修改(重启失效):
-
config set maxmemory 104857600:设置 Redis 最大占用内存为 100MB -
config get maxmemory:获取 Redis 最大占用内存 -
info:可以查看 Redis 内存使用情况,used_memory_human字段表示实际已经占用的内存,maxmemory表示最大占用内存
-
影响数据淘汰的相关配置如下,配置 conf 文件:
-
每次选取待删除数据的个数,采用随机获取数据的方式作为待检测删除数据,防止全库扫描,导致严重的性能消耗,降低读写性能
maxmemory-samples count -
达到最大内存后的,对被挑选出来的数据进行删除的策略
maxmemory-policy policy数据删除的策略 policy:3 类 8 种
第一类:检测易失数据(可能会过期的数据集 server.db[i].expires):
volatile-lru # 对设置了过期时间的 key 选择最近最久未使用使用的数据淘汰volatile-lfu # 对设置了过期时间的 key 选择最近使用次数最少的数据淘汰volatile-ttl # 对设置了过期时间的 key 选择将要过期的数据淘汰volatile-random # 对设置了过期时间的 key 选择任意数据淘汰第二类:检测全库数据(所有数据集 server.db[i].dict ):
allkeys-lru # 对所有 key 选择最近最少使用的数据淘汰allkeLyRs-lfu # 对所有 key 选择最近使用次数最少的数据淘汰allkeys-random # 对所有 key 选择任意数据淘汰,相当于随机第三类:放弃数据驱 逐
no-enviction #禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory)
数据淘汰策略配置依据:使用 INFO 命令输出监控信息,查询缓存 hit 和 miss 的次数,根据需求调优 Redis 配置
排序机制
基本介绍
Redis 的 SORT 命令可以对列表键、集合键或者有序集合键的值进行排序,并不更改集合中的数据位置,只是查询
SORT key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询
SORT key ALPHA #对key中字母排序,按照字典序
SORT
SORT <key> 命令可以对一个包含数字值的键 key 进行排序
假设 RPUSH numbers 3 1 2,执行 SORT numbers 的详细步骤:
-
创建一个和 key 列表长度相同的数组,数组每项都是 redisSortObject 结构
typedef struct redisSortObject {// 被排序键的值robj *obj;// 权重union {// 排序数字值时使用double score;// 排序带有 BY 选项的字符串robj *cmpobj;} u;} -
遍历数组,将各个数组项的 obj 指针分别指向 numbers 列表的各个项
-
遍历数组,将 obj 指针所指向的列表项转换成一个 double 类型的浮点数,并将浮点数保存在对应数组项的 u.score 属性里
-
根据数组项 u.score 属性的值,对数组进行数字值排序,排序后的数组项按 u.score 属性的值从小到大排列
-
遍历数组,将各个数组项的 obj 指针所指向的值作为排序结果返回给客户端,程序首先访问数组的索引 0,依次向后访问

对于 SORT key [ASC/DESC] 函数:
- 在执行升序排序时,排序算法使用的对比函数产生升序对比结果
- 在执行降序排序时,排序算法使用的对比函数产生降序对比结果
BY
SORT 命令默认使用被排序键中包含的元素作为排序的权重,元素本身决定了元素在排序之后所处的位置,通过使用 BY 选项,SORT 命令可以指定某些字符串键,或者某个哈希键所包含的某些域(field)来作为元素的权重,对一个键进行排序
SORT <key> BY <pattern> # 数值
SORT <key> BY <pattern> ALPHA # 字符
redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> SORT fruits ALPHA
1) "apple"
2) "banana"
3) "cherry"
redis> MSET apple-price 8 banana-price 5.5 cherry-price 7
OK
# 使用水果的价钱进行排序
redis> SORT fruits BY *-price
1) "banana"
2) "cherry"
3) "apple"
实现原理:排序时的 u.score 属性就会被设置为对应的权重
LIMIT
SORT 命令默认会将排序后的所有元素都返回给客户端,通过 LIMIT 选项可以让 SORT 命令只返回其中一部分已排序的元素
LIMIT <offset> <count>
- offset 参数表示要跳过的已排序元素数量
- count 参数表示跳过给定数量的元素后,要返回的已排序元素数量
# 对应 a b c d e f g
redis> SORT alphabet ALPHA LIMIT 2 3
1) "c"
2) "d"
3) "e"
实现原理:在排序后的 redisSortObject 结构数组中,将指针移动到数组的索引 2 上,依次访问 array[2]、array[3]、array[4] 这 3 个数组项,并将数组项的 obj 指针所指向的元素返回给客户端
GET
SORT 命令默认在对键进行排序后,返回被排序键本身所包含的元素,通过使用 GET 选项, 可以在对键进行排序后,根据被 排序的元素以及 GET 选项所指定的模式,查找并返回某些键的值
SORT <key> GET <pattern>
redis> SADD students "tom" "jack" "sea"
#设置全名
redis> SET tom-name "Tom Li"
OK
redis> SET jack-name "Jack Wang"
OK
redis> SET sea-name "Sea Zhang"
OK
redis> SORT students ALPHA GET *-name
1) "Jack Wang"
2) "Sea Zhang"
3) "Tom Li"
实现原理:对 students 进行排序后,对于 jack 元素和 *-name 模式,查找程序返回键 jack-name,然后获取 jack-name 键对应的值
STORE
SORT 命令默认只向客户端返回排序结果,而不保存排序结果,通过使用 STORE 选项可以将排序结果保存在指定的键里面
SORT <key> STORE <sort_key>
redis> SADD students "tom" "jack" "sea"
(integer) 3
redis> SORT students ALPHA STORE sorted_students
(integer) 3
实现原理:排序后,检查 sorted_students 键是否存在,如果存在就删除该键,设置 sorted_students 为空白的列表键,遍历排序数组将元素依次放入
执行顺序
调用 SORT 命令,除了 GET 选项之外,改变其他选项的摆放顺序并不会影响命令执行选项的顺序
SORT <key> ALPHA [ASC/DESC] BY <by-pattern> LIMIT <offset> <count> GET <get-pattern> STORE <store_key>
执行顺序:
- 排序:命令会使用 ALPHA 、ASC 或 DESC、BY 这几个选项,对输入键进行排序,并得到一个排序结果集
- 限制排序结果集的长度:使用 LIMIT 选项,对排序结果集的长度进行限制
- 获取外部键:根据排序结果集中的元素以及 GET 选项指定的模式,查找并获取指定键的值,并用这些值来作为新的排序结果集
- 保存排序结果集:使用 STORE 选项,将排序结果集保存到指定的键上面去
- 向客户端返回排序结果集:最后一步命令遍历排序结果集,并依次向客户端返回排序结果集中的元素
通知机制
数据库通知是可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况
- 关注某个键执行了什么命令的通知称为键空间通知(key-space notification)
- 关注某个命令被什么键执行的通知称为键事件通知(key-event notification)
图示订阅 0 号数据库 message 键:

服务器配置的 notify-keyspace-events 选项决定了服务器所发送通知的类型
- AKE 代表服务器发送所有类型的键空间通知和键事件通知
- AK 代表服务器发送所有类型的键空间通知
- AE 代表服务 器发送所有类型的键事件通知
- K$ 代表服务器只发送和字符串键有关的键空间通知
- EL 代表服务器只发送和列表键有关的键事件通知
- .....
发送数据库通知的功能是由 notifyKeyspaceEvent 函数实现的:
- 如果给定的通知类型 type 不是服务器允许发送的通知类型,那么函数会直接返回
- 如果给定的通知是服务器允许发送的通知
- 检测服务器是否允许发送键空间通知,允许就会构建并发送事件通知
- 检测服务器是否允许发送键事件通知,允许就会构建并发送事件通知
体系架构
事件驱动
基本介绍
Redis 服务器是一个事件驱动程序,服务器需要处理两类事件
- 文件事件 (file event):服务器通过套接字与客户端(或其他 Redis 服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端的通信会产生相应的文件事件,服务器通过监听并处理这些事件完成一系列网络通信操作
- 时间事件 (time event):Redis 服务器中的一些操作(比如 serverCron 函数)需要在指定时间执行,而时间事件就 是服务器对这类定时操作的抽象
文件事件
基本组成
Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器 (file event handler)
-
使用 I/O 多路复用 (multiplexing) 程序来同时监听多个套接字,并根据套接字执行的任务来为套接字关联不同的事件处理器
-
当被监听的套接字准备好执行连接应答 (accept)、 读取 (read)、 写入 (write)、 关闭 (close) 等操作时,与操作相对应的文件事件就会产生,这时文件事件分派器会调用套接字关联好的事件处理器来处理事件
文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字, 既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,保持了 Redis 内部单线程设计的简单性
文件事件处理器的组成结构:

I/O 多路复用程序将所有产生事件的套接字处理请求放入一个单线程的执行队列中,通过队列有序、同步的向文件事件分派器传送套接字,上一个套接字产生的事件处理完后,才会继续向分派器传送下一个

Redis 单线程也能高效的原因:
- 纯内存操作
- 核心是基于非阻塞的 IO 多路复用机制,单线程可以高效处理多个请求
- 底层使用 C 语言实现,C 语言实现的程序距离操作系统更近,执行速度相对会更快
- 单线程同时也避免了多线程的上下文频繁切换问题,预防了多线程可能产生的竞争问题
多路复用
Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select 、epoll、 evport 和 kqueue 这些函数库来实现的,Redis 在 I/O 多路复用程序的实现源码中用 #include 宏定义了相应的规则,编译时自动选择系统中性能最高的多路复用函数来作为底层实现
I/O 多路复用程序监听多个套接字的 AE_READABLE 事件和 AE_WRITABLE 事件,这两类事件和套接字操作之间的对应关系如下:
- 当套接字变得可读时(客户端对套接字执行 write 操作或者 close 操作),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行 connect 连接操作),套接字产生 AE_READABLE 事件
- 当套接字变得可写时(客户端对套接字执行 read 操作,对于服务器来说就是可以写了),套接字产生 AE_WRITABLE 事件
I/O 多路复用程序允许服务器同时监听套接字的 AE_READABLE 和 AE_WRITABLE 事件, 如果一个套接字同时产生了这两种事件,那么文件事件 分派器会优先处理 AE_READABLE 事件, 等 AE_READABLE 事件处理完之后才处理 AE_WRITABLE 事件
处理器
Redis 为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求:
- 连接应答处理器,用于对连接服务器的各个客户端进行应答,Redis 服务器初始化时将该处理器与 AE_READABLE 事件关联
- 命令请求处理器,用于接收客户端传来的命令请求,执行套接字的读入操作,与 AE_READABLE 事件关联
- 命令回复处理器,用于向客户端返回命令的执行结果,执行套接字的写入操作,与 AE_WRITABLE 事件关联
- 复制处理器,当主服务器和从服务器进行复制操作时,主从服务器都需要关联该处理器
Redis 客户端与服务器进行连接并发送命令的整个过程:
- Redis 服务器正在运作监听套接字的 AE_READABLE 事件,关联连接应答处理器
- 当 Redis 客户端向服务器发起连接,监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行,对客户端的连接请求进行应答,创建客户端套接字以及客户端状态,并将客户端套接字的 AE_READABLE 事件与命令请求处理器进行关联
- 客户端向服务器发送命令请求,客户端套接字产生 AE_READABLE 事件,引发命令请求处理器执行,读取客户端的命令内容传给相关程序去执行
- 执行命令会产生相应的命令回复,为了将这些命令回复传送回客户端,服务器会将客户端套接字的 AE_WRITABLE 事件与命令回复处理器进行关联
- 当客户端尝试读取命令回复时,客户端套接字产生 AE_WRITABLE 事件,触发命令回复处理器执行,在命令回复全部写入套接字后,服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联
时间事件
Redis 的时间事件分为以下两类:
- 定时事件:在指定的时间之后执行一次(Redis 中暂时未使用)
- 周期事件:每隔指定时间就执行一次
一个时间事件主要由以下三个属性组成:
- id:服务器为时间事件创建的全局唯一 ID(标识号),从小到大顺序递增,新事件的 ID 比旧事件的 ID 号要大
- when:毫秒精度的 UNIX 时间戳,记录了时间事件的到达(arrive)时间
- timeProc:时间事件处理器,当时间事件到达时,服务器就会调用相应的处理器来处理事件
时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值:
- 定时事件:事件处理器返回 AE_NOMORE,该事件在到达一次后就会被删除
- 周期事件:事件处理器返回非 AE_NOMORE 的整数值,服务器根据该值对事件的 when 属性更新,让该事件在一段时间后再次交付
服务器将所有时间事件都放在一个无序链表中,新的时间事件插入到链表的表头:

无序链表指是链表不按 when 属性的大小排序,每当时间事件执行器运行时就必须 遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器处理
无序链表并不影响时间事件处理器的性能,因为正常模式下的 Redis 服务器只使用 serverCron 一个时间事件,在 benchmark 模式下服务器也只使用两个时间事件,所以无序链表不会影响服务器的性能,几乎可以按照一个指针处理
事件调度
服务器中同时存在文件事件和时间事件两种事件类型,调度伪代码:
# 事件调度伪代码
def aeProcessEvents():
# 获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTime()
# 计算最接近的时间事件距离到达还有多少亳秒
remaind_ms = time_event.when - unix_ts_now()
# 如果事件已到达,那么 remaind_ms 的值可能为负数,设置为 0
if remaind_ms < 0:
remaind_ms = 0
# 根据 remaind_ms 的值,创建 timeval 结构
timeval = create_timeval_with_ms(remaind_ms)
# 【阻塞并等待文件事件】产生,最大阻塞时间由传入的timeval结构决定,remaind_ms的值为0时调用后马上返回,不阻塞
aeApiPoll(timeval)
# 处理所有已产生的文件事件
processFileEvents()
# 处理所有已到达的时间事件
processTimeEvents()
事件的调度和执行规则:
- aeApiPoll 函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,可以避免服务器对时间事件进行频繁的轮询(忙等待),也可以确保 aeApiPoll 函数不会阻塞过长时间
- 对文件事件和时间事件的处理都是同步、有序、原子地执行,服务器不会中途中断事件处理,也不会对事件进行抢占,所以两种处理器都要尽可地减少程序的阻塞时间,并在有需要时主动让出执行权,从而降低事件饥饿的可能性
- 命令回复处理器在写入字节数超过了某个预设常量,就会主动用 break 跳出写入循环,将余下的数据留到下次再写
- 时间事件也会将非常耗时的持久化操作放到子线程或者子进程执行
- 时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间通常会比设定的到达时间稍晚
多线程
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这是 Redis 的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络),多线程只是用来处理网络数据的读写和协议解析, 执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。
Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 redis.conf :
io-threads-do-reads yesCopy to clipboardErrorCopied
开启多线程后,还需要设置线程数,否则是不生效的,同样需要修改 redis 配置文件 :
io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程

参考文章:https://mp.weixin.qq.com/s/dqmiR0ECf4lB6Y2OyK-dyA
客户端
基本介绍
Redis 服务器是典型的一对多程序,一个服务器可以与多个客户端建立网络连接,服务器对每个连接的客户端建立了相应的 redisClient 结构(客户端状态,在服务器端的存储结构),保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构
Redis 服务器状态结构的 clients 属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构:
struct redisServer {
// 一个链表,保存了所有客户端状态
list *clients;
//...
};

数据结构
redisClient
客户端的数据结构:
typedef struct redisClient {
//...
// 套接字
int fd;
// 名字
robj *name;
// 标志
int flags;
// 输入缓冲区
sds querybuf;
// 输出缓冲区 buf 数组
char buf[REDIS_REPLY_CHUNK_BYTES];
// 记录了 buf 数组目前已使用的字节数量
int bufpos;
// 可变大小的输出缓冲区,链表 + 字符串对象
list *reply;
// 命令数组
rboj **argv;
// 命令数组的长度
int argc;
// 命令的信息
struct redisCommand *cmd;
// 是否通过身份验证
int authenticated;
// 创建客户端的时间
time_t ctime;
// 客户端与服务器最后一次进行交互的时间
time_t lastinteraction;
// 输出缓冲区第一次到达软性限制 (soft limit) 的时间
time_t obuf_soft_limit_reached_time;
}
客户端状态包括两类属性
- 一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,都要用到这些属性
- 另一类是和特定功能相关的属性,比如操作数据库时用到的 db 属性和 dict id 属性,执行事务时用到的 mstate 属性,以及执行 WATCH 命令时用到的 watched_keys 属性等,代码中没有列出
套接字
客户端状态的 fd 属性记录了客户端正在使用的套接字描述符,根据客户端类型的不同,fd 属性的值可以是 -1 或者大于 -1 的整数:
- 伪客户端 (fake client) 的 fd 属性的值为 -1,命令请求来源于 AOF 文件或者 Lua 脚本,而不是网络,所以不需要套接字连接
- 普通客户端的 fd 属性的值为大于 -1 的整数,因为合法的套接字描述符不能是 -1
执行 CLIENT list 命令可以列出目前所有连接到服务器的普通客户端,不包括伪客户端
名字
在默认情况下,一个连接到服务器的客户端是没有名字的,使用 CLIENT setname 命令可以为客户端设置一个名字
标志
客户端的标志属性 flags 记录了客户端的角色以及客户端目前所处的状态,每个标志使用一个常量表示
- flags 的值可以是单个标志:
flags = <flag> - flags 的值可以是多个标志的二进制:
flags = <flagl> | <flag2> | ...
一部分标志记录客户端的角色:
- REDIS_MASTER 表示客户端是一个从服务器,REDIS_SLAVE 表示客户端是一个从服务器,在主从复制时使用
- REDIS_PRE_PSYNC 表示客户端是一个版本低于 Redis2.8 的从服务器,主服务器不能使用 PSYNC 命令与该从服务器进行同步,这个标志只能在 REDIS_ SLAVE 标志处于打开状态时使用
- REDIS_LUA_CLIENT 表示客户端是专门用于处理 Lua 脚本里面包含的 Redis 命令的伪客户端
一部分标志记录目前客户端所处的状态:
- REDIS_UNIX_SOCKET 表示服务器使用 UNIX 套接字来连接客户端
- REDIS_BLOCKED 表示客户端正在被 BRPOP、BLPOP 等命令阻塞
- REDIS_UNBLOCKED 表示客户端已经从 REDIS_BLOCKED 所表示的阻塞状态脱离,在 REDIS_BLOCKED 标志打开的情况下使用
- REDIS_MULTI 标志表示客户端正在执行事务
- REDIS_DIRTY_CAS 表示事务使用 WATCH 命令监视的数据库键已经被修改
- .....
缓冲区
客户端状态的输入缓冲区用于保存客户端发送的命令请求,输入缓冲区的大小会根据输入内容动态地缩小或者扩大,但最大大小不能超过 1GB,否则服务器将关闭这个客户端,比如执行 SET key value ,那么缓冲区 querybuf 的内容:
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n #
输出缓冲区是服务器用于保存执行客户端命令所得的命令回复,每个客户端都有两个输出缓冲区可用:
- 一个是固定大小的缓冲区,保存长度比较小的回复,比如 OK、简短的字符串值、整数值、错误回复等
- 一个是可变大小的缓冲区,保存那些长度比较大的回复, 比如一个非常长的字符串值或者一个包含了很多元素的集合等
buf 是一个大小为 REDIS_REPLY_CHUNK_BYTES (常量默认 16*1024 = 16KB) 字节的字节数组,bufpos 属性记录了 buf 数组目前已使用的字节数量,当 buf 数组的空间已经用完或者回复数据太大无法放进 buf 数组里,服务器就会开始使用可变大小的缓冲区
通过使用 reply 链表连接多个字符串对象,可以为客户端保存一个非常长的命令回复,而不必受到固定大小缓冲区 16KB 大小的限制

命令
服务器对 querybuf 中的命令请求的内容进行分析,得出的命令参数以及参数的数量分别保存到客户端状态的 argv 和 argc 属性
- argv 属性是一个数组,数组中的每项都是字符串对象,其中 argv[0] 是要执行的命令,而之后的其他项则是命令的参数
- argc 属性负责记录 argv 数组的长度

服务器将根据项 argv[0] 的值,在命令表中查找命令所对应的命令的 redisCommand,将客户端状态的 cmd 指向该结构
命令表是一个字典结构,键是 SDS 结构保存命令的名字;值是命令所对应的 redisCommand 结构,保存了命令的实现函数、命令标志、 命令应该给定的参数个数、命令的总执行次数和总消耗时长等统计信息

验证
客户端状态的 authenticated 属性用于记录客户端是否通过了身份验证
- authenticated 值为 0,表示客户端未通过身份验证
- authenticated 值为 1,表示客户端已通过身份验证
当客户端 authenticated = 0 时,除了 AUTH 命令之外, 客户端发送的所有其他命令都会被服务器拒绝执行
redis> PING
(error) NOAUTH Authentication required.
redis> AUTH 123321
OK
redis> PING
PONG
时间
ctime 属性记录了创建客户端的时间,这个时间可以用来计算客户端与服务器已经连接了多少秒,CLIENT list 命令的 age 域记录了这个秒数
lastinteraction 属性记录了客户端与服务器最后一次进行互动 (interaction) 的时间,互动可以是客户端向服务器发送命令请求,也可以是服务器向客户端发送命令回复。该属性可以用来计算客户端的空转 (idle) 时长, 就是距离客户端与服务器最后一次进行互动已经过去了多少秒,CLIENT list 命令的 idle 域记录了这个秒数
obuf_soft_limit_reached_time 属性记录了输出缓冲区第一次到达软性限制 (soft limit) 的时间
生命周期
创建
服务器使用不同的方式来创建和关闭不同类型的客户端
如果客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用 connect 函数连接到服务器时,服务器就会调用连接应答处理器为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾

服务器会在初始化时创建负责执行 Lua 脚本中包含的 Redis 命令的伪客户端,并将伪客户端关联在服务器状态的 lua_client 属性
struct redisServer {
// 保存伪客户端
redisClient *lua_client;
//...
};
lua_client 伪客户端在服务器运行的整个生命周期会一直存在,只有服务器被关闭时,这个客户端才会被关闭
载入 AOF 文件时, 服务器会创建用于执行 AOF 文件包含的 Redis 命令的伪客户端,并在载入完成之后,关闭这个伪客户端
关闭
一个普通客户端可以因为多种原因而被关闭:
- 客户端进程退出或者被杀死,那么客户端与服务器之间的网络连接将被关闭,从而造成客户端被关闭
- 客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端会被服务器关闭
- 客户端是
CLIENT KILL命令的目标 - 如果用户为服务器设置了 timeout 配置选项,那么当客户端的空转时间超过该值时将被关闭,特殊情况不会被关闭:
- 客户端是主服务器(REDIS_MASTER )或者从服务器(打开了 REDIS_SLAVE 标志)
- 正在被 BLPOP 等命令阻塞(REDIS_BLOCKED)
- 正在执 行 SUBSCRIBE、PSUBSCRIBE 等订阅命令
- 客户端发送的命令请求的大小超过了输入缓冲区的限制大小(默认为 1GB)
- 发送给客户端的命令回复的大小超过了输出缓冲区的限制大小
理论上来说,可变缓冲区可以保存任意长的命令回复,但是为了回复过大占用过多的服务器资源,服务器会时刻检查客户端的输出缓冲区的大小,并在缓冲区的大小超出范围时,执行相应的限制操作:
- 硬性限制 (hard limit):输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器会关闭客户端(serverCron 函数中执行),积存在输出缓冲区中的所有内容会被直接释放,不会返回给客户端
- 软性限制 (soft limit):输出缓冲区的大小超过了软性限制所设置的大小,小于硬性限制的大小,服务器的操作:
- 用属性 obuf_soft_limit_reached_time 记录下客户端到达软性限制的起始时间,继续监视客户端
- 如果输出缓冲区的大小一直超出软性限制,并且持续时间超过服务器设定的时长,那么服务器将关闭客户端
- 如果在指定时间内不再超出软性限制,那么客户端就不会被关闭,并且 o_s_l_r_t 属性清零
使用 client-output-buffer-limit 选项可以为普通客户端、从服务器客户端、执行发布与订阅功能的客户端分别设置不同的软性限制和硬性限制,格式:
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
- 第一行:将普通客户端的硬性限制和软性限制都设置为 0,表示不限制客户端的输出缓冲区大小
- 第二行:将从服务器客户端的硬性限制设置为 256MB,软 性限制设置为 64MB,软性限制的时长为 60 秒
- 第三行:将执行发布与订阅功能的客户端的硬性限制设置为 32MB,软性限制设置为 8MB,软性限制的时长为 60 秒
服务器
执行流程
Redis 服务器与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转,所以一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作
命令请求
Redis 服务器的命令请求来自 Redis 客户端,当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,通过连接到服务器的套接字,将协议格式的命令请求发送给服务器
SET KEY VALUE -> # 命令
*3\r\nS3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n # 协议格式
当客户端与服务器之间的连接套接字因为客户端的写入而变得可读,服务器调用命令请求处理器来执行以下操作:
- 读取套接字中协议格式的命令请求,并将其 保存到客户端状态的输入缓冲区里面
- 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的 argv 属性和 argc 属性里
- 调用命令执行器,执行客户端指定的命令
最后客户端接收到协议格式的命令回复之后,会将这些回复转换成用户可读的格式打印给用户观看,至此整体流程结束
命令执行
命令执行器开始对命令操作:
-
查找命令:首先根据客户端状态的 argv[0] 参数,在命令表 (command table) 中查找参数所指定的命令,并将找到的命令保存到客户端状态的 cmd 属性里面,是一个 redisCommand 结构
命令查找算法与字母的大小写无关,所以命令名字的大小写不影响命令表的查找结果
-
执行预备操作:
- 检查客户端状态的 cmd 指针是否指向 NULL,根据 redisCommand 检查请求参数的数量是否正确
- 检查客户端是否通过身份验证
- 如果服务器打开了 maxmemory 功能,执行命令之前要先检查服务器的内存占用,在有需要时进行内存回收(逐出算法)
- 如果服务器上一次执行 BGSAVE 命令出错,并且服务器打开了 stop-writes-on-bgsave-error 功能,那么如果本次执行的是写命令,服务会拒绝执行,并返回错误
- 如果客户端当前正在用 SUBSCRIBE 或 PSUBSCRIBE 命令订阅频道,那么服务器会拒绝除了 SUBSCRIBE、SUBSCRIBE、 UNSUBSCRIBE、PUNSUBSCRIBE 之外的其他命 令
- 如果服务器正在进行载入数据,只有 sflags 带有 1 标识(比如 INFO、SHUTDOWN、PUBLISH等)的命令才会被执行
- 如果服务器执行 Lua 脚本而超时并进入阻塞状态,那么只会执行客户端发来的 SHUTDOWN nosave 和 SCRIPT KILL 命令
- 如果客户端正在执行事务,那么服务器只会执行客户端发来的 EXEC、DISCARD、MULTI、WATCH 四个命令,其他命令都会被放进事务队列中
- 如果服务器打开了监视器功能,那么会将要执行的命令和参数等信息发送给监视器
-
调用命令的实现函数:被调用的函数会执行指定的操作并产生相应的命令回复,回复会被保存在客户端状态的输出缓冲区里面(buf 和 reply 属性),然后实现函数还会为客户端的套接字关联命令回复处理器,这个处理器负责将命令回复返回给客户端
-
执行后续工作:
- 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志
- 根据执行命令所耗费的时长,更新命令的 redisCommand 结构的 milliseconds 属性,并将命令 calls 计数器的值增一
- 如果服务器开启了 AOF 持久化功能,那么 AOF 持久化模块会将执行的命令请求写入到 AOF 缓冲区里面
- 如果有其他从服务器正在复制当前这个服务器,那么服务器会将执行的命令传播给所有从服务器
-
将命令回复发送给客户端:客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将客户端输出缓冲区中的命令回复发送给客户端,发送完毕之后回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备
Command
每个 redisCommand 结构记录了一个Redis 命令的实现信息,主要属性
struct redisCommand {
// 命令的名字,比如"set"
char *name;
// 函数指针,指向命令的实现函数,比如setCommand
// redisCommandProc 类型的定义为 typedef void redisCommandProc(redisClient *c)
redisCommandProc *proc;
// 命令参数的个数,用于检查命令请求的格式是否正确。如果这个值为负数-N, 那么表示参数的数量大于等于N。
// 注意命令的名字本身也是一个参数,比如 SET msg "hello",命令的参数是"SET"、"msg"、"hello" 三个
int arity;
// 字符串形式的标识值,这个值记录了命令的属性,,
// 比如这个命令是写命令还是读命令,这个命令是否允许在载入数据时使用,是否允许在Lua脚本中使用等等
char *sflags;
// 对sflags标识进行分析得出的二进制标识,由程序自动生成。服务器对命令标识进行检查时使用的都是 flags 属性
// 而不是sflags属性,因为对二进制标识的检查可以方便地通过& ^ ~ 等操作来完成
int flags;
// 服务器总共执行了多少次这个命令
long long calls;
// 服务器执行这个命令所耗费的总时长
long long milliseconds;
};
serverCron
基本介绍
Redis 服务器以周期性事件的方式来运行 serverCron 函数,服务器初始化时读取配置 server.hz 的值,默认为 10,代表每秒钟执行 10 次,即每隔 100 毫秒执行一次,执行指令 info server 可以查看
serverCron 函数负责定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行
- 更新服务器的各类统计信息,比如时间、内存占用、 数据库占用情况等
- 清理数据库中的过期键值对
- 关闭和清理连接失效的客户端
- 进行 AOF 或 RDB 持久化操作
- 如果服务器是主服务器,那么对从服务器进行定期同步
- 如果处于集群模式,对集群进行定期同步和连接测试
时间缓存
Redis 服务器中有很多功能需要获取系统的当前时间,而每次获取系统的当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的 unixtime 属性和 mstime 属性被用作当前时间的缓存
struct redisServer {
// 保存了秒级精度的系统当前UNIX时间戳
time_t unixtime;
// 保存了毫秒级精度的系统当前UNIX时间戳
long long mstime;
};
serverCron 函数默认以每 100 毫秒一次的频率更新两个属性,所以属性记录的时间的精确度并不高
- 服务器只会在打印日志、更新服务器的 LRU 时钟、决定是否执行持久化任务、计算服务器上线时间(uptime)这类对时间精确度要求不高的功能上
- 对于为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能来说,服务器还是会再次执行系统调用,从而获得最准确的系统当前时间
LRU 时钟
服务器状态中的 lruclock 属性保存了服务器的 LRU 时钟
struct redisServer {
// 默认每10秒更新一次的时钟缓存,用于计算键的空转(idle)时长。
unsigned lruclock:22;
};
每个 Redis 对象都会有一个 lru 属性, 这个 lru 属性保存了对象最后一次被命令访问的时间
typedef struct redisObiect {
unsigned lru:22;
} robj;
当服务器要计算一个数据库键的空转时间(即数据库键对应的值对象的空转时间),程序会用服务器的 lruclock 属性记录的时间减去对象的 lru 属性记录的时间
serverCron 函数默认以每 100 毫秒一次的频率更新这个属性,所以得出的空转时间也是模糊的
命令次数
serverCron 中的 trackOperationsPerSecond 函数以每 100 毫秒一次的频率执行,函数功能是以抽样计算的方式,估算并记录服务器在最近一秒钟处理的命令请求数量,这个值可以通过 INFO status 命令的 instantaneous_ops_per_sec 域查看:
redis> INFO stats
# Stats
instantaneous_ops_per_sec:6
根据上一次抽样时间 ops_sec_last_sample_time 和当前系统时间,以及上一次已执行的命令数 ops_sec_last_sample_ops 和服务器当前已经执行的命令数,计算出两次函数调用期间,服务器平均每毫秒处理了多少个命令请求,该值乘以 1000 得到每秒内的执行命令的估计值,放入 ops_sec_samples 环形数组里
struct redisServer {
// 上一次进行抽样的时间
long long ops_sec_last_sample_time;
// 上一次抽样时,服务器已执行命令的数量
long long ops_sec_last_sample_ops;
// REDIS_OPS_SEC_SAMPLES 大小(默认值为16)的环形数组,数组的每一项记录一次的抽样结果
long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES];
// ops_sec_samples数组的索引值,每次抽样后将值自增一,值为16时重置为0,让数组成为一个环形数组
int ops_sec_idx;
};
内存峰值
服务器状态里的 stat_peak_memory 属性记录了服务器内存峰值大小,循环函数每次执行时都会查看服务器当前使用的内存数量,并与 stat_peak_memory 保存的数值进行比较,设置为较大的值
struct redisServer {
// 已使用内存峰值
size_t stat_peak_memory;
};
INFO memory 命令的 used_memory_peak 和 used_memory_peak_human 两个域分别以两种格式记录了服务器的内存峰值:
redis> INFO memory
# Memory
...
used_memory_peak:501824
used_memory_peak_human:490.06K
SIGTERM
服务器启动时,Redis 会为服务器进程的 SIGTERM 信号关联处理器 sigtermHandler 函数,该信号处理器负责在服务器接到 SIGTERM 信号时,打开服务器状态的 shutdown_asap 标识
struct redisServer {
// 关闭服务器的标识:值为1时关闭服务器,值为0时不做操作
int shutdown_asap;
};
每次 serverCron 函数运行时,程序都会对服务器状态的 shutdown_asap 属性进行检查,并根据属性的值决定是否关闭服务器
服务器在接到 SIGTERM 信号之后,关闭服务器并打印相关日志的过程:
[6794 | signal handler] (1384435690) Received SIGTERM, scheduling shutdown ...
[6794] 14 Nov 21:28:10.108 # User requested shutdown ...
[6794] 14 Nov 21:28:10.108 * Saving the final RDB snapshot before exiting.
[6794) 14 Nov 21:28:10.161 * DB saved on disk
[6794) 14 Nov 21:28:10.161 # Redisis now ready to exit, bye bye ...
管理资源
serverCron 函数每次执行都会调用 clientsCron 和 databasesCron 函数,进行管理客户端资源和数据库资源
clientsCron 函数对一定数量的客户端进行以下两个检查:
- 如果客户端与服务器之间的连接巳经超时(很长一段时间客户端和服务器都没有互动),那么程序释放这个客户端
- 如果客户端在上一次执行命令请求之后,输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多的内存
databasesCron 函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时对字典进行收缩操作
持久状态
服务器状态中记录执行 BGSAVE 命令和 BGREWRITEAOF 命令的子进程的 ID
struct redisServer {
// 记录执行BGSAVE命令的子进程的ID,如果服务器没有在执行BGSAVE,那么这个属性的值为-1
pid_t rdb_child_pid;
// 记录执行BGREWRITEAOF命令的子进程的ID,如果服务器没有在执行那么这个属性的值为-1
pid_t aof_child_pid
};
serverCron 函数执行时,会检查两个属性的值,只要其中一个属性的值不为 -1,程序就会执行一次 wait3 函数,检查子进程是否有信号发来服务器进程:
- 如果有信号到达,那么表示新的 RDB 文件已经生成或者 AOF 重写完毕,服务器需要进行相应命令的后续操作,比如用新的 RDB 文件替换现有的 RDB 文件,用重写后的 AOF 文件替换现有的 AOF 文件
- 如果没有信号到达,那么表示持久化操作未完成,程序不做动作
如果两个属性的值都为 -1,表示服务器没有进行持久化操作
-
查看是否有 BGREWRITEAOF 被延迟,然后执行 AOF 后台重写
-
查看服务器的自动保存条件是否已经被满足,并且服务器没有在进行持久化,就开始一次新的 BGSAVE 操作
因为条件 1 可能会引发一次 AOF,所以在这个检查中会再次确认服务器是否已经在执行持久化操作
-
检查服务器设置的 AOF 重写条件是否满足,条件满足并且服务器没有进行持久化,就进行一次 AOF 重写
如果服务器开启了 AOF 持久化功能,并且 AOF 缓冲区里还有待写入的数据, 那么 serverCron 函数会调用相应的程序,将 AOF 缓冲区中的内容写入到 AOF 文件里
延迟执行
在服务器执行 BGSAVE 命令的期间,如果客户端发送 BGREWRITEAOF 命令,那么服务器会将 BGREWRITEAOF 命令的执行时间延迟到 BGSAVE 命令执行完毕之后,用服务器状态的 aof_rewrite_scheduled 属性标识延迟与否
struct redisServer {
// 如果值为1,那么表示有 BGREWRITEAOF命令被延迟了
int aof_rewrite_scheduled;
};
serverCron 函数会检查 BGSAVE 或者 BGREWRITEAOF 命令是否正在执行,如果这两个命令都没在执行,并且 aof_rewrite_scheduled 属性的值为 1,那么服务器就会执行之前被推延的 BGREWRITEAOF 命令
执行次数
服务器状态的 cronloops 属性记录了 serverCron 函数执行的次数
struct redisServer {
// serverCron 函数每执行一次,这个属性的值就增 1
int cronloops;
};
缓冲限制
服务器会关闭那些输入或者输出缓冲区大小超出限制的客户端
初始化
初始结构
一个 Redis 服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程
第一步:创建一个 redisServer 类型的实例变量 server 作为服务器的状态,并为结构中的各个属性设置默认值,由 initServerConfig 函数进行初始化一般属性:
- 设置服务器的运行 ID、默认运行频率、默认配置文件路径、默认端口号、默认 RDB 持久化条件和 AOF 持久化条件
- 初始化服务器的 LRU 时钟,创建命令表
第二步:载入配置选项,用户可以通过给定配置参数或者指定配置文件,对 server 变量相关属性的默认值进行修改
第三步:初始化服务器数据结构(除了命令表之外),因为服务器必须先载入用户指定的配置选项才能正确地对数据结构进行初始化,所以载入配置完成后才进性数据结构的初始 化,服务器将调用 initServer 函数:
- server.clients 链表,记录了的客户端的状态结构;server.db 数组,包含了服务器的所有数据库
- 用于保存频道订阅信息的 server.pubsub_channels 字典, 以及保存模式订阅信息的 server.pubsub_patterns 链表
- 用于执行 Lua 脚本的 Lua 环境 server.lua
- 保存慢查询日志的 server.slowlog 属性
initServer 还进行了非常重要的设置操作:
- 为服务器设置进程信号处理器
- 创建共享对象,包含 OK、ERR、整数 1 到 10000 的字符串对象等
- 打开服务器的监听端口
- 为 serverCron 函数创建时间事件, 等待服务器正式运行时执行 serverCron 函数
- 如果 AOF 持久化功能已经打开,那么打开现有的 AOF 文件,如果 AOF 文件不存在,那么创建并打开一个新的 AOF 文件 ,为 AOF 写入做好准备
- 初始化服务器的后台 I/O 模块(BIO), 为将来的 I/O 操作做好准备
当 initServer 函数执行完毕之后, 服务器将用 ASCII 字符在日志中打印出 Redis 的图标, 以及 Redis 的版本号信息
还原状态
在完成了对服务器状态的初始化之后,服务器需要载入RDB文件或者AOF 文件, 并根据文件记录的内容来还原服务器的数据库状态:
- 如果服务器启用了 AOF 持久化功能,那么服务器使用 AOF 文件来还原数据库状态
- 如果服务器没有启用 AOF 持久化功能,那么服务器使用 RDB 文件来还原数据库状态
当服务器完成数据库状态还原工作之后,服务器将在日志中打印出载入文件并还原数据库状态所耗费的时长
[7171] 22 Nov 22:43:49.084 * DB loaded from disk: 0.071 seconds
驱动循环
在初始化的最后一步,服务器将打印出以下日志,并开始执行服务器的事件循环(loop)
[7171] 22 Nov 22:43:49.084 * The server is now ready to accept connections on pert 6379
服务器现在开始可以接受客户端的连接请求,并处理客户端发来的命令请求了
慢日志
基本介绍
Redis 的慢查询日志功能用于记录执行时间超过给定时长的命令请求,通过产生的日志来监视和优化查询速度
服务器配置有两个和慢查询日志相关的选项:
- slowlog-log-slower-than 选项指定执行时间超过多少微秒的命令请求会被记录到日志上
- slowlog-max-len 选项指定服务器最多保存多少条慢查询日志
服务器使用先进先出 FIFO 的方式保存多条慢查询日志,当服务器存储的慢查询日志数量等于 slowlog-max-len 选项的值时,在添加一条新的慢查询日志之前,会先将最旧的一条慢查询日志删除
配置选项可以通过 CONFIG SET option value 命令进行设置