第09章-数据库

9.1 服务器中的数据库

1
2
3
4
5
6
struct redisServer {
	...
	redisDb *db;   // 指向一个数据库数组
	int dbnum;     /* Total number of configured DBs */
	...
}; 
  • dunum 属性的值由配置项database决定,默认为16

9.2 切换数据库

1
2
3
4
5
struct client {
	...
    redisDb *db;  // 记录client正在使用的db 
    ... 
}
  • 默认为0号数据库
  • 可以使用select切换

9.3 数据库键空间

1
2
3
4
5
6
typedef struct redisDb {
	...
	dict *dict;    /* The keyspace for this DB */
	dict *expires; /* Timeout of keys with a timeout set */
	... 
} redisDb;
  • 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 选项决定了服务器所发送通知的类型,不同选项如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
K     Keyspace events, published with __keyspace@<db>__ prefix.
E     Keyevent events, published with __keyevent@<db>__ prefix.
g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
$     String commands
l     List commands
s     Set commands
h     Hash commands
z     Sorted set commands
t     Stream commands
d     Module key type events
x     Expired events (events generated every time a key expires)
e     Evicted events (events generated when a key is evicted for maxmemory)
m     Key miss events (events generated when a key that doesn't exist is accessed)
n     New key events (Note: not included in the 'A' class)
A     Alias for "g$lshztxed", so that the "AKE" string means all the events except "m" and "n".

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 自动间隔性保存

服务器配置项:

1
2
3
save 900 1    # 900秒内发生至少一次键修改则保存一次 
save 300 10
save 60 10000 

相关数据结构server.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct saveparam {
	time_t seconds;
	int changes;
};

struct redisServer {
	...
	struct saveparam *saveparams;  /* Save points array for RDB */
	... 

	/* RDB persistence */
	...
	long long dirty; /* Changes to DB from the last save */
	time_t lastsave; /* Unix time of last successful save */
	... 
}; 

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持久化的实现

1
2
3
4
5
struct redisServer {
	/* AOF persistence */
	sds aof_buf; /* AOF buffer, written before entering the event loop */
	... 
};

AOF持久化功能的实现可以分为三个步骤:

  • 命令追加
    • 当AOF功能打开时,服务器每执行一个写命令,会以协议格式将被执行的写命令追加到aof_buf缓冲区的末尾
  • 命令写入
    • redis服务器进程是一个事件循环,每结束一个事件循环之前,都会调用flushAppendOnlyFile函数,根据配置appendfsync选项来考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面
    • appendfsync配置有三个选项:always, everysec, no
  • 命令同步

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文件
  • 执行事件循环

14.4