第二部分 单机数据库的实现
第9章 数据库
服务器中的数据库
Redis服务器将所有的数据库保存在服务器状态redis.h/redisServer机构的db数组中
struct redisServer {
// ...
// 服务器数据库数量
int dbnum;
// 一个数组,保存着服务器中所有的数据库
redisDb *db;
// ...
}
dbnum
属性的值有服务器配置选项决定,默认为16
切换数据库
每个Redis客户端都有自己的目标数据库(默认为0号数据库),客户端可以通过SELECT命令来切换数据库。
typedef struct redisClient {
// ...
// 记录客户端当前使用的数据库
redisDb *db;
//...
}
typedef struct redisDb {
// ...
// 数据库键空间,保存着数据库中所有的键值对
dict *dict;
// ...
} redisDb;
- 读取一个键后,服务器根据是否存在更新服务器键空间命中次数货未命中次数
- 读取一个键后,服务器更新键最后一次使用时间,这个值用来计算键的闲置时间,可以使用OBJECT idletime 命令查看key的闲置时间
- 读取一个键时发现已过期,服务器会先删除这个过期键,才执行下面操作
- 如果客户端使用WATCH命令监视了某个键,那么服务器会对被监视键修改后,讲这个键标记为脏,从而让事务程序注意到这个已经被修改过
- 修改键后,都会对脏键计数器的值+1,这个计数会出发服务器持久化以及复制操作
设置键的生存时间或过期时间
127.0.0.1:6379> EXPIRE msg 5 # 设置5s后过期
(integer) 1
127.0.0.1:6379> GET msg # 5s后读取
(nil)
127.0.0.1:6379> SET msg "hello world"
OK
127.0.0.1:6379> EXPIRE msg 50
(integer) 1
127.0.0.1:6379> TTL msg # 查看剩余过期时间
(integer) 47
127.0.0.1:6379> TTL msg
(integer) 44
设置过期时间
- EXPIRE
- PEXPIRE
- EXPIREAT
- PEXPIREAT
保存过期时间
redisDb结构的expires字典保存了数据库中所有键的过期时间,这个字典成为过期字典
移出过期时间
PERSIST命令用于清除键的过期时间
Redis过期键删除策略
如果一个键过期了,那么什么时候会被删除呢?这个问题有三中可能的答案,分别时三中不同的删除策略:
- 定时删除:设置过期时间的同事,创建一个定时器,在键过期时间来来临时立即执行删除操作
- 优点:对内存友好,及时释放内存空间
- 缺点:对cpu不友好,可能占用大量cpu时间
- 惰性删除:每次读取时,看一下键是否过期,过期则删除
- 优点:对cpu友好
- 缺点:内存不友好,不能及时释放内存
- 定期删除:每个一段时间,检查数据库,删除里面的过期键。定期删除策略是前两种策略的一种整合和折中,定期策略的难点是确定删除操作执行的时长和频率。
Redis服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种策略,服务器可以很好的合理使用cpu时间和避免浪费内存空间之间取得平衡。
AOF、RDB和复制功能对过期键的处理
在执行SAVE
命令或者BGSAVE
命令创建一个新的RDB文件时,一斤过期的键不会被保存在RDB文件中。
载入RDB文件时:
- 如果服务器以主服务器模式运行,载入时,过期键会被忽略
- 如果服务器以从服务器模式运行,载入时,不论过不过期,都会被载入。因为主服务器在进行数据同步的时候,从服务器的数据库会被清空。
当服务器以AOF持久化模式运行时,如果数据库中某个键已经过期,但他没有被删除,那么AOF文件不会有任何影响。当过期键被删除后,程序会向AOF文件追加一条DEL命令,来显示的记录该键已经被删除。
在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的不会被保存到AOF文件中。
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:
- 主服务器在删除一个过期键后,会显示的向所有的从服务器发送一个
DEL
命令,告诉从服务器删除这个过期键 - 从服务器在执行客户端读命令时,即使碰到了过期键也不会将过期键删除,而是继续像处理未过期键一样来处理过期键
- 从服务器值有在街道主服务器发送的
DEL
命令后,才删除过期键
数据库通知
数据库通知可以让客户端通过订阅给定的频道或者模式,来获取数据库中键的变化,以及数据库中命令的执行情况。
第10章 RDB持久化
RDB持久化功能可以将Redis在内存中的数据库状态保存在磁盘里面,避免意外丢失。
RDB持久化的触发方式:
- 手动执行
- 更具服务器配置选项定期执行
RDB文件的创建与载入
可以通过以下两个命令生成RDB文件
SAVE
会阻塞Redis服务器进程,直到RDB文件创建完成为止,这期间服务器不处理任何命令请求BGSAVE
派生一个子进程,子进程负责创建RDB文件,这期间服务器继续处理命令请求- 在这期间
SAVE
命令会被拒绝 - 在这期间
BGSAVE
命令会被拒绝 - 在这期间
BGREWRITEAOF
命令会被延迟到BGSAVE
命令执行完之后执行 - 如果
BGREWRITEAOF
命令正在执行,那么BGSAVE
会被拒绝
- 在这期间
RDB文件的载入工作是在服务器启动时自动执行的,只要在启动时检测到RDB文件存在,它就会自动载入RDB文件。
因为AOF文件更新频率通常高于RDB文件,所以:
- 如果开启了AOF持久化,服务器首先使用AOF文件还原数据库
- AOF不存在时,才会使用RDB文件还原数据库
自动间隔性保存
Redis允许用户通过设置服务器设置的save
选项,让服务器每隔一段时间自动执行一次BGSAVE
命令。
例子:
save 900 1
save 300 10
save 60 10000
只要满足下面三个条件中的任意一个,BGSAVE
命令就会被执行:
- 服务器在900s内,对数据库进行了至少1次修改
- 服务器在300s内,对数据库至少执行了10次修改
- 服务器在60s内,对数据库至少执行了10000次修改
dirty计数器和lastsave属性
dirty
计数器记录距离上一次执行SAVE
命令或者BGSAVE
命令之后,服务器对数据库状态进行了多少次更新。
lastsave
属性记录了服务器上次执行SAVE
命令或者BGSAVE
命令的时间。
检查保存条件是否满足
Redis服务器会周期性的操作函数serverCron
默认每个100ms就会执行一次,期中的一项工作就是检查是否满足设置的保存条件,如果满足的话,就执行BGSAVE
RDB文件结构
-
REDIS_RDB_TYPE_STRING
-
REDIS_RDB_TYPE_LIST
-
REDIS_RDB_TYPE_SET
-
REDIS_RDB_TYPE_ZSET
-
REDIS_RDB_TYPE_HASH
-
REDIS_RDB_TYPE_LIST_ZIPLIST
-
REDIS_RDB_TYPE_SET_INTSET
-
REDIS_RDB_TYPE_ZSET_ZIPLIST
-
REDIS_RDB_TYPE_HASH_ZIPLIST
-
key总是一个字符
-
value保存着对应的值,不同类型对应不同的结构
-
字符串对象,TYPE ==
REDIS_RDB_TYPE_STRING
字符串对象的编码可以是REDIS_ENCODING_INT
或者REDIS_ENCODING_RAW
- 如果是
REDIS_ENCODING_INT
,结构如下图,其中ENCODING的值可以是REDIS_RDB_ENC_INT8
、REDIS_RDB_ENC_INT16
、REDIS_RDB_ENC_INT32
中的一种
- 如果是
REDIS_ENCODING_RAW
,会根据字符串的长度,有压缩和不压缩两种方式保存- 长度小于等于20,字符串直接被保存
- 长度大于20,字符串会被压缩保存
- 长度小于等于20,字符串直接被保存
- 如果是
-
列表对象,TYPE ==
REDIS_RDB_TYPE_LIST
value保存的是一个REDIS_ENCODING_LINKEDLIST
编码的列表对象
-
集合对象,TYPE ==
REDIS_RDB_TYPE_SET
value保存的是一个REDIS_ENCODING_HT
编码的集合对象
-
哈希表对象,TYPE ==
REDIS_RDB_TYPE_HASH
value保存的是一个REDIS_ENCODING_HT
编码的集合对象
-
有序集合对象,TYPE ==
REDIS_RDB_ZSET
value保存的是一个REDIS_ENCODING_SKIPLIST
编码的有序集合对象
-
INTSET编码的集合,TYPE ==
REDIS_RDB_TYPE_SET_INTSET
value保存的是一个整数集合对象 -
ZIPLIST编码的列表、哈希表、有序集合
如果TYPE的值为REDIS_RDB_TYPE_LIST_ZIPLIST
、REDIS_RDB_TYPE_ZSET_ZIPLIST
、REDIS_RDB_TYPE_HASH_ZIPLIST
,那么value保存的就是一个压缩列表对象
-
第11章 AOF持久化
AOF持久化时通过保存Redis服务器所执行的写命令来记录数据库状态。
AOF持久化的实现
AOF持久化功能的实现可以分为
-
命令追加:服务器执行完一个命令之后,会将被执行的写命令追加到缓冲区
-
文件写入:服务器进程就是一个事件循环,其中文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件负责像
serverCron
函数这样需要定时任务执行的函数,服务器每次结束一个事件循环之前,就会调用flushAppendOnlyFile
函数,考虑是否将缓冲区中的内容写入和保存到AOF文件中 -
文件同步:为了提高文件的写入效率,用户调用write函数后,并不会将数据真正写入磁盘,需要显示的调用同步函数,或者是操作系统执行同步操作
三个步骤
appendfsync
选项
appendfsync
选项决定了flushAppendOnlyFile
函数的行为
appendfsync 选项的值 | flushAppendOnlyFile 函数的行为 |
---|---|
always | 将缓冲区中的内容写入并同步到AOF文件 |
everysec | 将缓冲区内容写入AOF文件,如果上次同步AOF文件事件超过1s,那么对AOF文件进行同步(有一个线程专门负责执行) |
no | 将缓冲区内容写入AOF文件,不对AOF文件执行同步,同步过程由操作系统决定 |
AOF文件的载入与数据还原
载入过程步骤如下:
- 创建一个网络连接的伪终端,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果一样
- 从AOF文件中解析并读取一条写命令
- 使用伪客户端执行被读出的写命令
- 重复执行2和3步骤,直到AOF文件中所有的写命令都被处理完毕为止
AOF重写
随着服务器的运行,命令会不断增长,AOF体积会越来越大,为了解决AOF文件膨胀问题,Redis提供了AOF文件重写功能。
AOF文件重写的实现
Redis通过生成新AOF文件替换旧AOF文件,AOF的重写不需要对现有的AOF文件进行任何读取、分析、写入操作,这个功能是通过服务器当前数据库状态来实现的。
例如:
127.0.0.1:6379> RPUSH list "A" "B"
(integer) 2
127.0.0.1:6379> RPUSH list "C"
(integer) 3
127.0.0.1:6379> RPUSH list "D" "E"
(integer) 5
127.0.0.1:6379> LPOP list
"A"
127.0.0.1:6379> RPUSH list "F" "G"
(integer) 6
服务器需要在AOF文件中写入6条命令,如果想尽量少的命令记录list键的状态,那么只需要一条
127.0.0.1:6379> RPUSH list "C" "D" "E" "F" "G"
命令来替换保存在AOF文件中的6条命令,所有类型的键都可以用相同的方法去减少AOF文件中命令的数量。
Redis不希望AOF重写造成服务器服务处理请求,所以AOF重写是在子进程中执行,这样的好处:
- 重写期间,服务器不会阻塞,可以继续处理命令请求
不过,重写期间,服务器会继续执行命令,造成服务器当前状态和重写后的AOF文件所保存的数据库状态不一致。为了解决这个问题,服务器设置了一个AOF重写缓冲区,这个缓冲区记录了服务器创建子进程后开始使用,当Redis服务器执行完一个写命令后,同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区
当AOF后台重写完成后,子进程会向父进程发送一个信号,父进程接收到信号后,会完成以下工作:
- 将AOF重写缓冲区的内容写入到新AOF文件中
- 对新AOF文件进行改名,原子的覆盖现有的AOF文件
整个AOF后台重写过程中,只有信号处理函数会造成阻塞。
扩展阅读
http://oldblog.antirez.com/post/redis-persistence-demystified.html
第12章 事件
Redis服务器是一个事件驱动程序,两类事件:
- 文件事件:服务器对套接字操作的抽象,服务器和客户端的通信会产生相应的文件事件
- 时间事件:Redis服务器中一些操作需要在给定的时间点执行,时间事件就是对这类定时操作的抽象
文件事件
Redis基于Reactor模式开发了自己的网络事件处理器:
- 文件事件处理器使用I/O多路复用监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器
- 当被监听套接字准备好执行连接应答(accept)、读取(read),写入(write)、关闭(close)等操作时,文件事件就会产生
虽然文件事件处理器是单线程方式运行,但通过I/O多路复用实现了高性能的网络通信模型,并且保持了Redis内部单线程设计的简单性。
文件处理器的构成
- 套接字
- I/O多路复用程序
- 文件事件分派器
- 事件处理器
I/O多路复用程序总是将所有产生的事件的套接字都放到一个队列里面,然后通过这个队列,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字,当一个套接字产生的事件被处理完毕后,I/O多路复用程序才会继续向文件事件分派器传送下一个套接字,如下图:
Redis的多路复用程序通过包装常见的select、epoll、evport、kqueue这些I/O多路复用函数库实现,Redis为每个I/O多路复用函数库都实现了相同的API,所以底层是可以互换的
时间事件
Redis时间事件分为两类:
- 定时事件:让给你一段程序在指定的时间之后执行一次
- 周期性事件:让程序每隔指定时间就执行一次
服务器将所有的时间事件通过一个无序链表实现,每当时间事件执行器运行时,他就会遍历这个那个链表,查找已经达的时间事件,并调用相应的事件处理器。
第13章 客户端
略
第14章 服务器
略