• redis 设计与实现


    Redis 和其他很多 key-value 数据库的不同之处在于,Redis 不仅支持简单的字符串键值对,它 还提供了一系列数据结构类型值,比如列表、哈希、集合和有序集,并在这些数据结构类型上 定义了一套强大的 API 。

    通过对不同类型的值进行操作,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:

       • Redis 的字符串表示为 sds ,而不是 C 字符串(以 结尾的 char*)。 • 对比 C 字符串,sds 有以下特性:

         – 可以高效地执行长度计算(strlen); – 可以高效地执行追加操作(append); – 二进制安全;

       • 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 的配置,以及内存限制,等等。 不过事务中的命令和普通命令在执行上还是有一点区别的,其中最重要的两点是:

    1. 非事务状态下的命令以单个命令为单位执行,前一个命令和后一个命令的客户端不一定 是同一个;

      而事务状态则是以一个事务为单位,执行事务队列中的所有命令:除非当前事务执行完 毕,否则服务器不会中断事务,也不会执行其他客户端的其他命令。

    2. 在非事务状态下,执行命令所得的结果会立即被返回给客户端; 而事务则是将所有命令的结果集合到回复队列,再作为 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> WATCH name OK
    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)。 

  • 相关阅读:
    随便瞎说
    第六次作业
    第五次作业
    第四次作业
    第二次作业
    第一周java心得
    Ext.Net学习笔记之动态加载TabPanel
    Ext.net学习笔记之ViewPort布局
    MyEclipse连接mySql数据库笔记
    配置服务器
  • 原文地址:https://www.cnblogs.com/bjxq-cs88/p/9449802.html
Copyright © 2020-2023  润新知