Redis 和其余不少 key-value 数据库的不一样之处在于,Redis 不只支持简单的字符串键值对,它 还提供了一系列数据结构类型值,好比列表、哈希、集合和有序集,并在这些数据结构类型上 定义了一套强大的 API 。redis
经过对不一样类型的值进行操做,Redis 能够很轻易地完成其余只支持字符串键值对的 key-value 数据库很难(或者没法)完成的任务。算法
在 Redis 的内部,数据结构类型值由高效的数据结构和算法进行支持,而且在 Redis 自身的构 建当中,也大量用到了这些数据结构。数据库
1.1 简单动态字符串数组
Sds (Simple Dynamic String,简单动态字符串)是 Redis 底层所使用的字符串表示,它被用 在几乎全部的 Redis 模块中。安全
1.2 sds 的用途服务器
Sds 在 Redis 中的主要做用有如下两个:
1. 实现字符串对象(StringObject);
2. 在 Redis 程序内部用做 char* 类型的替代品;数据结构
1.3 总结 SDS:app
• Redis 的字符串表示为 sds ,而不是 C 字符串(以 \0 结尾的 char*)。 • 对比 C 字符串,sds 有如下特性:数据结构和算法
– 能够高效地执行长度计算(strlen); – 能够高效地执行追加操做(append); – 二进制安全;async
• sds 会为追加操做进行优化:加快追加操做的速度,并下降内存分配的次数,代价是多占 用了一些内存,并且这些内存不会被主动释放。
2.1 双端链表
链表做为数组以外的一种经常使用序列抽象,是大多数高级语言的基本数据类型,由于 C 语言自己 不支持链表类型,大部分 C 程序都会本身实现一种链表类型,Redis 也不例外——它实现了一 个双端链表结构。
Note: Redis 列表使用两种数据结构做为底层实现:
1. 双端链表
2. 压缩列表
由于双端链表占用的内存比压缩列表要多,因此当建立新的列表键时,列表会优先考虑使用压 缩列表做为底层实现,而且在有须要的时候,才从压缩列表实现转换到双端链表实现。
双端链表的实现
双端链表的实现由 listNode 和 list 两个数据结构构成,下图展现了由这两个结构组成的一 个双端链表实例:
• Redis 实现了本身的双端链表结构。
• 双端链表主要有两个做用:
– 做为 Redis 列表类型的底层实现之一;
– 做为通用数据结构,被其余功能模块所使用; • 双端链表及其节点的性能特性以下:
– 节点带有前驱和后继指针,访问前驱节点和后继节点的复杂度为 O(1) ,而且对链表 的迭代能够在从表头到表尾和从表尾到表头两个方向进行;
– 链表带有指向表头和表尾的指针,所以对表头和表尾进行处理的复杂度为 O(1) ;
– 链表带有记录节点数量的属性,因此能够在 O(1) 复杂度内返回链表的节点数量(长度);
3.1 跳跃表
跳跃表(skiplist)是一种随机化的数据,由 William Pugh 在论文《Skip lists: a probabilistic alternative to balanced trees》中提出,这种数据结构以有序的方式在层次化的链表中保存元 素,它的效率能够和平衡树媲美——查找、删除、添加等操做均可以在对数指望时间下完成, 而且比起平衡树来讲,跳跃表的实现要简单直观得多。
表头(head):负责维护跳跃表的节点指针。
跳跃表节点:保存着元素值,以及多个层。
层:保存着指向其余元素的指针。高层的指针越过的元素数量大于等于低层的指针,为了 提升查找的效率,程序老是从高层先开始访问,而后随着元素值范围的缩小,慢慢下降层 次。
表尾:所有由NULL组成,表示跳跃表的末尾。 由于跳跃表的定义能够在任何一本算法或数据结构的书中找到,因此本章不介绍跳跃表的具体
实现方式或者具体的算法,而只介绍跳跃表在 Redis 的应用、核心数据结构和 API 。
跳跃表是一种随机化数据结构,它的查找、添加、删除操做均可以在对数指望时间下完
成。
跳跃表目前在 Redis 的惟一做用就是做为有序集类型的底层数据结构(之一,另外一个构 成有序集的结构是字典)。
为了适应自身的需求,Redis 基于 William Pugh 论文中描述的跳跃表进行了修改,包括:
1. score 值可重复。
2. 对比一个元素须要同时检查它的score和memeber。
3. 每一个节点带有高度为 1 层的后退指针,用于从表尾方向向表头方向迭代。
4. Redis 数据类型:
5, 事务
Redis 经过 MULTI 、DISCARD 、EXEC 和 WATCH 四个命令来实现事务功能,本章首先讨 论使用 MULTI 、DISCARD 和 EXEC 三个命令实现的通常事务,而后再来讨论带有 WATCH 的事务的实现。
由于事务的安全性也很是重要,因此本章最后经过常见的 ACID 性质对 Redis 事务的安全性进 行了说明。
(1) .事务提供了一种“将多个命令打包,而后一次性、按顺序地执行”的机制,而且事务在执行的期 间不会主动中断——服务器在执行完事务中的全部命令以后,才会继续处理其余客户端的其余 命令。
如下是一个事务的例子,它先以 MULTI 开始一个事务,而后将多个命令入队到事务中,最后 由 EXEC 命令触发事务,一并执行事务中的全部命令:
redis> MULTI OK
redis> SET book-name "Mastering C++ in 21 days"
81
Redis 设计与实现, 初版
QUEUED
redis> GET book-name QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series" QUEUED
redis> SMEMBERS tag QUEUED
redis> EXEC 1) OK 2) "Mastering C++ in 21 days" 3) (integer) 3 4) 1) "Mastering Series"
2) "C++" 3) "Programming"
一个事务从开始到执行会经历如下三个阶段:
1. 开始事务。
2. 命令入队。
3. 执行事务。
(2) .命令入队 当客户端处于非事务状态下时,全部发送给服务器端的命令都会当即被服务器执行:
可是,当客户端进入事务状态以后,服务器在收到来自客户端的命令时,不会当即执行命令, 而是将这些命令所有放进一个事务队列里,而后返回 QUEUED ,表示命令已入队:
如下流程图展现了这一行为:
Redis 设计与实现, 初版
redis> SET msg "hello moto" OK
redis> GET msg "hello moto"
redis> MULTI OK
redis> SET msg "hello moto" QUEUED
redis> GET msg QUEUED
举个例子,若是客户端执行如下命令:
redis> MULTI OK
redis> SET book-name "Mastering C++ in 21 days" QUEUED
redis> GET book-name QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series" QUEUED
redis> SMEMBERS tag QUEUED
那么程序将为客户端建立如下事
数组索引 |
cmd |
argv |
argc |
0 |
SET |
["book-name", "Mastering C++ in 21 days"] |
2 |
1 |
GET |
["book-name"] |
1 |
2 |
SADD |
["tag", "C++", "Programming", "Mastering Series"] |
4 |
3 |
SMEMBERS |
["tag"] |
1 |
(3) 执行事务 前面说到,当客户端进入事务状态以后,客户端发送的命令就会被放进事务队列里。
但其实并非全部的命令都会被放进事务队列,其中的例外就是 EXEC 、DISCARD 、MULTI 和 WATCH 这四个命令——当这四个命令从客户端发送到服务器时,它们会像客户端处于非 事务状态同样,直接被服务器执行:
程序会首先执行 SET 命令,而后执行 GET 命令,再而后执行 SADD 命令,最后执行 SMEM- BERS 命令。
执行事务中的命令所得的结果会以 FIFO 的顺序保存到一个回复队列中。 好比说,对于上面给出的事务队列,程序将为队列中的命令建立以下回复队列:
数组索引 |
回复类型 |
回复内容 |
0 |
status code reply |
OK |
1 |
bulk reply |
"Mastering C++ in 21 days" |
2 |
integer reply |
3 |
3 |
multi-bulk reply |
["Mastering Series", "C++", "Programming"] |
当事务队列里的全部命令被执行完以后,EXEC 命令会将回复队列做为本身的执行结果返回给 客户端,客户端从事务状态返回到非事务状态,至此,事务执行完毕。
事务的整个执行过程能够用如下伪代码表示:
def execute_transaction(): # 建立空白的回复队列
reply_queue = []
# 取出事务队列里的全部命令、参数和参数数量
for cmd, argv, argc in client.transaction_queue: # 执行命令,并取得命令的返回值
reply = execute_redis_command(cmd, argv, argc) # 将返回值追加到回复队列末尾
reply_queue.append(reply) # 清除客户端的事务状态
clear_transaction_state(client)
# 清空事务队列 clear_transaction_queue(client)
# 将事务的执行结果返回给客户端 send_reply_to_client(client, reply_queue)
4.1.5 在事务和非事务状态下执行命令
不管在事务状态下,仍是在非事务状态下,Redis 命令都由同一个函数执行,因此它们共享很
多服务器的通常设置,好比 AOF 的配置、RDB 的配置,以及内存限制,等等。 不过事务中的命令和普通命令在执行上仍是有一点区别的,其中最重要的两点是:
非事务状态下的命令以单个命令为单位执行,前一个命令和后一个命令的客户端不必定 是同一个;
而事务状态则是以一个事务为单位,执行事务队列中的全部命令:除非当前事务执行完 毕,不然服务器不会中断事务,也不会执行其余客户端的其余命令。
在非事务状态下,执行命令所得的结果会当即被返回给客户端; 而事务则是将全部命令的结果集合到回复队列,再做为 EXEC 命令的结果返回给客户端。
4.1.6 事务状态下的 DISCARD 、MULTI 和 WATCH 命令
除了 EXEC 以外,服务器在客户端处于事务状态时,不加入到事务队列而直接执行的另外三
个命令是 DISCARD 、MULTI 和 WATCH 。
86 Chapter 4. 功能的实现
Redis 设计与实现, 初版 DISCARD 命令用于取消一个事务,它清空客户端的整个事务队列,而后将客户端从事务状态
调整回非事务状态,最后返回字符串 OK 给客户端,说明事务已被取消。
Redis 的事务是不可嵌套的,当客户端已经处于事务状态,而客户端又再向服务器发送 MULTI 时,服务器只是简单地向客户端发送一个错误,而后继续等待其余命令的入队。MULTI 命令 的发送不会形成整个事务失败,也不会修改事务队列中已有的数据。
WATCH 只能在客户端进入事务状态以前执行,在事务状态下发送 WATCH 命令会引起一个 错误,但它不会形成整个事务失败,也不会修改事务队列中已有的数据(和前面处理 MULTI 的状况同样)。
4.1.7 带 WATCH 的事务
WATCH 命令用于在事务开始以前监视任意数量的键:当调用 EXEC 命令执行事务时,若是
任意一个被监视的键已经被其余客户端修改了,那么整个事务再也不执行,直接返回失败。 如下示例展现了一个执行失败的事务例子:
redis> MULTI OK
redis> SET name peter QUEUED
redis> EXEC (nil)
如下执行序列展现了上面的例子是如何失败的:
在时间 T4 ,客户端 B 修改了 name 键的值,当客户端 A 在 T5 执行 EXEC 时,Redis 会发现 name 这个被监视的键已经被修改,所以客户端 A 的事务不会被执行,而是直接返回失败。
下文就来介绍 WATCH 的实现机制,而且看看事务系统是如何检查某个被监视的键是否被修 改,从而保证事务的安全性的。
4.1.8 WATCH 命令的实现
在每一个表明数据库的 redis.h/redisDb 结构类型中,都保存了一个 watched_keys 字典,字典 的键是这个数据库被监视的键,而字典的值则是一个链表,链表中保存了全部监视这个键的客 户端。
4.1.9 WATCH 的触发
在任何对数据库键空间(key space)进行修改的命令成功执行以后(好比 FLUSHDB 、SET 、DEL 、LPUSH 、SADD 、ZREM ,诸如此类),multi.c/touchWatchKey 函数都会被调用 ——它检查数据库的 watched_keys 字典,看是否有客户端在监视已经被命令修改的键,若是 有的话,程序将全部监视这个/这些被修改键的客户端的 REDIS_DIRTY_CAS 选项打开:
最后,当一个客户端结束它的事务时,不管事务是成功执行,仍是失败,watched_keys 字典
中和这个客户端相关的资料都会被清除。
4.1.10 事务的 ACID 性质
在传统的关系式数据库中,经常用 ACID 性质来检验事务功能的安全性。
Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。 如下四小节是关于这四个性质的详细讨论。
原子性(Atomicity)
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增长任何维持原子性的机制,因此
Redis 事务的执行并非原子性的。 若是一个事务队列中的全部命令都被成功地执行,那么称这个事务执行成功。
另外一方面,若是 Redis 服务器进程在执行事务的过程当中被中止——好比接到 KILL 信号、宿主 机器停机,等等,那么事务执行失败。
当事务失败时,Redis 也不会进行任何的重试或者回滚动做。 一致性(Consistency)
Redis 的一致性问题能够分为三部分来讨论:入队错误、执行错误、Redis 进程被终结。 入队错误
在命令入队的过程当中,若是客户端向服务器发送了错误的命令,好比命令的参数数量 不对,等等,那么服务器将向客户端返回一个出错信息,而且将客户端的事务状态设为 REDIS_DIRTY_EXEC 。
当客户端执行 EXEC 命令时,Redis 会拒绝执行状态为 REDIS_DIRTY_EXEC 的事务,并返回失 败信息。
redis 127.0.0.1:6379> MULTI OK
redis 127.0.0.1:6379> set key (error) ERR wrong number of arguments for 'set' command
redis 127.0.0.1:6379> EXISTS key QUEUED
redis 127.0.0.1:6379> EXEC (error) EXECABORT Transaction discarded because of previous errors.
所以,带有不正确入队命令的事务不会被执行,也不会影响数据库的一致性。
若是命令在事务执行的过程当中发生错误,好比说,对一个不一样类型的 key 执行了错误的操做, 那么 Redis 只会将错误包含在事务的结果中,这不会引发事务中断或整个失败,不会影响已执 行事务命令的结果,也不会影响后面要执行的事务命令,因此它对事务的一致性也没有影响。
Redis 进程被终结
若是 Redis 服务器进程在执行事务的过程当中被其余进程终结,或者被管理员强制杀死,那么根
据 Redis 所使用的持久化模式,可能有如下状况出现:
内存模式:若是 Redis 没有采起任何持久化机制,那么重启以后的数据库老是空白的,所
以数据老是一致的。
RDB 模式:在执行事务时,Redis 不会中断事务去执行保存 RDB 的工做,只有在事务执 行以后,保存 RDB 的工做才有可能开始。因此当 RDB 模式下的 Redis 服务器进程在事 务中途被杀死时,事务内执行的命令,无论成功了多少,都不会被保存到 RDB 文件里。 恢复数据库须要使用现有的 RDB 文件,而这个 RDB 文件的数据保存的是最近一次的数 据库快照(snapshot),因此它的数据可能不是最新的,但只要 RDB 文件自己没有由于 其余问题而出错,那么还原后的数据库就是一致的。
AOF 模式:由于保存 AOF 文件的工做在后台线程进行,因此即便是在事务执行的中途, 保存 AOF 文件的工做也能够继续进行,所以,根据事务语句是否被写入并保存到 AOF 文件,有如下两种状况发生:
1)若是事务语句未写入到 AOF 文件,或 AOF 未被 SYNC 调用保存到磁盘,那么当进 程被杀死以后,Redis 能够根据最近一次成功保存到磁盘的 AOF 文件来还原数据库,只 要 AOF 文件自己没有由于其余问题而出错,那么还原后的数据库老是一致的,但其中的 数据不必定是最新的。
2)若是事务的部分语句被写入到 AOF 文件,而且 AOF 文件被成功保存,那么不完整的 事务执行信息就会遗留在 AOF 文件里,当重启 Redis 时,程序会检测到 AOF 文件并不 完整,Redis 会退出,并报告错误。须要使用 redis-check-aof 工具将部分红功的事务命令 移除以后,才能再次启动服务器。还原以后的数据老是一致的,并且数据也是最新的(直 到事务执行以前为止)。
隔离性(Isolation)
Redis 是单进程程序,而且它保证在执行事务时,不会对事务进行中断,事务能够运行直到执
行完全部事务队列中的命令为止。所以,Redis 的事务是老是带有隔离性的。 持久性(Durability)
由于事务不过是用队列包裹起了一组 Redis 命令,并无提供任何额外的持久性功能,因此事 务的持久性由 Redis 所使用的持久化模式决定:
在单纯的内存模式下,事务确定是不持久的。
在 RDB 模式下,服务器可能在事务执行以后、RDB 文件更新以前的这段时间失败,所
以 RDB 模式下的 Redis 事务也是不持久的。
在 AOF 的“老是 SYNC ”模式下,事务的每条命令在执行成功以后,都会当即调用 fsync
或 fdatasync 将事务数据写入到 AOF 文件。可是,这种保存是由后台线程进行的,主 4.1. 事务 91
Redis 设计与实现, 初版
Redis 设计与实现, 初版 线程不会阻塞直到保存成功,因此从命令执行成功到数据保存到硬盘之间,仍是有一段
很是小的间隔,因此这种模式下的事务也是不持久的。
其余 AOF 模式也和“老是 SYNC ”模式相似,因此它们都是不持久的。
4.1.11 小结
事务提供了一种将多个命令打包,而后一次性、有序地执行的机制。
事务在执行过程当中不会被中断,全部事务命令执行完以后,事务才能结束。
多个命令会被入队到事务队列中,而后按先进先出(FIFO)的顺序执行。
带WATCH命令的事务会将客户端和被监视的键在数据库的watched_keys字典中进行关 联,当键被修改时,程序会将全部监视被修改键的客户端的 REDIS_DIRTY_CAS 选项打开。
只有在客户端的REDIS_DIRTY_CAS选项未被打开时,才能执行事务,不然事务直接返回 失败。
Redis 的事务保证了 ACID 中的一致性(C)和隔离性(I),但并不保证原子性(A)和 持久性(D)。