第09章-数据库
9.1 服务器中的数据库
|
|
dunum
属性的值由配置项database
决定,默认为16
9.2 切换数据库
|
|
- 默认为0号数据库
- 可以使用
select
切换
9.3 数据库键空间
|
|
dict
中存放了数据库所有的键(字符串类型),称为键空间
9.3.6 读写键空间时的维护操作
-
在读取一个键之后(读写都涉及读),服务器会根据键是否存在来更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数,这两个值可以在
INFO stats
命令的keyspace_hits
属性和keyspace_misses
属性中查看 -
在读取一个键之后,服务器会更新键的LRU时间,可以使用
object idletime <key>
命令查看键的闲置时间 -
如果在读取一个键时发现它已过期,会先删除这个过期键
-
如果客户端使用watch监视了某个键,那么服务器在对被监视的键进行修改后,会将其标记为dirty;并对脏键计数器加1
-
如果服务器开启了数据库通知功能,那么对键修改后,将按配置发送相应的数据库通知
9.4 设置键的生存时间或过期时间
expire, pexpire, expireat, pexpireat
都是用pexpireat
实现的
redisDb
结构体中的expires
字典保存了数据库所有键的过期时间,键空间和过期字典共用键对象,以节约空间。
persist
命令可以移除一个键的过期时间
9.5 过期键删除策略
- 定时删除 (对CPU和吞吐量的影响)
- 惰性删除 (对内存的影响)
- 定期删除 (难以确定操作时长和频率)
当前(这本书的当前)redis时间事件的实现方式是无序链表,查找一个事件的时间复杂度为O(N) (我直接好家伙
9.6 redis的过期键删除策略
redis中实际使用的是惰性删除和定期删除两种策略
9.6.1 惰性删除策略实现
过期键的惰性删除由db.c/expireIfNeeded
函数实现,相当一个过滤器
9.6.2 定期删除策略实现
过期键的定期删除策略由expire.c/activeExpireCycle
函数实现,每当服务器周期性操作server.c/serverCron
函数时,该函数会被调用,在规定时间内分多次遍历服务器中的各个数据库,从数据库的expires
字典中随机检查一部分键的过期时间,并删除其中的过期键.
一个全局下标标记此次从哪个数据库开始删除,遍历了指定个数的数据库或运行指定时间后退出
9.7 AOF、RDB和复制功能对过期键的处理
- 执行SAVE命令或者BGSAVE命令时,已过期的键不会被保存在新创建的RDB中(好像非常废话)
- 载入RDB文件时,也会忽略过期的键;从服务器执行RDB载入不会删除已有的过期键,但是在主从同步时,从服务器会执行flushdb命令,一般来说过期键对载入RDB文件没有影响。
- 对于AOF文件而言,过期的键会写入一条DEL命令
- AOF重写时也会忽略过期的键
- 当服务器在复制模式下,主服务器在删除一个过期键后会发送del命令给从服务器;从服务器即使在读到过期键也不会将其删除,而是等待主服务器的命令
9.8 数据库通知
redis2.8 新增数据库通知,这个功能可以让客户端通过订阅给定的channel或者pattern来获知数据库汇总键的变化,以及数据库中命令的执行情况。
有两种通知:
- 键空间通知(key-space notification),某个键执行了什么命令。
- 格式:
subscribe __keyspace@0_ _:key
,表明订阅数据库0的key
这个键
- 格式:
- 键事件通知(key-event notification),某种命令被哪个键执行了。
- 格式:
subscribe __keyevent@0_ _:del
,表明监听数据库0的删除命令
- 格式:
详细参考文档
服务器配置的notify-keyspace-events
选项决定了服务器所发送通知的类型,不同选项如下:
|
|
9.8.1 发送通知
notify.c/notifyKeyspaceEvent
函数
第10章-RDB持久化
10.1 RDB文件的创建与载入
SAVE和BGSAVE都是由rdb.c/rdbSave
函数以不同的调用方式完成
RDB文件的载入是服务器启动时自动执行的,没有专门用于载入RDB文件的命令,函数为rdb.c/rdbLoad
BGSAVE命令执行期间,服务器处理SAVE、BGSAVE、BGREWRITEAOF三个命令的方式会和平时不同:
- 首先会禁止执行SAVE命令防止产生竞争条件;
- 其次是不允许执行第二个BGSAVE命令;
- 最后会执行期间会延迟BGREWITEAOF命令到BGSAVE命令执行完成(因为这两个命令都是fork子进程,数据规模越大,开销越大,故禁止)
服务器在载入RDB文件期间,会一直处于阻塞状态
10.2 自动间隔性保存
服务器配置项:
|
|
相关数据结构server.h
:
|
|
dirty计数器记录了上次保存后进行了多少次修改(包括写入、更新、删除)
serverCron
函数每隔100ms就会执行一次,其中包含了检查是否满足保存的条件,如果满足则执行BGSAVE命令
10.3 RDB文件结构
文件存储结构像协议一样有啥好讲的? 序列化和反序列化不是没有学问,而是我为什么要看? checksum是在载入的过程中计算,载入完成后,如果计算结果和checksum不同则认为损坏
字符串压缩与不压缩
[todo]
10.4 分析RDB文件
- 不包含任何键值对的RDB
- 包含字符串键的RDB
- 包含带有过期时间的字符串键的RDB文件
- 包含一个集合键的RDB文件
redis自带redis-check-dump
obsidian tag
#redis #RDB持久化
第11章-AOF持久化
被写入AOF文件的所有命令都是以redis的命令请求协议格式保存的(纯文本格式)
11.1 AOF持久化的实现
|
|
AOF持久化功能的实现可以分为三个步骤:
- 命令追加
- 当AOF功能打开时,服务器每执行一个写命令,会以协议格式将被执行的写命令追加到aof_buf缓冲区的末尾
- 命令写入
- redis服务器进程是一个事件循环,每结束一个事件循环之前,都会调用
flushAppendOnlyFile
函数,根据配置appendfsync选项来考虑是否需要将aof_buf
缓冲区中的内容写入和保存到AOF文件里面 - appendfsync配置有三个选项:
always, everysec, no
- redis服务器进程是一个事件循环,每结束一个事件循环之前,都会调用
- 命令同步
11.2 AOF文件的载入与数据还原
创建一个不带网络连接的伪客户端,重放命令
11.3 AOF重写
实际上AOF文件重写不需要对现有的AOF文件进行任何的读取、分析或写入操作,只需要读取当前服务器数据库数据即可。
在实际中,为了避免客户端在执行命令时可能会出现输入缓冲区溢出,在处理列表、哈希表、集合、有序集合这些类型时,会先检查其数量,如果超过redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD
,则会将命令拆成多条。
aof可能涉及大量的写操作,所以fork一个子进程。但这又带来子进程数据和主进程不一致的情况。解决方法是,设置了一个AOF重写缓冲区,用于存放执行重写期间服务器遇到的命令,等重写完成后再执行缓冲区中的命令,以保证数据一致。
第12章-事件
redis是一个事件驱动程序,有两类事件:
- 文件事件(file event)
- 时间事件(time event)
12.1 文件事件
redis基于reactor模式开发了自己的网络时间处理器,称为file event handler。
使用IO多路复用程序来同时监听多个套接字
虽然文件时间处理其以单线程方式运行,但是通过IO多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与redis服务器中其他同样以单线程方式运行的模块进行对接,这也保持了redis内部单线程设计的简单性。
文件事件处理器的构成:套接字 -> IO多路复用程序 -> dispatcher -> 事件处理器
IO多路复用程序总是将所有产生事件的套接字都放到一个队列里面,然后根据这个队列,有序、同步、每次一个套接字的方式向dispatcher发送套接字。
redis的IO多路复用程序的所有功能都是通过包装常见的select、epoll、kqueue这些IO多路复用函数库来实现的,见文件ae_select.c, ae_epoll.c, ae_kqueue.c
12.1.3 事件类型
ae.h/AE_READABLE
- 客户端对套接字执行write操作、close操作,或者有新的acceptable套接字出现时
ae.h/AE_WRITEABLE
- 当客户端对套接字执行read操作时
IO多路复用允许服务器同时监听套接字的AE_READABLE时间和AE_WRITABLE事件(网络延时导致同时到达?),优先处理AE_READABLE事件。 (非常合理)
12.1.4 API
ae.c/aeCreateFileEvent
ae.c/aeDeleteFileEvent
ae.c/aeGetFileEvents
ae.c/aeWait
ae.c/aeApiPoll
ae.c/aeProcessEvent
ae.c/aeGetApiName
[#todo]
12.1.5 文件事件的处理器
- 连接应答处理器
networking.c/acceptTcpHandler
- 命令请求处理器
networking.c/readQueryFromClient
- 命令回复处理器
networking.c/sendReplyToClient
12.2 时间事件
分为两类:
- 定时事件
- 周期事件
一个时间事件主要由以下三个属性组成:
- id:时间事件全局唯一id,顺序递增
- when:毫秒精度的UNIX时间戳,记录了时间事件的到达时间
- timeProc:时间事件处理器,一个函数,用来处理时间事件
一个时间事件是定时事件还是周期事件,取决于时间事件处理器的返回值
12.2.1 实现
服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找到达的时间事件
注意:
无序链表并不影响时间事件处理器的性能,因为正常模式下服务器只使用serverCron一个时间事件(O(n)就不优化成O(logn)了?)
12.2.2 API
12.2.3 serverCron函数
redis.c/serverCron
的主要工作包括:
- 更新服务器的各类统计信息,例如时间、内存占用、数据库占用情况等
- 清理数据库中过期的键值对
- 关闭和清理连接失效的客户端
- 尝试进行AOF或RDB持久化操作
- 如果服务器是主服务器,那么对从服务器进行定期同步
- 如果处于集群模式,对集群进行定期同步和连接测试
12.3 事件的调度与执行
事件的调度和执行由ae.c/aeProcessEvents
负责
第13章-客户端
redis.h/redisClient
13.1 客户端属性
- 套接字描述符
- 名字
- 标志
- redis_force_aof
- redis_force_repl
- 输入缓冲区
- 命令与命令参数
- 命令的实现函数
- 输出缓冲区
- 身份验证
- 时间
13.2 客户端的创建与关闭
- 创建
- 关闭
第14章-服务端
14.1 命令请求的执行过程
- 发送命令请求
- 客户端会将这个命令请求转换成协议格式
- 读取命令请求
- 命令执行器
- 查找命令实现
- 执行预备操作
- 调用命令的实现函数
- 执行后续工作
- 将命令恢复发送给客户端
- 客户端接收并打印回复
14.2 serverCron
函数
- 更新服务器时间缓存
- 每次都获取系统当前时间都要执行一遍系统调用,所以缓存一份(我表示还能这么玩?)
- 只适用于时间精度要求低的
- 更新LRU时钟
- 更新服务器每秒执行命令次数
- 更新服务器内存峰值记录
- 处理
SIGTER
信号- 接收到信号后更新
shutdown_asap
标识
- 接收到信号后更新
- 管理客户端资源
- 关闭长时间无互动的客户端连接
- 如果客户端上次命令请求占用的输入缓冲区大小超过一定长度,那么将其释放,并创建一个新的输入缓冲区
- 管理数据库资源
- 调用
databasesCron
函数对部分数据库进行检查,删除其中的过期键,并在需要时对字典进行收缩工作
- 调用
- 执行被延迟的BGREWRITEAOF
- 检查持久化操作的运行状态
- aof_child_pid
- rdb_child_pid
- 只要一个不是-1就执行一次wait3函数,检查子进程是否有信号发来服务器进程
- 将AOF缓冲区中的内容写入AOF文件
- 关闭异步客户端
- 关闭那些输出缓冲区超出限制的客户端
- 增加cronloops计数器的值
14.3 初始化服务器
- 初始化服务器状态结果
redis.c/initServerConfig
- 载入配置选项
- 初始化服务器数据结构
- 相关字段
server.clients
链表server.db
数组server.pubsub_channels
server.lua
server.slowlog
- 调用
initServer()
- 为服务器设置进程信号处理器
- 创建共享对象
- 打开服务器监听端口
- 为serverCron函数创建时间事件,等待服务器正式运行时执行serverCron函数
- 如果使用AOF模式,那么打开AOF文件;否则新建文件
- 初始化服务器的后台IO模块(bio),为将来的IO操作做好准备
- 相关字段
- 还原数据库状态
- 即载入AOF文件或RDB文件
- 执行事件循环