Redis底层数据结构
文章目录
- 1. 数据库
- 2. 过期键删除策略
- 3. 数据库通知
- 4. RDB持久化
- 4.1 RDB文件结构
- 5. AOF持久化
1. 数据库
//数据库结构
struct redisServer {
redisDb *db;//数组,保存服务器中所有数据库
int dbnum; //服务器数据库数量
struct saveparam *saveparams;//记录了保存条件数组
long long dirtry; //修改计数器
time_t lastsave; //上一次执行保存的时间
sds aof_buf; //aof缓冲区
list *clients; //链表,保存了所有客户端的状态。
redisClient *lua_client;//lua脚本关联的伪客户端
time_t unixtime;//保存秒级精度的系统当前unix时间戳
long long mstime;//保存毫秒精度的系统当前unix时间戳
unsigned lruclock;//lru时间缓存
size_t stat_peak_memory;//已使用内存峰值
int shutdown_asap; //关闭服务器标识
int aof_rewrite_scheduled;//值为1表示有BGREWRITEAOF命令被延迟
pid_t rdb_child_pid;//执行bgsave命令子进程的iD
pid_t aof_child_pid;//执行BGREWRITEAOF命令子进程的进程ID
int cronloops; //serverCron函数的运行次数计数器
char *masterhost://主服务器地址
int masterport;//主服务器的端口
dict *pubsub_channels;//保存所有频道的订阅关系
list* pubsub_patterns;//保存所有模式订阅关系
long long slowlog_entry_id;//下一条慢查询日志的ID
list *slowlog;//保存了所有慢查询日志的链表
long long slowlog_log_slower_than;//服务器配置slowlog-log-slower-than选项的值
unsigned long slowlog_max_len;//服务器配置slowlog_max_len的值
redisClient* monitors;//监视器链表
};
每个redis客户端都有自己的目标数据库,每当客户端执行数据库写命令或者数据库读命令时候,目标数据库就会成为这些命令的操作对象。
//客户端状态结构
struct redisClient {
redisDb *db;//记录客户端正在使用的数据库,指针
int slave_listenint_port; //从服务器监听端口号
multiState mstate;//事务状态
};
struct redisDb{
dict *dict;//数据库键空间,保存数据库中所有键值对
dict *expires;//过期字典,保存了数据库所有键的过期时间。键是指针,值是longlong
dict *watched_keys;//正在被watch命令监视的键
};
redisDb结构的dict保存了数据库中所有键值对,这个字典称为键空间。
基于键空间实现的命令:
randomkey
:在键空间中随机返回一个键。dbsize
:返回数据库中键的数量。rename
: 修改键的名称。flushdb
: 删除键空间中所有键值对。
使用redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行额外的维护操作:
- 读取一个键(读操作和写操作都会对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中次数或键空间不命中次数。
info stats
进行查看。 - 读取一个键后,服务器会更新键的LRU的时间。
object idletime
可以查看键的空闲时间。 - 如果服务器在读取一个键时发现这个键过期,那么服务器就会先删除这个过期键,然后才执行余下操作。
- 客户端使用watch监视某个键,如果服务器修改过这个键,会将这个键标记为脏。
- 服务器每次修改一个键之后,都会对脏键计数器的值加1,这个计数器会触发服务器的持久化以及复制操作。
expire、pexpire、expireat三个命令都是基于pexpireat命令实现的。
persist
移除一个键的过期时间。
2. 过期键删除策略
- 定时删除: 在设置键过期的时间的同时,创建一个定时器,让定时器在键过期的时间来临时,立即删除对键的删除操作。
- 惰性删除: 每次从键空间获取键的时,检查键是否过期。过期删除,否则返回该的value。
- 定期删除: 每隔一段时间,程序就对数据库进行一次检查,删除过期键。每次检查多少过期键、检查多少个数据库,算法决定。
定时删除对内存是最友好的,cpu时间不友好。创建一个定时器需要使用redis中的时间事件,当前时间事件的实现方式为无序列表。
惰性事件策略对cpu时间最友好。内存不友好。如果过期键的一直没有被读取,会一直存在于数据库。
定期删除: 通过每隔一段时间删除过期键操作,限制删除频率和时长减少对cpu时间的影响。减少过期键的内存占用。 定期删除策略的难点在于删除操作执行的时长和频率。
redis使用惰性删除和定期删除两种策略。惰性删除所有读写数据库的redis命令在执行之前都会对输入键进行检查。
定时删除通过activeExpireCycle函数实现,每当redis服务器周期操作serverCron函数执行,会被调用。它在规定的时间内,分多次遍历服务器的各个数据库,从数据库的expires字典随机检查一部分键的过期时间,并删除其中的过期键。
在执行save命令或者bgsave命令创建一个新的RDB文件, 程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
服务器对RDB文件进行载入:
- 如果服务器以主服务器模式运行,在载入RDB文件程序会检查键是否过期。
- 从服务器模式运行,文件中保存所有键,不论是否过期,都会载入数据库。
aof如果数据库某个键已经过期,但还没有被惰性删除或者定时删除,aof文件不会产生任何影响。只有在过期键被删除,才会在aof文件后追加del命令。在执行aof重写过程中,会对数据库中的键进行检查,过期键不会被保存到重写的aof文件中。
当服务器运行在复制模式下,从服务器的过期删除动作由主服务器控制。
- 主服务器删除某个过期键的时候,会显式向所有从服务器发送del命令。
- 从服务器在执行客户短发送的读命令,即使碰到过期间也不会进行删除,而是正常处理过期键。
- 从服务器只有接收到主服务器的del命令后,才会删除过期键。
3. 数据库通知
这个功能可以让客户端通过订阅给定频道或者模式,来获取数据库中键的变化,以及数据库中命令的执行情况。需要配置
notify-keyspace-events
设置监听命令。
关注”某个键执行了什么命令“的通知称为键空间通知(keysapce),”键事件通知“(keyevent)关注某个命令被什么键执行了。
4. RDB持久化
Redis是一个键值对数据库,服务器中通常包含着任意个非空数据库,而每个非数据库中又可以包含任意个键值对。我们将数据库中的非空数据库以及它们的键值对统称为数据库状态。
Redis提供了RDB持久化功能,这个功能可以将Redis在内存中的数据库状态保存到磁盘里面,避免数据意外丢失。
RDB持久化即可以手动执行,也可以根据服务器配置选项定期执行。该功能可以将某个时间点上的数据库状态保存到一个RDB文件中。RDB文件是一个经过压缩的二进制文件。
redis命令可以生成RDB文件,一个是SAVE,一个是BGSAVE。SAVE命令会阻塞Redis服务器进程,RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
BGSAVE命令会派生出一个子进程,然后由主进程负责创建RDB文件,服务器进程继续处理命令请求。
因为AOF文件的更新频率通常比RDB文件的更新频率高
,如果服务器开启AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态
。只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。
在BGSAVE命令执行期间,客户端发送的SAVE、BGSAVE命令会被服务器拒绝,服务器禁止SAVE和BGSAVE命令同时执行时为了避免父进程和子进程同时执行两个rdbsave调用,产生竞争条件。
BGREWRITEAOF
和BGSAVE
两个命令不能同时执行。
- 如果
BGSAVE
命令正在执行,那么客户端发送的BGREWRITEAOF
命令会被延迟到BGSAVE命令会被延迟到BGSAVE命令执行完毕之后的执行。 - 如果BGREWRITEAOF命令正在执行,那么客户端发送的BGSAVE命令会被服务器拒绝。
服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。
struct saveparam {
time_t seconds; // 秒数
int changes;//修改数
};
除了saveparams数组之外,服务器状态还维持着一个dirty计数器,以及一个lastsave属性。
- dirty计数器记录距离上一次成功执行save命令或者bgsave命令之后,服务器对数据库状态进行了多少次修改。
- lastsave属性是unix时间戳,记录了服务器上一次成功执行save命令或者bgsave命令的时间。
Redis服务器周期性操作函数ServerCron默认每隔100毫秒就会执行,该函数用于对正在运行的服务器进行维护。满足条件会在这个函数中调用BGSAVE命令。
4.1 RDB文件结构
完整RDB文件所包含的各个部分:
- REDIS: 五个字符。
- db_version: 4字节,记录RDB文件的版本号。
- databases: 包含着零个或多个数据库,以及各个数据库中的键值对数据。
- EOF: 1字节,标志着RDB文件正文内容结束。
- check_sum: 8字节长的无符号整数,保存着一个校验和,通过前几个部分内容计算而来。以此检查RDB文件是否有出错或损坏的情况出现。
每个非空数据库的结构:
- selectDB: 切换数据库的标志。
- db_number: 数据库号码。
- key_value_pairs: 保存了数据库中所有键值对数据,如果键值对带有过期时间,那么过期时间也会和键值对保存在一起。
key_value_pairs结构
- 不带过期时间: TYPE key value
- 带过期时间: EXPIRETIME_MS ms TYPE key value
5. AOF持久化
AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。服务器启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态。
AOF持久化功能的实现可以分为命令追加、文件写入、文件同步。
当AOF持久化处于打开状态,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令将被追加的写命令追加到服务器状态的aof_buff缓冲区末尾。
redis服务器进程是一个事件循环,这个循环中的文件事件负责接受客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行serverCron函数这样需要定时运行的函数。
appendfsync的参数配置以及含义:
- always: 将aof_buf缓冲区中的所有内容写入并同步到AOF文件。即使出现故障停机,aof持久化也只会丢失一个时间循环所产生的命令数据。
- everysec: 将aof_buf缓冲区中的所有内容写入到aof文件,如果上次同步aof文件的时间距离现在超过一秒,那么再次进行同步,并且这个同步操作是由一个线程专门负责执行的。即使出现故障停机也最多会丢失一秒钟的命令数据。
- no: 将aof_buf缓冲区的所有内容写入到aof文件,但并不对aof文件进行同步,何时同步由操作系统来操作。
现在操作系统,当用户调用write函数,将一些数据写入到文件的时候,操作系统通常会将数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满或者超过了指定时限之后,才真正将缓冲区的数据写入到磁盘,
服务器通过读入并执行aof文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。
AOF持久化是保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝,AOF文件中内容会越来越多,使用aof继续进行数据还原的时间就越多。所以AOF提供了文件重写的功能。
AOF重写功能是通过读取服务器当前的数据库状态实现的。在保证进行重写aof一个命令不超过
REDIS_AOF_REWRITE_ITEMS_PER_CMD
的常量值,尽量使用一条命令进行重写。
AOF重写过程中,服务器还是会处理客户端命令请求,为了解决可能出现的数据不一致的问题,redis服务器设置了
aof重写缓冲区
,这个缓冲区在服务器创建子进程之后开始使用,当redis服务器执行完一个写命令。他会将这个写命令发送给aof缓冲区和aof重写缓冲区。