Redis设计与实现
Redis设计与实现
第一部分 数据结构与对象
第2章 简单动态字符串
每个sds.h/sdshdr结构表示一个SDS值:
1 |
|
SDS与C字符串的区别
C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符’\0’。
第3章 链表
Node
每个链表节点使用一个adlist.h/listNode结构来表示:
1 |
|
List
1 |
|
第4章 字典
哈希表dictht
1 |
|
字典dict
1 |
|
rehash
扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来 完成,Redis对字典的哈希表执行rehash的步骤如下:
1)为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于 要执行的操作,以及ht[0]当前包含的键值对数量(也即是ht[0].used属性 的值):
- ·如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于 ht[0].used*2的2 n (2的n次方幂);
- ·如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于 ht[0].used的2 n 。
2)将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是 重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定 位置上。
3)当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空 表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希 表,为下一次rehash做准备。
渐进式rehash - 重点
以下是哈希表渐进式rehash的详细步骤:
1)为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
2)在字典中维持一个索引计数器变量rehashidx,并将它的值设置 为0,表示rehash工作正式开始。
3)在rehash进行期间,每次对字典执行添加、删除、查找或者更新 操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在 rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程 序将rehashidx属性的值增一。
4)随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有 键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表 示rehash操作已完成。
特别
渐进式rehash进行期间,字典的删除(delete)、查找 (find)、更新(update)等操作会在两个哈希表上进行。例如,要在 字典里面查找一个键的话,程序会先在ht[0]里面进行查找,如果没找到 的话,就会继续到ht[1]里面进行查找,诸如此类。
另外,在渐进式rehash执行期间,新添加到字典的键值对一律会被 保存到ht[1]里面,而ht[0]则不再进行任何添加操作,这一措施保证了 ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变 成空表。
第5章 跳跃表
1 |
|
zskipList
最左边的是zskiplist结构, 该结构包含以下属性:
·header:指向跳跃表的表头节点。 ·tail:指向跳跃表的表尾节点。 ·level:记录目前跳跃表内,层数最大的那个节点的层数(表头节
点的层数不计算在内)。
·length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数 量(表头节点不计算在内)。
zSkipNode
四个zskiplistNode结构,该结构包含以下 属性:
·层(level):节点中用L1、L2、L3等字样标记节点的各个层,L1 代表第一层,L2代表第二层,以此类推。每个层都带有两个属性:前进 指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记 录了前进指针所指向节点和当前节点的距离。在上面的图片中,连线上 带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头 向表尾进行遍历时,访问会沿着层的前进指针进行。
·后退(backward)指针:节点中用BW字样标记节点的后退指 针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头 遍历时使用。
·分值(score):各个节点中的1.0、2.0和3.0是节点所保存的分 值。在跳跃表中,节点按各自所保存的分值从小到大排列。
·成员对象(obj):各个节点中的o1、o2和o3是节点所保存的成员 对象。
总结
- ·Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中 zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而 zskiplistNode则用于表示跳跃表节点。
- ·每个跳跃表节点的层高都是1至32之间的随机数。
- ·在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。
- ·跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
第6章 整数集合
每个intset.h/intset结构表示一个整数集合:
1 |
|
升级
每当我们要将一个新元素添加到整数集合里面,并且新元素的类型 比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级 (upgrade),然后才能将新元素添加到整数集合里面。
升级整数集合并添加新元素共分为三步进行:
- 1)根据新元素的类型,扩展整数集合底层数组的空间大小,并为 新元素分配空间。
- 2)将底层数组现有的所有元素都转换成与新元素相同的类型,并 将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需 要继续维持底层数组的有序性质不变。
- 3)将新元素添加到底层数组里面。
降级 - 不支持
整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。
第7章 压缩列表
—结构
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的 连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可以包 含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整 数值。
—总结
1 |
|
第8章 对象
Redis中的每个对象都由一个redisObject结构表示,该结构中和保存 数据有关的三个属性分别是type属性、encoding属性和ptr属性:
1 |
|
字符串对象的编码可以是int、raw或者embstr。
列表对象的编码可以是ziplist或者linkedlist。
哈希对象的编码可以是ziplist或者hashtable。
有序集合的编码可以是ziplist或者skiplist。
第二部分 单机数据库的实现
第9章 数据库
过期键删除策略
这个问题有三种可能的答案,它们分别代表了三种不同的删除策略:
- ·定时删除:在设置键的过期时间的同时,创建一个定时器 (timer),让定时器在键的过期时间来临时,立即执行对键的删除操 作。
- ·惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期就返回该键。
- ·定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
AOF、RDB和复制功能对过期键的处理
生成RDB:已过期的键不会被保存到新创建的RDB 文件中。
载入RDB:
- ·如果服务器以主服务器模式运行,那么在载入RDB文件时,程序 会对文件中保存的键进行检查,未过期的键会被载入到数据库中,而过 期键则会被忽略,所以过期键对载入RDB文件的主服务器不会造成影 响。
- ·如果服务器以从服务器模式运行,那么在载入RDB文件时,文件 中保存的所有键,不论是否过期,都会被载入到数据库中。不过,因为 主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所 以一般来讲,过期键对载入RDB文件的从服务器也不会造成影响。
写入AOF:
- 当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过 期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个 过期键而产生任何影响。
- 当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加 (append)一条DEL命令,来显式地记录该键已被删除。
AOF重写:程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。
复制:当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:
- ·主服务器在删除一个过期键之后,会显式地向所有从服务器发送 一个DEL命令,告知从服务器删除这个过期键。
- ·从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
- ·从服务器只有在接到主服务器发来的DEL命令之后,才会删除过 期键。
第10章 RDB持久化
创建
- SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求:
- BGSAVE命令会派 生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进 程)继续处理命令请求:
载入
服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作 完成为止。
文件
- ·RDB文件是一个经过压缩的二进制文件,由多个部分组成。
- ·对于不同类型的键值对,RDB文件会使用不同的方式来保存它 们。
第11章 AOF持久化
写入
Redis的服务器进程就是一个事件循环(loop),这个循环中的文件 事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间 事件则负责执行像serverCron函数这样需要定时运行的函数。
载入
Redis读取AOF文件并还原数据库状态的详细步骤如下:
- 1)创建一个不带网络连接的伪客户端(fake client):因为Redis的 命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接 来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接 的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令的效果和 带网络连接的客户端执行命令的效果完全一样。
- 2)从AOF文件中分析并读取出一条写命令。
- 3)使用伪客户端执行被读出的写命令。
- 4)一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处 理完毕为止。
当完成以上步骤之后,AOF文件所保存的数据库状态就会被完整地 还原出来,整个过程如图11-2所示。
后台重写
1 |
|
子进程执行AOF重写期间,服务器进程需要执行以 下三个工作:
1)执行客户端发来的命令。
2)将执行后的写命令追加到AOF缓冲区。
3)将执行后的写命令追加到AOF重写缓冲区。
1 |
|
当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父 进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:
1)将AOF重写缓冲区中的所有内容写入到新AOF文件中,这时新 AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
2)对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF 文件,完成新旧两个AOF文件的替换。
1 |
|
第12章 事件
Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:
·文件事件(file event):Redis服务器通过套接字与客户端(或者 其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽 象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事 件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
·时间事件(time event):Redis服务器中的一些操作(比如 serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对 这类定时操作的抽象。
第13章 客户端
—基本概念
Redis服务器是典型的一对多服务器程序:一个服务器可以与多个 客户端建立网络连接,每个客户端可以向服务器发送命令请求,而服务 器则接收并处理客户端发送的命令请求,并向客户端返回命令回复。
通过使用由I/O多路复用技术实现的文件事件处理器,Redis服务器 使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通 信。
对于每个与服务器进行连接的客户端,服务器都为这些客户端建立 了相应的redis.h/redisClient结构(客户端状态),这个结构保存了客户 端当前的状态信息,以及执行相关功能时需要用到的数据结构,其中包 括:
- ·客户端的套接字描述符。
- ·客户端的名字。
- ·客户端的标志值(flag)。 ·指向客户端正在使用的数据库的指针,以及该数据库的号码。 ·客户端当前要执行的命令、命令的参数、命令参数的个数,以及指向命令实现函数的指针。
- ·客户端的输入缓冲区和输出缓冲区。
- ·客户端的复制状态信息,以及进行复制所需的数据结构。
- ·客户端执行BRPOP、BLPOP等列表阻塞命令时使用的数据结 构。
- ·客户端的事务状态,以及执行WATCH命令时用到的数据结构。
- ·客户端执行发布与订阅功能时用到的数据结构。
- ·客户端的身份验证标志。
- ·客户端的创建时间,客户端和服务器最后一次通信的时间,以及 客户端的输出缓冲区大小超出软性限制(soft limit)的时间。
Redis服务器状态结构的clients属性是一个链表,这个链表保存了所 有与服务器连接的客户端的状态结构,对客户端执行批量操作,或者查 找某个指定的客户端,都可以通过遍历clients链表来完成:
—客户端属性
—客户端创建和关闭
Lua脚本的伪客户端
服务器会在初始化时创建负责执行Lua脚本中包含的Redis命令的伪 客户端,并将这个伪客户端关联在服务器状态结构的lua_client属性中:
1 |
|
lua_client伪客户端在服务器运行的整个生命期中会一直存在,只有 服务器被关闭时,这个客户端才会被关闭。
AOF文件的伪客户端
服务器在载入AOF文件时,会创建用于执行AOF文件包含的Redis 命令的伪客户端,并在载入完成之后,关闭这个伪客户端。
—总结
1 |
|
第14章 服务器
Redis服务器负责与多个客户端建立网络连接,处理客户端发送的 命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源 管理来维持服务器自身的运转。
—命令请求过程
1 |
|
关键词:redisClient、redisCommand、argv、argc、buf、
—serverCron函数执行
1 |
|
—初始化服务器
1 |
|
第三部分 多机数据库的实现
第15章 复制
在Redis中,用户可以通过执行SLAVEOF命令或者设置slaveof选 项,让一个服务器去复制(replicate)另一个服务器,我们称呼被复制 的服务器为主服务器(master),而对主服务器进行复制的服务器则被 称为从服务器(slave),如图15-1所示。
—旧版实现
Redis的复制功能分为同步(sync)和命令传播(commandpropagate)两个操作:
·同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
·命令传播操作则用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,让主从服务器的数据库重新回到一致状态。
旧版缺陷分析
在Redis中,从服务器对主服务器的复制可以分为以下两种情况:
- ·初次复制:从服务器以前没有复制过任何主服务器,或者从服务
器当前要复制的主服务器和上一次复制的主服务器不同。 - ·断线后重复制:处于命令传播阶段的主从服务器因为网络原因而
中断了复制,但从服务器通过自动重连接重新连上了主服务器,并继续
复制主服务器。
对于初次复制来说,旧版复制功能能够很好地完成任务,但对于断线后重复制来说,旧版复制功能虽然也能让主从服务器重新回到一致状
态,但效率却非常低。
—新版实现
PSYNC命令具有完整重同步(full resynchronization)和部分重同步 (partial resynchronization)两种模式:
·其中完整重同步用于处理初次复制情况:完整重同步的执行步骤 和SYNC命令的执行步骤基本一样,它们都是通过让主服务器创建并发 送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同 步。
·而部分重同步则用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。
第16章 Sentinel
Sentinel(哨岗、哨兵)是Redis的高可用性(high availability)解决 方案:由一个或多个Sentinel实例(instance)组成的Sentinel系统 (system)可以监视任意多个主服务器,以及这些主服务器属下的所有 从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务 器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替 已下线的主服务器继续处理命令请求。
故障转移
在选举产生出领头Sentinel之后,领头Sentinel将对已下线的主服务
器执行故障转移操作,该操作包含以下三个步骤:
1)在已下线主服务器属下的所有从服务器里面,挑选出一个从服 务器,并将其转换为主服务器。
2)让已下线主服务器属下的所有从服务器改为复制新的主服务 器。
3)将已下线主服务器设置为新的主服务器的从服务器,当这个旧 的主服务器重新上线时,它就会成为新的主服务器的从服务器。
第17章 集群
Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。
本节将对集群的节点、槽指派、命令执行、重新分片、转向、故障转移、消息等各个方面进行介绍。
—节点
一个Redis集群通常由多个节点(node)组成,在刚开始的时候, 每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要 组建一个真正可工作的集群,我们必须将各个独立的节点连接起来,构 成一个包含多个节点的集群。
连接各个节点的工作可以使用CLUSTER MEET命令来完成,该命 令的格式如下:
1 |
|
向一个节点node发送CLUSTER MEET命令,可以让node节点与ip和 port所指定的节点进行握手(handshake),当握手成功时,node节点就 会将ip和port所指定的节点添加到node节点当前所在的集群中。
—槽指派
Redis集群通过分片的方式来保存数据库中的键值对:集群的整个 数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个 槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。
当数据库中的16384个槽都有节点在处理时,集群处于上线状态 (ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群 处于下线状态(fail)。
通过向节点发送CLUSTER ADDSLOTS命令,我们可以将一个或多个槽指派(assign)给节点负责:
CLUSTER ADDSLOTS
—在集群中执行命令
在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。
当客户端向节点发送与数据库键有关的命令时,接收命令的节点会
计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了
自己:
·如果键所在的槽正好就指派给了当前节点,那么节点直接执行这
个命令。·如果键所在的槽并没有指派给当前节点,那么节点会向客户端返 回一个MOVED错误,指引客户端转向(redirect)至正确的节点,并再 次发送之前想要执行的命令。
—分片
Redis集群的重新分片操作可以将任意数量已经指派给某个节点 (源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属 的键值对也会从源节点被移动到目标节点。
重新分片操作可以在线(online)进行,在重新分片的过程中,集 群不需要下线,并且源节点和目标节点都可以继续处理命令请求。
重新分片的实现原理
Redis集群的重新分片操作是由Redis的集群管理软件redis-trib负责 执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib则通过 向源节点和目标节点发送命令来进行重新分片操作。
—复制与故障转移
Redis集群中的节点分为主节点(master)和从节点(slave),其中 主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主 节点下线时,代替下线主节点继续处理命令请求。
第四部分 独立功能的实现-源码
第18章 发布与订阅
Redis的发布与订阅功能由PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令组成。
总结
1 |
|
第19章 事务
Redis通过MULTI、EXEC、WATCH等命令来实现事
第20章 Lua脚本
Redis从2.6版本开始引入对Lua脚本的支持,通过在服务器中嵌入 Lua环境,Redis客户端可以使用Lua脚本,直接在服务器端原子地执行 多个Redis命令。