Redis设计与实现2:单机数据库的实现

数据库

多数据库结构

一个Redis实例可以支持多个数据库,当客户端与服务端连接并指定到某个数据库时,两者的结构如下图所示:

客户端指定数据库

redisServerredisClient是两个结构体,前者拥有数据库数组。当客户端使用SELECT命令选择数据库是,会把客户端的db指针指向数据库数组中的一个对象。

数据库键空间

Redis的每个数据库都用redisDb的结构体表示:

redisDb

dict 属性是一个字典,存储所有的键值对,叫做键空间。每个键都是一个字符串对象,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象当中的一种。

一个键空间的例子

键空间操作

在对数据库进行读写操作时,Redis还会执行一些额外的操作来记录一些指标,比如命中率,可以通过INFO命令查看。

键的过期时间

当用EXPIRE/PEXPIRE/EXPIREAT/PEXPIREAT给键设置过期时间时,过期时间会保存在redisDb对象的expires字典中:

保存过期时间

此处dictexpires中值相同的键其实是同一个对象,只是为了简化说明所以划成两份。

TTL命令和PTTL命令会计算当前时间和expires中存储的时间的差值然后输出剩下的生存时间。

过期键的删除策略

Redis删除过期键的策略结合了惰性删除和定期删除两种。

惰性删除

当对键进行读取命令时,Redis会判断该键是否过期,如果过期那么就删除,然后继续执行命令。缺点是,如果一个键一直没有被访问,那么就永远停留在内存里,造成资源浪费,因此Redis还采用了定期删除的策略。

定期删除

当定期删除被触发时,Redis会遍历各个数据库,然后从expires字典中随机检查一部分键的过期时间,如果过期了那么就删除过期的键。

AOF、RDB和复制功能对过期键的处理

生成RDB文件以及AOF重写

当Redis生成RDB文件或者进行AOF重写时,会对键进行检查,过期的键不会写入到文件中。

载入RDB文件

如果服务器以主服务器模式运行,那么过期的键不会被载入内存;如果服务器以从服务器模式运行,那么过期的键会载入内存。

AOF文件写入

当服务器以AOF持久化模式运行时,如果某个键已经过期但还没有被删除,那么不会有任何影响。当过期键被删除后,Redis会向AOF文件追加一条DEL命令,显示记录键已被删除。

复制

当服务器运行在主从复制模式下运行时,当主服务器删除一个键后会向从服务器发送一个DEL命令;当客户端在从服务器读取时即使碰到过期的键也不会删除,只有收到主服务器发来的DEL命令后才会把键删除。

RDB持久化

Redis提供了RDB持久化功能可以将数据存储在硬盘里,我们可以使用SAVE或者BGSAVE命令创建RDB文件,两者的区别是SAVE命令由服务器进程执行,执行时会阻塞进程,在此期间Redis会阻塞来自客户端的请求,直到RDB文件创建完毕;BGSAVE命令由子进程执行,执行完毕后会通知服务器进程,因此不会阻塞客户端的请求。

自动创建RDB文件

处了上述两个命令之外,我们还可以通过配置来让Redis自动创建RDB文件,其本质是自动执行BGSAVE命令。

#自动执行BGSAVE命令的默认配置
save 900 1
save 300 10
save 60 10000

如果你进行了上面的配置,那么只要满足下面三个条件的任意一个,服务器就会执行BGSAVE命令:

  1. 900秒内数据库进行了至少1次修改
  2. 300秒内数据库进行了至少10次修改
  3. 60秒内数据库进行了至少10000次修改

自动保存RDB文件的配置存储在redisServer结构体的saveparams数组中,其数据结构如图所示:

saveparam数据结构

redisServer还有一个dirty属性记录了数据库被修改次数,执行SAVEBGSAVE命令成功后会自动归零。Redis通过一个周期性执行的函数,默认每100毫秒对自动配置以及dirty属性进行检查,如果达到触发条件就执行BGSAVE命令。

RDB文件结构

RDB文件(版本6)结构如下图所示,图中的层次只是为了展示方便,实际并不存在。


RDB文件结构

value的编码

RDB文件中的TYPE属性决定了value的存储格式,下面依次介绍。

字符串对象 [TYPE=REDIS_RDB_TYPE_STRING]

如果字符串对象的编码是REDIS_ENCODING_INT,那么会以下面的结构存储:

存储结构

比如:
存储结构

如果对象的编码是REDIS_ENCODING_RAW,那么有两种情况:

  1. 字符串长度小于等于20字节,其存储结构如下:


    存储结构
  2. 如果字符串长度大于20字节,那么字符串会经过压缩再存储,其结构如下:

    存储结构

    REDIS_RDB_ENC_LZF表示经过LZF算法压缩。

列表对象 [TYPE=REDIS_RDB_TYPE_LIST]

对应的value的编码是REDIS_ENCODING_LINKEDLIST,其存储结构如图:

存储结构

集合对象 [TYPE=REDIS_RDB_TYPE_SET]

对应的value的编码是REDIS_ENCODING_HT,其存储结构类似列表对象:

存储结构

INTSET编码的集合对象 [TYPE=REDIS_RDB_TYPE_SET_INTSET]

value保存的是一个整数集合,Redis会把整数集合转换为一个字符串对象,然后把转换后出的字符串对象写入RDB文件。

哈希表对象 [TYPE=REDIS_RDB_TYPE_HASH]

对应的value的编码是REDIS_ENCODING_HT,其存储结构如图:

存储结构

有序集合对象 [TYPE=REDIS_RDB_TYPE_ZSET]

对应的value的编码是REDIS_ENCODING_SKIPLIST,其存储结构如图:

存储结构

ZIPLIST编码的列表、哈希表或有序集合 [TYPE=REDIS_RDB_TYPE_LIST_ZIPLIST,REDIS_RDB_TYPE_HASH_ZIPLIST,REDIS_RDB_TYPE_ZSET_ZIPLIST]

value保存的是一个压缩列表对象,Redis会把它转换为一个字符串对象,然后把转换后出的字符串对象写入RDB文件。

AOF持久化

RDB持久化功能是把数据库的键值对存储到文件中,AOF持久化功能则是把服务器执行的写命令以Redis的命令请求协议格式存储到文件中。

在了解AOF持久化过程前,我们需要先了解Redis的进程模型,其本质是一个事件循环模型,流程图如下:


事件循环模型

当执行完一个写命令后,服务器会把命令以协议格式追加到AOF缓冲区的末尾,在每一个事件循环结束前会将AOF缓冲区中的内容写到AOF文件。
需要注意的是,写入文件不代表写入磁盘,现代操作系统中为提高效率,通常在写文件时会把数据写入内存缓冲区,等到缓冲区满时或者超过一定时间后才会真正写入磁盘,这一操作称为同步。
虽然每次事件循环都会写入AOF文件,但是并不是每次都会执行同步操作,Redis提供了appendfsync配置项,有以下3个选项:

  1. always 每一次事件循环都进行同步
  2. everysec 每隔一秒进行同步
  3. no 由操作系统决定是否同步

还原数据时,Redis会创建一个没有网络连接的伪客户端,依次执行AOF文件中的命令。执行完毕后数据库就恢复了之前的状态。

AOF重写

如果AOF文件中存储的命令不断增长下去,那么AOF文件会越来越大,载入时间会越来越长。Redis提供了一种重写AOF文件的方法,可以把多条命令合并成一条(或几条)生成一个新的AOF文件,并替换原文件。

RPUSH list "A" "B"
RPUSH list "C"
RPUSH list "D" "E"
LPOP list
LPOP list
RPUSH list "F" "G"

上面的5条语句经过重写后可以合并成下面一行命令:

RPUSH list "C" "D" "E" "F" "G"

Redis通过读取数据库当前键值对的方式进行AOF文件重写,而不是分析原来的AOF文件。
重写过程中会产生大量的写操作,为了不阻塞进程,Redis在子进程中进行重写工作。这样导致的一个问题是,当AOF重写正在进行时,可能会有新的命令被服务器执行,导致新的AOF文件和数据库状态不一致。Redis的解决方法是:除了AOF缓冲区外再增加一个AOF重写缓冲区,这个缓冲区只在开启子进程后使用,服务器把执行的写命令同时追加到这两个缓冲区的末尾,当子进程完成重写后通知服务器进程,服务器把重写缓冲区中的内容全部写入新的AOF文件,然后原子地替换原来的AOF文件。

自动AOF重写

当serverCron函数执行时,它会检查以下条件是否全部满足,如果全部满足的话,就触发自动的AOF重写操作:

  1. 没有RDB持久化或AOF持久化在执行;
  2. 没有AOF重写在进行;
  3. 当前AOF文件大小要大于server.aof_rewrite_min_size(默认为1MB),或者在redis.conf配置了auto-aof-rewrite-min-size大小;
  4. 当前AOF文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比(在配置文件设置了auto-aof-rewrite-percentage参数,不设置默认为100%)

客户端

在网络连接部分,Redis使用了I/O多路复用技术,用单线程接收和响应客户端的请求。每个客户端对应一个redisClient类型的结构体,多个客户端以链表的形式存储在redisServer对象中。可以使用CLIENT list命令查看所有的客户端信息,其中age属性是客户端连接到服务器的秒数。

id=2 addr=127.0.0.1:35400 fd=6 name= age=8 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client

可以使用CLIENT SETNAME name为当前连接的客户端设置一个名字。

id=2 addr=127.0.0.1:35400 fd=6 name=hello age=603 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client

标志

redisClient结构体中有一个重要的属性flags,主要有以下几种标志常量:

  1. REDIS_MASTER 表示客户端代表的是一个主服务器
  2. REDIS_SLAVE 表示客户端代表是的一个从服务器
  3. REDIS_LUA_CLIENT 表示客户端是专门处理Lua脚本的伪客户端
  4. REDIS_BLOCKED 表示客户端正在被BRPOPBLPOP等命令阻塞
  5. REDIS_MULTI 表示客户端正在执行事务
  6. REDIS_FORCE_AOF 强制服务器将当前命令写入AOF文件
  7. REDIS_FORCE_REPL 强制主服务器将当前命令复制给所有从服务器

flags属性可以是多个常量的组合,比如REDIS_LUA_CLIENT|REDIS_FORCE_AOF

服务器

当服务器接收到客户端发来的命令后,会在命令表中查找对应的命令,其数据结构如图所示:

命令表

每一个命令对应一个redisCommand类型的对象,其中:

  • name 表示命令的名称
  • proc 指向具体的实现函数
  • arity 命令的参数个数,命令本身也是参数。如果该值是负数,如-N,则表示参数的数量大于等于N
  • sflags 命令的属性,比如是不是可写命令,是不是只读命令每种属性由一个字符表示,比如wm表示这是一个可写命令(w),并且可能需要占用大量内存,服务器需要进行内存空间的检查(m),SET命令的属性就是wm

服务器查找命令时是忽略大小写的。

命令执行器

在执行命令前,服务器会执行一些检查工作,比如命令参数个数是否正确、客户端是否通过了身份验证、如果服务器开启了maxmemory功能,那么还要检查内存占用情况并按需回收等步骤。
在命令执行后,服务器也会执行一些后续工作,以下列出部分:

  1. 如果开启了慢日志查询功能,服务器会检查是否要为刚才执行的命令添加一条慢查询日志
  2. 如果开启了AOF功能,那么就写入到AOF缓冲区
  3. 如果由其他从服务器正在复制当前这个服务器,那么服务器会把刚才执行的命令传播给所有从服务器

serverCron函数

serverCron函数每100毫秒执行一次,负责管理服务器的资源,主要有以下几个任务:

  1. 更新时间缓存
    为了减少系统调用的次数,在redisServer对象中有两个字段分别缓存了秒级和毫秒级精度的UNIX时间戳,由于精度不高,所以时间缓存仅用于打印日志、更新服务器的LRU时钟、决定是否执行持久化任务、计算服务器uptime这类对时间精度不高的功能上。对于设置过期时间等需要高精度时间的功能还是会通过系统调用。
  2. 更新LRU时钟
    redisServerlruclock属性缓存了服务器的LRU时钟,serverCron函数每10秒更新该属性的值。Redis通过lruclock减去每个redisObject中的lru属性的值就可以得到键的空转时间。
  3. 更新服务器每次执行命令次数
    Redis通过抽样计算的方式把多次抽样结果存储在一个数组中,通过遍历数组计算平均数的方式,得到过去1秒的平均执行命令次数,每100毫秒进行一次抽样。
  4. 更新内存峰值记录
    每次serverCron函数执行时都会对比之前记录的内存峰值,如果比之前的大,就设置为新的内存峰值。
  5. 处理SIGTERM信号
    当Redis收到SIGTERM信号时会把redisServer对象的shutdown_asap属性设置为1,表示关闭服务器,serverCron函数每次都会检查这个属性,如果要关闭服务器,则执行RDB持久化,然后关闭服务器。
  6. 管理客户端资源
    serverCron函数每次执行会对一定数量的客户端进行以下检查:
  7. 如果客户端在很长一段时间里都没有跟服务端互动,那么程序就释放连接
  8. 如果客户端在上一次执行命令请求后输入缓冲区的大小超过了一定的长度,那么程序就释放这块内存并重新创建一个默认大小的输入缓冲区
  9. 管理数据库资源
    检查数据库,删除过期的键,在需要时也会对字典进行收缩操作。
  10. 执行被延迟的BGREWRITEAOF
    在服务器执行BGSAVE命令期间,如果客户端发来BGREWRITEAOF命令,那么服务器会在BGSAVE命令执行完毕后延迟执行BGREWRITEAOF命令。当redisServeraof_rewrite_scheduled值为1时,表示有延迟的BGREWRITEAOF命令需要被执行。
  11. 检查持久化操作的运行状态
    serverCron函数会检查当前是否有持久化任务正在执行,如果没有,那么会走下面的流程:


    检查持久化操作的运行状态
  12. 将AOF缓冲区中的内容写入AOF文件
  13. 关闭输出缓冲区超过大小限制的客户端

发布订阅

发布订阅功能分为两种,一种是精确匹配SUBSCRIBE/UNSUBSCRIBE,一种是模式匹配PSUBSCRIBE/PUNSUBSCRIBE,这两种订阅状态分别存储在redisServer结构体的pubsub_channelspubsub_patterns字段中。

精确匹配
模式匹配

当客户端向服务器发送PUBLISH消息时,服务器会遍历pubsub_channels中某个频道下的客户端发送消息,然后再遍历pubsub_patterns,找出符合条件的客户端发送消息。

在集群模式中,当客户端向某个节点发送PUBLISH消息后,服务器会向整个集群广播这条消息,每一个节点都会执行PUBLISH消息,这样,连接在其它节点上的客户端也能收到这条消息。

事务

Redis的事务主要有三个步骤:

  1. 事务开始
    MULTI命令标志事务的开始,服务器会打开客户端状态的事务标识,表示客户端已进入事务模式。
  2. 命令入队
    当服务器接收到处于事务模式的客户端发来的消息后,除非是EXECDISCARDWATCHMULTI中的一个命令,否则就把命令放入队列,并且向客户端回复QUEUED消息。
  3. 事务执行
    服务器依次执行队列中的消息,清空客户端的事务状态,并把执行命令的全部结果返回给客户端。

Redis的事务具有原子性。事务中的命令要么全部执行,要么一个都不执行(比如监控的键被修改),但是Redis的事务即使全部执行不代表所有的命令都正确执行,比如某条命令参数错误导致执行失败,Redis仍然会执行完接下来的命令,对已经执行的命令不会有任何影响。

作者认为事务中出现错误通常是编程错误,比如少写了一个参数。

WATCH命令

服务器会把当前所有正在被监控的键以及客户端的状态存在redisDb结构体的watched_keys字段中。

watched_keys

当执行对数据库的修改后(如SETLPUSH等命令),会遍历watched_keys字段,如果被修改的键存在字典中,那么就打开该键关联的所有客户端的REDIS_DIRTY_CAS标识,当服务器执行EXEC时,它会先检查客户端的REDIS_DIRTY_CAS标识是否打开,如果已经打开,那么就拒绝执行事务。

参考/图片出处:
1. 机械工业出版社 -《Redis设计与实现》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容