第 9 章 数据库
9.1 服务器中的数据库
redis.h/redisServer 结构的 db 数组中,每个元素都是 redis.h/redisDb 结构,代表一个数据库
初始化服务器时会根据服务器状态的 dbnum 属性来决定创建多少个数据库
dbnum 由服务器配置的 database 选项决定,默认为 16,所以会默认创建 16 个数据库
9.2 切换数据库
Redis 客户端的目标数据库为 0 号数据库,可以通过 SELECT 命令来切换目标数据库
127.0.0.1:6379> set 32 "hello you"
OK
127.0.0.1:6379> get 32
"hello you"
127.0.0.1:6379> select 2
OK
127.0.0.1:6379[2]> get 32
(nil)
127.0.0.1:6379[2]> set 32 "you hello"
OK
127.0.0.1:6379[2]> get 32
"you hello"
在服务器内部,客户端状态的 redisClient 结构的 db 属性记录了客户端当前的目标数据,是一个指向 redisDb 结构的指针
redisClint.db 指针指向 redisServer.db 数组的一个元素,被指向的元素则是客户端的目标数据库
9.3 数据库键空间
redis 是键值对数据库服务器,每个数据库都是 redis.h/redisDb 结构表示,dict 字典保存了数据库中的键值对,这个字典为键空间
键空间与数据库是直接对应的
- 键空间的键是数据库的键,也是一个字符串对象
- 键空间的值是数据库的值,每个值可以是任意一种 Redis 对象
所有针对数据库的操作,实际上都是通过对键空间字典进行操作实现的
添加新键
添加新键值对到数据库,实际上就是将新键值对添加到键空间中,键为字符串对象,值为任意一种 Redis 对象
删除键、更新键、对键取值
同理添加新键
其他命令
FLUSHDB、RANDOMKEY、DESIZE 同样也是通过对键空间进行操作的
9.3.6
不仅只有指定的读写操作,还有一些额外的维护操作,包括
-
服务器会根据键是否存在来更新键空间命中次数和不命中次数,使用 INFO status 命令的 keyspace_hits 和 keyspace_misses 属性查看
-
服务器会更新键的 lru (最后一次使用)时间,可以计算键的闲置时间, OBJECT idletime key 查看
-
读取一个键时发现已经过期了,则会先删除再进行余下的操作
-
如果客户端使用 watch 命令监视了某个键,则服务器修改后,会将其标记为脏,从而让事务程序注意到键已经修改
-
每修改一个键后,脏键计数器的值 + 1,会触发服务器的持久化以及复制操作
-
如果服务器开启数据库通知功能,在对键进行修改之后,服务器会按配置发送相应的数据库通知
9.4 设置键的生存时间或过期时间
9.4.1 设置过期时间
所有命令底层还是由 PEXPIRE 命令转换而来的
9.4.2 保存过期时间
redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,即过期字典:
-
键为指针,指向键空间的某个键对象
-
值为 long long 类型,保存了键指向的键空间键对象的过期时间,毫秒精度的 UNIX 时间戳
键空间和过期字典的键都指向同一个,此处为明显展示而列举两个
9.4.3 移除过期命令
PERSTST 命令移除键的过期时间:
在过期字典中查找给定的键,并解除键和值在过期字典中的关联
9.4.4 计算并返回剩余生存时间
TTL 秒为单位,PTTL 毫秒为单位,通过计算键的过期时间与当前时间之差
9.4.5 过期键的判定
- 检查给定键是否存在于过期字典;存在则取得过期时间
- 检查当前 UNIX 时间戳是否大于键的过期时间;是则键已经过期,否则未过期
或者使用 TTL 或 PTTL 命令,但实际中是使用 is_expired 函数
9.5 过期键删除策略
- 定时删除:主动
- 设置过期时间的同时创建定时器,过期时间到达立即删除
- 惰性删除:被动
- 当从键空间获取键时,检查是否过期,过期则删除,没有则返回
- 定期删除:主动
- 每隔一段时间,对数据库检查,删除多少,检查多少数据库则由算法决定
9.5.1 定时删除
-
内存友好
-
CPU 最不友好
-
需要使用 Redis 的时间事件,时间事件实现方式为无序链表,则查找一个事件的时间复杂度为 O(N)
9.5.2 惰性删除
-
对 CPU 最友好
-
对内存最不友好
-
浪费大量内存
9.5.3 定期删除
每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响,有效减少了过期键的内存浪费
时长和频率难以确定:
-
执行频繁、时间长,会退化成定时删除
-
执行少、时间短,退化为惰性删除
9.6 Redis 的过期键删除策略
9.6.1 惰性删除策略的实现
由 db.c/expireIfNeeded 函数实现,所有读写数据库的 Redis 命令在执行之前都会调用此函数对输入键进行检查
- 过期,此函数将键从数据库中删除
- 未过期,此函数不做动作
命令执行时也需要按照两种情况执行
9.6.2 定期删除策略的实现
由 redis.c/activeExpireCycle 函数实现,Redis 周期性操作 redis.c/serverCron 函数执行时,activeExpireCycle 函数会调用,在规定时间内,分多次遍历各个数据库,从 expires 字典中随机检查一部分键的过期时间,并删除过期键
activeExpireCycle:
- 运行时,从一定数量的数据库取出一定数量的随机键进行检查,删除过期键
- current_db 会记录当前函数检查的进度,并在下一次调用此函数时按上一次进度进行处理
- 函数不断执行,所有数据库都会被检查一遍,current_db 置为 0 ,开始新一轮的检查
9.7 AOF、ROB 和复制功能对过期键的处理
9.7.1 生成 RDB 文件
在执行 save 命令或者 bgsave 命令创建新的 RDB 文件时,对键会进行检查,已经过期的键不会被保存到新创建的 RDB 文件中
9.7.2 载入 RDB 文件
对 RDB 文件进行载入:
-
以主服务器模式运行,在载入时会对文件中保存的键进行检查,未过期的键会被载入到数据库中,已过期的会被忽略
-
以从服务器模式运行,文件中所有键都会被载入数据库,但是主从服务器进行数据同步时,从服务器的数据库就会被清空
9.7.3 AOF 文件写入
服务器以 AOF 持久化模式运行时,如果某个键过期,但没有被惰性删除或者定期删除,AOF 文件不会因这个过期键产生任何影响
当过期键被删除后,会向 AOF 文件追加一条 DEL 命令显示记录该键已被删除
9.7.4 AOF 重写
类似生成 RDB 文件,对键检查,已过期的不会被保存
9.7.5 复制
当服务器在复制模式下,从服务器的过期删除动作由主服务器控制:
- 主服务器删除一个过期键,显示向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键
- 从服务器在执行客户端发送的读命令时,遇到过期键也不会将过期键删除,当成未过期键处理
- 从服务器只有在接到猪服务器发来的 DEL 命令之后才会删除过期键
通过主服务器控制从服务器统一删除过期键,来保证主从服务器的一致性
9.8 数据库通知
让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及命令的执行情况
- 键空间通知:
- 某个键执行了什么命令
- 键事件通知
- 某个命令被什么键执行了
notify-keyspace-events 决定发送通知的类型
-
发送所有类型的键空间通知和键事件通知:AKE
-
发送所有类型的键空间通知:AK
-
发送所有类型的键事件通知:AE
-
发送和字符串键相关的键空间通知:K$
-
发送和列表键相关的键事件通知:EL
9.8.1 发送通知
发送数据库通知功能由 notify.c/notifyKeyspaceEvent 实现:
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid);
type 为当前想要发送的通知的类型,根据这个值来判断通知是否就是服务器配置 notify-keyspace-event 选项所选定的通知类型来决定是否发送通知
event 事件名称
keys 产生事件的键
dbid 产生事件的数据库号
根据这些参数来构建事件通知的内容、以及接受通知的频道名
REDIS_NOTIFY_SET:集合键通知
REDIS_NOTIFY_GENERIC:通用类型通知
notifyKeyspaceEvent:
- server.notify_keyspace_events 为 notify-keyspace-events 选项设置的值,如果给定的通知类型 type 不是服务器允许发送的通知类型,函数直接返回
- 是服务器允许发送的通知类型,再检测是否允许发送键空间通知,允许则构建并发送事件通知
- 最后再检测是否允许发送键事件通知,允许中午构建并发送事件通知
pubsubPublishMessage 是PUBLISH 命令的实现函数