Redis设计与实现

黄健宏

前言

  • 它内置了集合数据类型,并支持对集合执行交集、并集、差集等集合计算操作

第1章 引言

  • RedisBook.com提供了一份带有详细注释的Redis源代码

1.2 章节编排

  • 数据库键总是一个字符串对象(string object);❑而数据库键的值则可以是字符串对象、列表对象(list object)、哈希对象(hash object)、集合对象(set object)、有序集合对象(sorted setobject)这五种对象中的其中一种。

2.2 SDS与C字符串的区别

  • 和C字符串不同,因为SDS在len属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度仅为O(1)。
  • 通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。
  • .惰性空间释放 惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。
  • 通过惰性空间释放策略,SDS避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化。
  • 为了确保Redis可以适用于各种不同的使用场景,SDS的API都是二进制安全的(binary-safe),所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。
  • 表2-1 C字符串和SDS之间的区别

2.4 重点回顾

  • 比起C字符串,SDS具有以下优点:1)常数复杂度获取字符串长度。2)杜绝缓冲区溢出。3)减少修改字符串长度时所需的内存重分配次数。4)二进制安全。5)兼容部分C字符串函数。
  • 比起C字符串,SDS具有以下优点: 1)常数复杂度获取字符串长度。 2)杜绝缓冲区溢出。 3)减少修改字符串长度时所需的内存重分配次数。 4)二进制安全。 5)兼容部分C字符串函数。

第3章 链表

  • 列表键的底层实现之一就是链表
  • 除了链表键之外,发布与订阅、慢查询、监视器等功能也用到了链表,Redis服务器本身还使用链表来保存多个客户端的状态信息,以及使用链表来构建客户端输出缓冲区(output buffer)

第4章 字典

  • 字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。
  • Redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、查、改操作也是构建在对字典的操作之上的。
  • 除了用来表示数据库之外,字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。

4.1 字典的实现

  • Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
  • ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。

4.2 哈希算法

  • 当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。
  • MurmurHash算法最初由Austin Appleby于2008年发明,这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快。

4.3 解决键冲突

  • Redis的哈希表使用链地址法(separate chaining)来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题。
  • 因为dictEntry节点组成的链表没有指向链表表尾的指针,所以为了速度考虑,程序总是将新节点添加到链表的表头位置(复杂度为O(1)),排在其他已有节点的前面。

4.4 rehash

  • 随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。

4.5 渐进式rehash

  • 扩展或收缩哈希表需要将ht[0]里面的所有键值对rehash到ht[1]里面,但是,这个rehash动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。
  • 另外,在渐进式rehash执行期间,新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作,这一措施保证了ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表。

4.7 重点回顾

  • Redis中的字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。

第5章 跳跃表

  • 跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
  • Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构

5.1 跳跃表的实现

  • 每次创建一个新跳跃表节点的时候,程序都根据幂次定律(powerlaw,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。
  • 每次创建一个新跳跃表节点的时候,程序都根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。

5.3 重点回顾

  • 每个跳跃表节点的层高都是1至32之间的随机数。

第6章 整数集合

  • 整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

6.1 整数集合的实现

  • 整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。

6.2 升级

  • 每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级(upgrade),然后才能将新元素添加到整数集合里面。
  • 所以向整数集合添加新元素的时间复杂度为O(N)。

6.3 升级的好处

  • 整数集合的升级策略有两个好处,一个是提升整数集合的灵活性,另一个是尽可能地节约内存。

6.4 降级

  • 整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。

第7章 压缩列表

  • 压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
  • 当一个哈希键只包含少量键值对,比且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做哈希键的底层实现。

7.1 压缩列表的构成

  • 压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。

7.2 压缩列表节点的构成

  • 每个压缩列表节点可以保存一个字节数组或者一个整数值
  • 节点的encoding属性记录了节点的content属性所保存数据的类型以及长度:

7.3 连锁更新

  • Redis将这种在特殊情况下产生的连续多次空间扩展操作称之为“连锁更新”(cascade update)

第8章 对象

  • Redis的对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放;另外,Redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。

8.1 对象的类型与编码

  • Redis使用对象来表示数据库中的键和值,每次当我们在Redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。
  • Redis中的每个对象都由一个redisObject结构表示,该结构中和保存数据有关的三个属性分别是type属性、encoding属性和ptr属性:
  • 对于Redis数据库保存的键值对来说,键总是一个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种
  • 通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大地提升了Redis的灵活性和效率,因为Redis可以根据不同的使用场景来为一个对象设置不同的编码,从而优化对象在某一场景下的效率。

8.2 字符串对象

  • 字符串对象的编码可以是int、raw或者embstr。
  • embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr两个结构
  • 可以用long double类型表示的浮点数在Redis中也是作为字符串值来保存的。
  • int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。
  • embstr编码的字符串对象在执行修改命令之后,总会变成一个raw编码的字符串对象。

8.3 列表对象

  • 列表对象的编码可以是ziplist或者linkedlist。
  • 字符串对象是Redis五种类型的对象中唯一一种会被其他四种类型对象嵌套的对象。
  • 当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:❑列表对象保存的所有字符串元素的长度都小于64字节;❑列表对象保存的元素数量小于512个;不能满足这两个条件的列表对象需要使用linkedlist编码。

8.4 哈希对象

  • 哈希对象的编码可以是ziplist或者hashtable。
  • 当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:❑哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;❑哈希对象保存的键值对数量小于512个;不能满足这两个条件的哈希对象需要使用hashtable编码。

8.5 集合对象

  • 集合对象的编码可以是intset或者hashtable。

8.6 有序集合对象

  • 有序集合的编码可以是ziplist或者skiplist。
  • skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表
  • 当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:❑有序集合保存的元素数量小于128个;❑有序集合保存的所有元素成员的长度都小于64字节;不能满足以上两个条件的有序集合对象将使用skiplist编码。

8.7 类型检查与命令多态

  • 类型特定命令所进行的类型检查是通过redisObject结构的type属性来实现的:
  • Redis除了会根据值对象的类型来判断键是否能够执行指定命令之外,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令。
  • DEL、EXPIRE等命令和LLEN等命令的区别在于,前者是基于类型的多态——一个命令可以同时用于处理多种不同类型的键,而后者是基于编码的多态——一个命令可以同时用于处理多种不同编码。

8.8 内存回收

  • 所以Redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收

8.9 对象共享

  • 除了用于实现引用计数内存回收机制之外,对象的引用计数属性还带有对象共享的作用。
  • 在Redis中,让多个键共享同一个值对象需要执行以下两个步骤: 1)将数据库键的值指针指向一个现有的值对象; 2)将被共享的值对象的引用计数增一。
  • 在Redis中,让多个键共享同一个值对象需要执行以下两个步骤:1)将数据库键的值指针指向一个现有的值对象;2)将被共享的值对象的引用计数增一。
  • 目前来说,Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。
  • 因此,尽管共享更复杂的对象可以节约更多的内存,但受到CPU时间的限制,Redis只对包含整数值的字符串对象进行共享。

8.10 对象的空转时长

  • 除了前面介绍过的type、encoding、ptr和refcount四个属性之外,redisObject结构包含的最后一个属性为lru属性,该属性记录了对象最后一次被命令程序访问的时间
  • 如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。

8.11 重点回顾

  • Redis会共享值为0到9999的字符串对象。

9.1 服务器中的数据库

  • dbnum属性的值由服务器配置的database选项决定,默认情况下,该选项的值为16,所以Redis服务器默认会创建16个数据库

9.2 切换数据库

  • 默认情况下,Redis客户端的目标数据库为0号数据库,但客户端可以通过执行SELECT命令来切换目标数据库。
  • 通过修改redisClient.db指针,让它指向服务器中的不同数据库,从而实现切换目标数据库的功能——这就是SELECT命令的实现原理。

9.3 数据库键空间

  • redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间(key space)

9.4 设置键的生存时间或过期时间

  • 通过EXPIRE命令或者PEXPIRE命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL),在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为0的键:
  • SETEX命令可以在设置一个字符串键的同时为键设置过期时间,因为这个命令是一个类型限定的命令(只能用于字符串键)
  • EXPIREAT命令或PEXPIREAT命令,以秒或者毫秒精度给数据库中的某个键设置过期时间
  • 虽然有多种不同单位和不同形式的设置命令,但实际上EXPIRE、PEXPIRE、EXPIREAT三个命令都是使用PEXPIREAT命令来实现的:无论客户端执行的是以上四个命令中的哪一个,经过转换之后,最终的执行效果都和执行PEXPIREAT命令一样。
  • redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典
  • PERSIST命令可以移除一个键的过期时间

9.5 过期键删除策略

  • ❑定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。 ❑惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。 ❑定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
  • 惰性删除策略对CPU时间来说是最友好的

9.6 Redis的过期键删除策略

  • Redis服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。
  • 过期键的惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查:

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

  • 在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
  • 如果服务器以从服务器模式运行,那么在载入RDB文件时,文件中保存的所有键,不论是否过期,都会被载入到数据库中。不过,因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所以一般来讲,过期键对载入RDB文件的从服务器也不会造成影响。
  • 和生成RDB文件时类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。
  • 通过由主服务器来控制从服务器统一地删除过期键,可以保证主从服务器数据的一致性,也正是因为这个原因,当一个过期键仍然存在于主服务器的数据库时,这个过期键在从服务器里的复制品也会继续存在。

9.8 数据库通知

  • 这一类关注“某个键执行了什么命令”的通知称为键空间通知(key-space notification)

9.9 重点回顾

  • 数据库主要由dict和expires两个字典构成,其中dict字典负责保存键值对,而expires字典则负责保存键的过期时间。 ❑因为数据库由字典构成,所以对数据库的操作都是建立在字典操作之上的

第10章 RDB持久化

  • RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态

10.1 RDB文件的创建与载入

  • SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求:
  • RDB文件的载入工作是在服务器启动时自动执行的,所以Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件。
  • 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。❑只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。
  • 服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。

10.2 自动间隔性保存

  • 因为BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。

10.3 RDB文件结构

  • 会将载入数据所计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或者损坏的情况出现。

第11章 AOF持久化

  • AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的
  • RDB持久化保存数据库状态的方法是将msg、fruits、numbers三个键的键值对保存到RDB文件中,而AOF持久化保存数据库状态的方法则是将服务器执行的SET、SADD、RPUSH三个命令保存到AOF文件中。

11.1 AOF持久化的实现

  • AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤。
  • Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。
  • flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定,各个不同值产生的行为如表11-1所示。
  • 系统提供了fsync和fdatasync两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面,从而确保写入数据的安全性。

11.3 AOF重写

  • 因为AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝,AOF文件中的内容会越来越多,文件的体积也会越来越大,如果不加以控制的话,体积过大的AOF文件很可能对Redis服务器、甚至整个宿主计算机造成影响,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。
  • 际上,AOF文件重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的
  • 首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是AOF重写功能的实现原理。

11.4 重点回顾

  • AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读入、分析或者写入操作。

12.1 文件事件

  • Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)

12.2 时间事件

  • 服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

第13章 客户端

  • 通过使用由I/O多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。

13.1 客户端属性

  • 目前Redis服务器会在两个地方用到伪客户端,一个用于载入AOF文件并还原数据库状态,而另一个则用于执行Lua脚本中包含的Redis命令。

15.1 旧版复制功能的实现

  • Redis的复制功能分为同步(sync)和命令传播(command propagate)两个操作

15.3 新版复制功能的实现

  • PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式:

15.4 部分重同步的实现

  • 复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认大小为1MB。
  • 相反地,如果从服务器保存的运行ID和当前连接的主服务器的运行ID并不相同,那么说明从服务器断线之前复制的主服务器并不是当前连接的这个主服务器,主服务器将对从服务器执行完整重同步操作。

15.7 心跳检测

  • 在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令

15.8 重点回顾

  • 部分重同步通过复制偏移量、复制积压缓冲区、服务器运行ID三个部分来实现。

第16章 Sentinel

  • Sentinel(哨岗、哨兵)是Redis的高可用性(high availability)解决方案:由一个或多个Sentinel实例(instance)组成的Sentinel系统(system)可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

16.1 启动并初始化Sentinel

  • Sentinel本质上只是一个运行在特殊模式下的Redis服务器

16.2 获取主服务器信息

  • Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息。
  • Sentinel无须用户提供从服务器的地址信息,就可以自动发现从服务器。

16.4 向主服务器和从服务器发送信息

  • 在默认情况下,Sentinel会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送以下格式的命令

16.5 接收来自主服务器和从服务器的频道信息

  • 对于每个与Sentinel连接的服务器,Sentinel既通过命令连接向服务器的__sentinel__:hello频道发送信息,又通过订阅连接从服务器的__sentinel__:hello频道接收信息

16.6 检测主观下线状态

  • 在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。

16.7 检查客观下线状态

  • 当Sentinel从其他Sentinel那里接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。
  • 当认为主服务器已经进入下线状态的Sentinel的数量,超过Sentinel配置中设置的quorum参数的值,那么该Sentinel就会认为主服务器已经进入客观下线状态。

16.8 选举领头Sentinel

  • 如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出领头Sentinel为止。

16.9 故障转移

  • 1)在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器。 2)让已下线主服务器属下的所有从服务器改为复制新的主服务器。 3)将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。

第17章 集群

  • Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。

17.2 槽指派

  • Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。
  • slots属性是一个二进制位数组(bit array),这个数组的长度为16384/8=2048个字节,共包含16384个二进制位。

17.3 在集群中执行命令

  • 节点和单机服务器在数据库方面的一个区别是,节点只能使用0号数据库,而单机Redis服务器则没有这一限制。

17.4 重新分片

  • Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。

17.6 复制与故障转移

  • Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。
  • 如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。

17.7 消息

  • Redis集群中的各个节点通过Gossip协议来交换各自关于不同节点的状态信息,其中Gossip协议由MEET、PING、PONG三种消息实现

18.1 频道的订阅与退订

  • Redis将所有频道的订阅关系都保存在服务器状态的pubsub_channels字典里面,这个字典的键是某个被订阅的频道,而键的值则是一个链表,链表里面记录了所有订阅这个频道的客户端

第19章 事务

  • Redis通过MULTI、EXEC、WATCH等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。

19.1 事务的实现

  • 一个事务从开始到结束通常会经历以下三个阶段:1)事务开始。2)命令入队。3)事务执行。
  • MULTI命令可以将执行该命令的客户端从非事务状态切换至事务状态,这一切换是通过在客户端状态的flags属性中打开REDIS_MULTI标识来完成的

19.2 WATCH命令的实现

  • WATCH命令是一个乐观锁(optimistic locking),它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。

19.3 事务的ACID性质

  • 在Redis中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当Redis运行在某种特定的持久化模式下时,事务也具有耐久性(Durability)。
  • Redis的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。
  • “一致”指的是数据符合数据库本身的定义和要求,没有包含非法或者无效的错误数据。
  • 如果一个事务在入队命令的过程中,出现了命令不存在,或者命令的格式不正确等情况,那么Redis将拒绝执行这个事务。
  • 因为Redis使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事务进行中断,因此,Redis的事务总是以串行的方式运行的,并且事务也总是具有隔离性的。
  • 不论Redis在什么模式下运作,在一个事务的最后加上SAVE命令总可以保证事务的耐久性

第21章 排序

  • Redis的SORT命令可以对列表键、集合键或者有序集合键的值进行排序。

21.4 BY选项的实现

  • 通过使用BY选项,SORT命令可以指定某些字符串键,或者某个哈希键所包含的某些域(field)来作为元素的权重,对一个键进行排序。

21.8 STORE选项的实现

  • 通过使用STORE选项,我们可以将排序结果保存在指定的键里面,并在有需要时重用这个排序结果

第23章 慢查询日志

  • slowlog-log-slower-than选项指定执行时间超过多少微秒(1秒等于1 000 000微秒)的命令请求会被记录到日志上。

第24章 监视器

  • 通过执行MONITOR命令,客户端可以将自己变为一个监视器,实时地接收并打印出服务器当前处理的命令请求的相关信息: