第 9 章 数据库
本章将对Redis服务器的数据库实现进行详细介绍,说明服务器保存数据库的方法,客户端切换数据库的方法,数据库保存键值对的方法,以及针对数据库的添加、删除、查看更新操作的实现方法等。除此之外,本章还会说明服务器保存键的过期时间的方法,以及服务器自动删除过期键的方法,以及数据库通知功能的实现方法。
9.1 服务器中的数据库
Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer
结构的db
数组中,db
数组的每个项都是一个redis.h/redisDb
结构,每个redisDB
结构代表一个数据库:
1 | struct redisServer { |
dbnum属性决定了Redis初始化时创建数据库的数量,默认为16个,结构如下图
9.2 切换数据库
每个Redis
客户端都有自己的目标数据库,每当客户端执行数据库操作的时候,目标数据库就会成为这些命令的操作对象。
默认情况下,Redis
客户端的目标数据库为0号数据库,但客户端可以通过执行SELECT
命令来切换目标数据库。
1 | SELECT 2 |
在服务器内部,客户端状态redisClient
结构的db
属性记录了客户端当前的目标数据库,这个属性是一个指向redisDB
结构的指针,被指向的元素就是客户端的目标数据库。
到目前为止,Redis仍然没有可以返回客户端目标数据库的命令。在数次切换数据库之后,你很可能会忘记自己当前正在使用的是哪个数据库。当出现这种情况时,为了避免对数据库进行误操作,在执行Redis命令特别是像FLUSHDB
这样的危险命令之前,最好先执行一个SELECT
命令,显式切换到指定的数据库,然后才执行别的命令。
9.3 数据库键空间
Redis是一个键值对(key-value pair)数据库服务器,redis.h
结构的dict
字典保存了数据库中的所有键值对,我们将这个字典称为键空间
- 键空间的键也就是数据库的键,每个键都是一个字符串对象。
- 键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的任意一种Redis对象。
9.3.1 添加新键
添加一个新键值对到数据库,实际上就是将一个新键值对添加到键空间字典里面,其中键为字符串对象,而值则为任意一种类型的Redis对象。
9.3.2 删除键
删除数据库中的一个键,实际上就是在键空间里面删除键所对应的键值对对象。
9.3.3 更新键
略
9.3.4 对键取值
略
9.3.5 其他键空间操作
用于清空整个数据库的FLUSHDB
命令,就是通过删除键空间中的所有键值对来实现的。
又比如说,用于随机返回数据库中某个键的 RANDOMKEY
命令,就是通过在键空间中随机返回一个键来实现的。
9.3.6 读写键空间时的维护操作
当使用Redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作还会执行一些额外的维护操作,其中包括
-
在读取一个键之后,服务器会根据键是否存在来更新服务器的键空间命中次数或键空间不命中次数
-
在读取一个键之后,服务器会更新键的LRU(最后一次使用)时间,这个值可以用于计算键的闲置时间
-
如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键,然后才执行余下的其他操作
-
如果有客户端使用
WATCH
命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏,从而让事务程序注意到这个键已经被修改过
9.4 设置键的生存时间或过期时间
客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL),在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为0的键。
9.4.1 设置过期时间
Redis有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除)
EXPIRE
:用于将键key的生存时间设置为xx秒。PEXPIRE
:用于将键key的生存时间设置为xx毫秒。EXPIREAT
:用于将键key的过期时间设置为指定的秒数时间戳PEXPIREAT
:用于将键key的过期时间设置为指定的毫秒数时间戳
9.4.2 保存过期时间
redisDb
结构的expires
字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典:
- 过期字典的键是一个指针,这个指针指向键空间中的某个键对象
- 过期字典的值是一个
long long
类型的整数,这个整数保存了键所指向的数据库键的过期时间,它是一个毫秒精度的UNIX时间戳
例如下图
9.4.3 移除过期时间
PERSIST
命令可以移除一个键的过期时间
1 | PERSIST message |
9.4.4 计算并返回剩余生存时间
TTL
命令以秒为单位返回键的剩余生存时间,而PTTL
命令则以毫秒为单位返回键的剩余生存时间:
1 | TTL message |
9.4.5 过期键的判定
通过过期字典,程序可以用以下步骤检查一个给定键是否过期
- 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间。
- 检查当前UNIX时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则的话,键未过期。
9.5 过期键删除策略
过期键有三种删除策略
- 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检査取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
- 定期删除:每隔一段时间,程序就对数据库进行一次检査,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。
9.5.1 定时删除
定时删除策略对内存是最友好的:通过使用定时器,定时删除策略可以保证过期键会尽可能快地被删除,并释放过期键所占用的内存。
它对CPU时间是最不友好的:在过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分CPU时间,
9.5.2 惰性删除
惰性删除策略对CPU时间来说是最友好的:程序只会在取岀键时才对键进行过期检査这可以保证删除过期键的操作只会在非做不可的情况下进行,并且删除的目标仅限于当前处理的键,这个策略不会在删除其他无关的过期键上花费任何CPU时间。
惰性删除策略的缺点是,它对内存是最不友好的:如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被访问,它所占用的内存永远不会释放。
9.5.3 定期删除
定期删除策略是前两种策略的一种整合和折中:
- 定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
- 除此之外,通过定期删除过期键,定期删除策略有效地减少了因为过期键而带来的内存浪费。
9.6 Redis的过期删除策略
Redis服务器实际使用的是惰性删除和定期删除两种策略
9.6.1 惰性删除策略的实现
惰性删除策略由db.c/expireIfNeeded
函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded
函数对输入键进行检查:
- 如果输入键已经过期,那么
expireIfNeeded
函数将输入键从数据库中删除 - 如果输入键未过期,那么
expireIfNeeded
函数不做动作。
流程图如下
expireIfNeeded
源码如下
1 | /* |
9.6.2 定期删除策略的实现
过期键的定期删除策略由redis.c/activeExpireCycle
函数实现,每当Redis的服务器周期性操作redis.c/serverCron
函数执行时,activeExpireCycle
函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires
字典中随机检查一部分键的过期时间,并删除其中的过期键。
1 | # 默认每次检查的数据库数量 |
activeExpireCycle
函数的工作模式可以总结为
- 函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
- 全局变量
current_db
会记录当前activeExpireCycle
函数检查的进度,并在下一次activeExpireCycle
函数调用时,接着上一次的进度进行处理 - 服务器中的所有数据库都会被检查一遍,这时函数将
current_db
变量重置为0,
9.7 AOP、RDB和复制功能对过期键的处理
在这一节,我们将探讨过期键对Redis服务器中其他模块的影响
9.7.1 生成RDB文件
在执行SAVE
命令或者BGSAVE
命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键将不保存到新创建的RDB文件中。
9.7.2 载入RDB文件
在启动Redis服务器时,如果服务器开启了RDB功能,那么服务器将对RDB文件进行载入
- 如果服务器以主服务器模式运行,那么在载入RDB文件时,程序会对文件中保存的键进行检査,未过期的键会被载入到数据库中,而过期键则会被忽略,
- 如果服务器以从服务器模式运行,那么在载入RDB文件时,文件中保存的所有键,不论是否过期,都会被载入到数据库中。不过,因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所以一般来讲,过期键对载入RDB文件的从服务器也不会造成影响。
9.7.3 AOF文件写入
当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响。
当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显式地记录该键已被删除。
9.7.4 AOF重写
在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。
9.7.5 复制
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:
- 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键。
- 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
- 从服务器只有在接到主服务器发来的DEL命令之后,才会删除过期键。
具体流程如下
9.8 数据库通知
数据库通知功能可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。
其中
- 关注“某个键执行了什么命令”的通知称为键空间通知
- 关注“某个命令被什么键执行了”的通知称为键事件通知
9.8.1 发送消息
发送数据库通知的功能是由notify.c/notifykeyspaceEvent
函数实现的:
每当一个Redis命令需要发送数据库通知的时候,该命令的实现函数就会调用notifyKeyspaceEvent
函数,并向函数传递该命令所引发的事件的相关信息。
例如下面是SADD
命令的实现函数saddCommand
的一部分代码
1 | void saddCommand(redisClient *c){ |
9.8.2 发送通知的实现
下面是notifyKeyspaceEvent
函数的伪代码实现
def notifykeyspaceEvent(type, event, key, dbid):
# 如果给定的通知不是服务器允许发送的通知,那么直接返回
if not (server.notify_keyspace_events & type):
return
# 发送键空间通知
if server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE:
# 将通知发送给频道"__keyspace@{dbid}__:{key}
# 内容为键所发生的事件<event>
# 构建频道名字
chan = "__keyspace@{dbid}__:{key}". format(dbid=dbid, key=key)
# 发送通知
pubsubPublishMessage(chan,event)
# 发送键事件通知
if server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT:
# 将通知发送给频道"__keyevent@{dbid}__:{event}
# 内容为发生事件的键<key>
# 构建频道名字
chan = "__keyspace@{dbid}__:{event}". format(dbid=dbid, event=event)
# 发送通知
pubsubPublishMessage(chan, key)