Redis(五). 运行机制
1.数据库
1.1 数据库结构
每一个数据库的结构 如下
/* Redis database representation. There are multiple databases identified
* by integers from 0 (the default database) up to the max configured
* database. The database number is the 'id' field in the structure. */
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */ 保存着数据库中的所有键值对数据 键空间(key space)
dict *expires; /* Timeout of keys with a timeout set */ 保存着键的过期信息
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/ 实现列表阻塞原语,如 BLPOP
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */ 用于实现 WATCH 命令
int id; /* Database ID */ 保存着数据库以整数表示的号码
long long avg_ttl; /* Average TTL, just for stats */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
clusterSlotToKeyMapping *slots_to_keys; /* Array of slots to keys. Only used in cluster mode (db 0). */
} redisDb;
1.2 切换数据库 id
id 域来了解自己正在使用的是哪个数据库,不用指针来一个个判断
1.3 数据库键空间
因为 Redis 是一个键值对数据库(key-value pairs database),所以它的数据库本身也是一个字典(俗称 key space)
1.4 键的过期时间
127.0.0.1:6379> setex mykey 10 1111 //设置过期时间
OK
127.0.0.1:6379> ttl mykey
(integer) 5
127.0.0.1:6379> get mykey
(nil)
1.5 过期时间的保存
redisDb 结构的 expires 字典里
typedef struct redisDb {
// ...
dict *expires; //用字典保存 value 存储的是到期时间
// ...
} redisDb;
1.6 设置过期时间
Redis 有四个命令可以设置键的生存时间(可以存活多久)和过期时间(什么时候到期): ? EXPIRE 以秒为单位设置键的生存时间; ? PEXPIRE 以毫秒为单位设置键的生存时间; ? EXPIREAT 以秒为单位,设置键的过期 UNIX 时间戳; ? PEXPIREAT 以毫秒为单位,设置键的过期 UNIX 时间戳。
虽然有那么多种不同单位和不同形式的设置方式,但是 expires 字典的值只保存“以毫秒为单位的过期 UNIX 时间戳
1.7 过期键的判断
通过 expires 字典,可以用以下步骤检查某个键是否过期:
- 检查键是否存在于 expires 字典:如果存在,那么取出键的过期时间;
- 检查当前 UNIX 时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则,键未过期。
1.8 过期键的删除
1.定时删除,定时事件,到达过期时间时,事件执行删除;内存友好,CPU不友好,效率较低 2.惰性删除:每次取出时,判断时间,若过期,则删除;内存不友好,过期键不被访问长期占用内存 3.定期删除:定时任务一样对expires 字典检测,删除过期;折中策略,每隔一段时间执行删除任务,时间可控,减少CPU时间占用
Redis 使用惰性删除 + 定期删除 相互配合
1.9 过期键的惰性删除策略
写请求 ->判断是不是过期,过期删除 —> 在执行命令
读请求-> 判断是不是过期,过期删除 --> 过期是nil /返回value
1.10 过期键的定期删除策略
定期删除由 redis.c/activeExpireCycle 函数执行,函数在规定的时间限制内,尽可能地遍历各个数据库的 expires 字典,随机地检查一部分键的过期时间,并删除其中的过期键;
1.11 过期键对 AOF 、RDB 和复制的影响
RDB文件 :
在创建新的 RDB 文件时,程序会对键进行检查,过期的键不会被写入到更新后的 RDB 文件中。因此,过期键对更新后的 RDB 文件没有影响。
AOF文件 :
先不做处理。当过期键被惰性删除、或者定期删除之后,程序会向 AOF 文件追加一条 DEL 命令,来显式地记录该键已被删除
AOF重写 :
和 RDB 文件类似,当进行 AOF 重写时,程序会对键进行检查,过期的键不会被保存到重写后的 AOF 文件。因此,过期键对重写后的 AOF 文件没有影响。
集群复制:
过期键的删除由主节点统一控制
- 如果服务器是主节点,那么它在删除一个过期键之后,会显式地向所有附属节点发送一个 DEL 命令
- 服务器是附属节点,请求主节点删除,当接到从主节点发来的 DEL 命令之后,附属节点才会真正的将过期键删除掉
1.12 数据库收缩扩容
规则个前面说的数据结构字典完全一样
2.RDB
Redis 以数据结构的形式将数据维持在内存中,为了让这些数据在 Redis 重启之后仍然可用,Redis 分别提供了 RDB 和 AOF 两种持久化模式
在 Redis 运行时,RDB 程序将当前内存中的数据库快照保存到磁盘文件中,在 Redis 重启动时,RDB 程序可以通过载入 RDB 文件来还原数据库的状态
2.1 保存
SAVE 和 BGSAVE 两个命令都会调用 rdbSave 函数,但它们调用的方式各有不同:
? SAVE 直接调用 rdbSave ,阻塞 Redis 主进程,直到保存完成为止。在主进程阻塞期间,服务器不能处理客户端的任何请求。SAVE 执行时新的SAVE 、BGSAVE 或 BGREWRITEAOF 调用都不会产生任何作用,服务器会检查 BGSAVE 是否正在执行当中,如果是的话,服务器就不调用 rdbSave ,而是向客户端返回一个出错信息,告知在 BGSAVE 执行期间,不能执行SAVE 。
? BGSAVE 则 fork 出一个子进程,子进程负责调用 rdbSave ,并在保存完成之后向主进程发送信号,通知保存已完成。因为 rdbSave 在子进程被调用,所以 Redis 服务器在BGSAVE 执行期间仍然可以继续处理客户端的请求。当 BGSAVE 正在执行时,调用新 BGSAVE 命令的客户端会收到一个出错信息,告知 BGSAVE 已经在执行当中。
2.2载入
Redis 服务器启动时,rdbLoad 函数就会被执行,它读取 RDB 文件,并将文件中的数据库数据载入到内存中;
等到载入完成之后,服务器才会开始正常处理所有命令。只有 PUBLISH 、SUBSCRIBE 、PSUBSCRIBE 、UNSUBSCRIBE 、PUNSUBSCRIBE 五个命令的请求会被正确地处理,其他的错误
注:发布与订阅功能和其他数据库功能是完全隔离的,前者不写入也不读取数据库,所以在服务器载入期间,订阅与发布功能仍然可以正常使用,而不必担心对载入数据的完整性产生影响
AOF 文件的保存频率通常要高于 RDB 文件保存的频率,那么程序优先使用 AOF 文件来还原数据。只有在 AOF 功能未打开的情况下,Redis 才会使用 RDB 文件来还原数据;
2.3 RDB 文件结构
内容 | 长度或大小 | 含义 |
---|
REDIS | 5个字符 | 标识着一个 RDB 文件的开始 | RDB-VERSION | 文件版本 | RDB 版本号 | DB-DATA | 保存的数据 | | EOF | | 标志着数据库内容的结尾(不是文件的结尾) ,值为 255 | CHECK-SUM | uint_64t 类型值 | RDB 文件所有内容的校验和 | | | |
3.AOF
AOF 则以协议文本的方式,将所有对数据库进行过写入的命令(及其参数)记录到 AOF文件,以此达到记录数据库状态的目的;(逻辑备份类似)
3.1 AOF 命令同步
在客户端执行命令
127.0.0.1:6379[2]> lpush mylist 11 22 33
(integer) 3
127.0.0.1:6379[2]> keys *
1) "mylist"
127.0.0.1:6379[2]> lpop mylist
"33"
127.0.0.1:6379[2]> lpush mylist 33333
(integer) 3
127.0.0.1:6379[2]>
对数据库有修改的写入命令就会被同步到 AOF 文件中:
lpush mylist 11 22 33
lpop mylist
lpush mylist 33333
的四个命令在 AOF 文件中就实际保存如下:
*6 //本条指令/参数 6个 字符串 组成
$5 //一条指令5 个字符
RPUSH //第1个
$6 //一条指令6 个字符
mylist //第2个
$1 // 一条指令1个字符
1 //第3个
$1 // 一条指令1个字符
2 //第4个
$1 // 一条指令1个字符
3 //第5个
$1
4 //第6个
*2//本条指令 2个 字符串 组成
$4 //一条指令4 个字符
LPOP //第1个
$6 //一条指令6 个字符
mylist //第2个
*3 //本条指令 3个 字符串 组成
$4 //一条指令4 个字符
LPUSH
$4 //一条指令6 个字符
mylist
$5
33333
AOF 文件写入三个步骤
- 命令传播:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。
- 缓存追加:AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的 AOF 缓存中。
- 文件写入和保存:AOF 缓存中的内容被写入到 AOF 文件末尾,如果设定的 AOF 保存条件被满足的话,fsync 函数或者 fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。
3.2 命令传播
当一个 Redis 客户端需要执行命令时,它通过网络连接,将协议文本发送给 Redis 服务器
SET KEY VALUE
"*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n"
3.3 缓存追加
当命令被传播到 AOF 程序之后,程序会根据命令以及命令的参数,将命令从字符串对象转换回原来的协议文本;它会被追加到 redis.h/redisServer 结构的 aof_buf 末尾
3.4 文件写入和保存
执行以下两个工作:
3.5 AOF 保存模式
- AOF_FSYNC_NO :不保存。
- AOF_FSYNC_EVERYSEC :每一秒钟保存一次。
- AOF_FSYNC_ALWAYS :每执行一个命令保存一次。
3.6 AOF 文件的读取和数据还原
Redis 读取 AOF 文件并还原数据库的详细步骤如下:
- 创建一个不带网络连接的伪客户端(fake client)。
- 读取 AOF 所保存的文本,并根据内容还原出命令、命令的参数以及命令的个数。
- 根据命令、命令的参数和命令的个数,使用伪客户端执行该命令。
- 执行 2 和 3 ,直到 AOF 文件中的所有命令执行完毕。
3.7 AOF 重写
AOF 文件通过同步 Redis 服务器所执行的命令,从而实现了数据库状态的记录,但是,这种同步方式会造成一个问题:随着运行时间的流逝,AOF 文件会变得越来越大;
RPUSH list 1 2 3 4 // [1, 2, 3, 4]
RPOP list // [1, 2, 3]
LPOP list // [2, 3]
LPUSH list 1 // [1, 2, 3]
被频繁操作的键,对它们所调用的命令可能有成百上千、甚至上万条;AOF 文件的体积就会急速膨胀;
如果我们要保存这个列表的当前状态,并且尽量减少所使用的命令数,那么最简单的方式不是去 AOF 文件上分析前面执行的四条命令,而是直接读取 list 键在数据库的当前值,然后用一条 RPUSH 1 2 3 命令来代替前面的四条命令;
3.8 AOF 后台重写
Redis 不希望 AOF 重写造成服务器无法处理请求,所以Redis 决定将 AOF 重写程序放到(后台)子进程里执行,这样处理的最大好处是
- 子进程进行 AOF 重写期间,主进程可以继续处理命令请求
- 子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性
问题:新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的AOF 文件中的数据不一致
解决:Redis 主进程在接到新的写命令之后,除了会将这个写命令的协议内容追加到现有的 AOF文件之外,还会追加到这个缓存中;
3.9 AOF重写触发条件
查以下条件是否全部满足,如果是的话,就会触发自动的 AOF 重写:
- 没有 BGSAVE 命令在进行。
- 没有 BGREWRITEAOF 在进行。
- 当前 AOF 文件大小大于 server.aof_rewrite_min_size (默认值为 1 MB)。
- 当前 AOF 文件大小和最后一次 AOF 重写后的大小之间的比率大于等于指定的增长百分比。(默认增长百分比为 100%)
4. 事件
处理文件事件:在多个客户端中实现多路复用,接受它们发来的命令请求,并将命令的执行结果返回给客户端。
时间事件:实现服务器常规操作(server cron job)
4.1 文件事件
多个客户端通过套接字连接到 Redis 服务器中,但只有在套接字可以无阻塞地进行读或者写时,服务器才会和这些客户端进行交互。
Redis 将这类因为对套接字进行多路复用而产生的事件称为文件事件(file event),文件事件可以分为读事件和写事件两类
读事件
读事件标志着客户端命令请求的发送状态。
客户端 X 向服务器发送命令请求,并且命令请求已到达时,客户端 X 的读事件状态变为就绪
客户端 X | 就绪 | 已发送,并且已到达 |
---|
客户端 | 读事件状态 | 命令发送状态 | 客户端 Y | 等待 | 未发送 | 客户端 Z | 等待 | 未发送 |
写事件
写事件标志着客户端对命令结果的接收状态
一个写事件会在两种状态之间切换:
示例:服务器正等待客户端 X 变得可写,从而将命令的执行结果返回给它
客户端 | 读事件状态 | 写事件状态 |
---|
客户端X | 等待 | 已就绪 | 客户端 Y | 等待 | 无 | 客户端 Z | 等待 | 无 |
当命令执行结果被传送回客户端之后,客户端和写事件之间的关联会被解除(只剩下读事件)
4.2 时间事件
时间事件记录着那些要在指定时间点运行的事件,多个时间事件以无序链表的形式保存在服务器状态中
- when :以毫秒格式的 UNIX 时间戳为单位,记录了应该在什么时间点执行事件处理函数
- timeProc :事件处理函数 (根据返回值判读是否是一次事件,处理一次的事件执行完成之后就会被删除不在执行,否则的话更新when 下次继续执行)
- next : 下一个时间事件
4.3 时间事件应用实例:服务器常规操作
redis.c/serverCron 执行内容;每隔 10 毫秒就会被运行一次;而具体的间隔可以由用户自己调整
- 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等
- 清理数据库中的过期键值对。
- 对不合理的数据库进行大小调整。
- 关闭和清理连接失效的客户端。
- 尝试进行 AOF 或 RDB 持久化操作。
- 如果服务器是主节点的话,对附属节点进行定期同步。
- 如果处于集群模式的话,对集群进行定期同步和连接测试;
4.4 事件的执行与调度
Redis 里面的两种事件呈合作关系,它们之间包含以下三种属性:
- 一种事件会等待另一种事件执行完毕之后,才开始执行,事件之间不会出现抢占
- 事件处理器先处理文件事件(处理命令请求),再执行时间事件(调用 serverCron)
- 文件事件的等待时间(类 poll 函数的最大阻塞时间),由距离到达时间最短的时间事件决定;
文件事件的优先级高(客户端的连接一般),事件是串行的,所以会等待文件事件结束才会执行,所以调度的时候会等待文件事件执行完成之后在执行,所以有时候比约定的时间要晚一点;
5.服务器和客户端
启动 Redis 服务器,到服务器可以接受外来客户端的网络连接这段时间,Redis 需要执行一系列初始化操作;
- 初始化服务器全局状态。
- 载入配置文件。
- 创建 daemon 进程。
- 初始化服务器功能模块。
- 载入数据。
- 开始事件循环。
5.1 初始化服务器全局状态
服务器中的所有数据库;命令表;事件状态;服务器的网络连接信息:套接字地址、端口,以及套接字描述符;所有已连接客户端的信息;Lua 脚本的运行环境;实现订阅与发布(pub/sub)功能所需的数据结构;日志(log)和慢查询日志(slowlog);数据持久化(AOF 和 RDB)的配置和状态;服务器配置选项;统计信息;程序创建一个 redisServer 结构的实例变量 server 用作服务器的全局状态,并将server 的各个属性初始化为默认值。
5.2载入配置文件
程序为 server 变量(也即是服务器状态)的各个属性设置了默认值,但这些默认值有时候并不是最合适的;
- 用户可能想使用 AOF 持久化,而不是默认的 RDB 持久化。
- 用户可能想用其他端口来运行 Redis ,以避免端口冲突。
- 用户可能不想使用默认的 16 个数据库,而是分配更多或更少数量的数据库。
- 用户可能想对默认的内存限制措施和回收策略做调整
5.3创建 daemon 进程
Redis 默认以 daemon 进程的方式运行;当服务器初始化进行到这一步时,程序将创建 daemon 进程来运行 Redis ,并创建相应的 pid文件
5.4初始化服务器功能模块
为 server 变量的数据结构子属性分配内存 ;初始化这些数据结构
初始化 Redis 进程的信号功能;初始化日志功能;初始化客户端功能;初始化共享对象;初始化数据库;初始化网络连接;初始化各个统计变量;完成这一步之后,服务器打印出 Redis 的 ASCII LOGO 、服务器版本等信息,表示所有功能模块已经就绪,可以等待被使用了
5.5载入数据
程序需要将持久化在 RDB 或者 AOF 文件里的数据,载入到服务器进程里面;服务器打印出一段载入完成信息:
[6717] 22 Feb 11:59:14.830 * DB loaded from disk: 0.068 seconds
5.6开始事件循环
[6717] 22 Feb 11:59:14.830 * The server is now ready to accept connections on port 6379
以下是初始化完成之后,服务器状态和各个模块之间的关系图
|