redis设计与实现


前言

正直春招时期,看到一些大厂后端面经。心底拔凉拔凉的,啥玩意?平时都只是注重应用层(会用就行了),没有接触一些底层。但是……一些大厂的面试官就会问你一些关于底层的东西。:triumph:

吓得我赶紧读本redis设计与实现压压惊。全书有24章,坚持每天读一章,并记录。第一章是引言,所以过。

第2章 简单动态字符串

redis没有使用c语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作redis的默认字符串表示。

SDS的定义

每个sds.h/sdshdr结构表示一个SDS值:

struct sdshdr{
    // 记录buf数组中已使用字节的数量
    // 等于SDS所保存字符串的长度
    int len;
    // 记录buf数组中未使用字节的数量
    int free;
    // 字节数组,用于保存字符串
    char buf[];
};

SDS示例.png

SDS与c字符串的区别

  • 常数复杂度获取字符串长度
  • 杜绝缓冲区溢出
  • 减少修改字符串时带来的内存重分配次数
    • 空间预分配
    • 惰性空间释放
  • 二进制安全
  • 兼容部分C字符串函数
    总结:
C字符串 SDS
获取字符串长度的复杂度为O(N) 获取字符串长度的复杂度为O(1)
API是不安全的,可能会造成缓冲区溢出 API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配 修改字符串长度N次最多需要执行N次内存重分配
只能保存文本数据 可以保存文本或者二进制数据
可以使用所有<string.h>库中的函数 可以使用一部分<string.h>库中的函数

重点回顾

Redis只会使用C字符串作为字面量,在大多数情况下,Redis使用SDS作为字符串表示。

第3章 链表

作为一种常用的数据结构,链表内置在很多高级的编程里面,因为Redis使用的C语言并没有内置这种数据结构,所以Redis构建了自己的链表实现。

Redis链表实现的特性

  • 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。
  • 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。
  • 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头结点和表尾节点的复杂度为O(1)。
  • 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。
  • 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

重点回顾

  • 链表被广泛用于实现Redis的各种功能,比如列表键、发布与订阅、慢查询、监视器等。
  • 每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以Redis的链表实现是双端链表。
  • 每个链表使用一个list结构来表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度等信息。
  • 因为链表表头节点的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表。
  • 通过为链表设置不同的类型特定函数,Redis的链表可以用于保存各种不同类型的值。

第4章 字典

重点回顾

  • 字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键。
  • Redis中的字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
  • 当字典被用作数据库的底层实现,或者哈希键的底层是现实,Redis使用MurmurHash2算法来计算键的哈希值。
  • 哈希表使用链地址法来解决冲突,被非配到同一个索引上的多个键值对会连接成一个单向链表。
  • 在对哈希表进行扩展或者收缩操作时,程序需要将现有哈希表包含的所有键值对rehash到新哈希表里面,并且这个rehash过程并不是一次性地完成的,而是渐进式地完成的。

第5章 跳跃表

跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳跃表支持平均O(log^N)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。

重点回顾

  • 跳跃表是有序集合的底层实现之一。
  • Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点。
  • 每个跳跃表节点的层高都是1~32之间的随机数。
  • 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是惟一的。
  • 跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。

第6章 整数集合

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

typedef struct intset{
    // 编码方式
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组
    int8_t contents[];
}

contents数组是整数集合的底层实现:数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。

重点回顾

  • 整数集合是集合键的底层实现之一。
  • 整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型,改变这个数组的类型。
  • 升级操作为整个集合带来了操作上的灵活性,并且尽可能地节约了内存。
  • 整数集合只支持升级操作,不支持降级操作。

第7章 压缩列表

压缩列表(ziplist)是列表键和哈希键的底层实现之一。

重点回顾

  • 压缩列表是一种为节约内存而开发的顺序型数据结构
  • 压缩列表被用作列表键和哈希键的底层实现之一。
  • 压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值。
  • 添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的几率并不高。

第8章 对象

字符串对象

字符串对象的编码可以是int、raw或者embstr。
| 值 | 编码 |
| —- | —- |
| 可以用long类型保存的整数 | int |
| 可以用long double类型保存的浮点数 | embstr或者raw |
| 字符串值,或者因为长度太大而没办法用long类型表示的整数,又或者因为长度太大而没办法用long double类型表示的浮点数 | embstr或者raw |

int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。

列表对象

列表对象的编码可以是ziplist或者linkedlist。
当列表对象可以同时满足一下两个条件时,列表对象使用ziplist编码:

  • 列表对象保存的所有字符串元素的长度都小于64字节
  • 列表对象保存的元素数量小于512个,不能满足这两个条件的列表对象需要使用linkedlist编码。
    以上两个条件的上限值是可以修改的

    哈希对象

    哈希对象的编码可以是ziplist或者hashtable。
    编码转换条件上限值如上列表对象的上限值。

    集合对象

    集合对象的编码可以是intset或者hashtable。
    当集合对象可以同时满足一下两个条件时,对象使用intset编码:
  • 集合对象保存的所有元素都是整数值。
  • 集合对象保存的元素数量不超过512个。
    不能满足这两个条件的集合对象需要使用hashtable编码。

    有序集合对象

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

    类型检查与命令多态

    redis中用于操作键的命令基本上可以分为两种类型。
    其中一种命令可以对任何类型的键执行,比如说del、expire、rename、type、object命令等。
    还有一种命令只能对特定类型的键执行,比如说:
  • set、get、append、strlen等命令只能对字符串键执行。
  • hdel、hset、hget、hlen等只能对哈希键执行。
  • rpush、lpop、linsert、llen等只能对列表键执行。
  • sadd、spop、sinter、scard等只能对集合键执行。
  • zadd、zcard、zrank、zscore等只能对有序集合键执行。

del、expire等命令和llen等命令的区别在于,前者是基于类型的多态–一个命令可以同时用于处理多种不同类型的键。而后者是基于编码的多态–一个命令可以同时用于处理多种不同的编码。

内存回收

因为C语言并不具备自动内存回收功能,所以redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制。

  • 创建一个新对象时,计数初始化1
  • 对象被新程序使用时,计数值+1
  • 对象不再被一个程序使用时,计数值-1
  • 对象计数值为0时,对象所占用的内存会被释放。

对象共享

目前来说,redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。
为什么不共享字符串

对象的空转时长

object idletime命令可以打印出给定键的空转时长,这一空转时长就是通过将当前时间减去键的值对象的lru(该属性记录了对象最后一次被访问的时间)时间计算得出的。

重点回顾

  • redis数据库中的每个键值对的键和值都是一个对象
  • redis共有字符串、列表、哈希、集合、有序集合五种类型的对象,每种类型的对象至少都有两种或以上的编码方式,不同的编码可以在不同的使用场景上优化对象的使用效率。
  • 服务器在执行某些命令之前,会先检查给定键的类型能否执行指定的命令,而检查一个键的类型就是检查键的值对象的类型。
  • redis的对象系统带有引用计数实现的内存回收机制,当一个对象不再被使用时,该对象所占用的内存就会被自动释放。
  • redis会共享值为0到9999的字符串对象。
  • 对象会记录自己的最后一次被访问的时间。这个时间可以用于计算对象的空转时间。

第9章 数据库

服务器中的数据库

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

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

通过expire命令或者pexpire命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL)。
设置过期时间命令:expire(秒)、pexpire(毫秒)、expireat(秒时间戳)、pexpireat(毫秒时间戳),
虽然有多种不同单位和不同形式的设置命令,但实际上expire、pexpire、expireat三个命令都是使用pexpireat命令来实现的:无论客户端执行的是以上四个命令的哪一个,经过转换之后,最终都会转换为pexpireat命令执行。
ttl(返回:秒)、pttl(返回:毫秒)命令接受一个带有生存时间或者过期时间的键,返回这个键的剩余生存时间。也就是距离这个键被删除还有多长时间。
移除过期时间命令:persist,它会解除键和值(过期时间)在过期字典中的关联。

redis的过期键删除策略

redis服务器实际使用的是惰性删除和定期删除两种策略,通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。

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

AOF、RDB都是redis数据持久化的方式。

RDB

RDB,简而言之,就是在不同的时间点,将redis存储的数据生成快照并存储到磁盘等介质上。
在执行save命令或者bgsave命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中

AOF

AOF,则是换了一个角度来实现持久化,那就是将redis执行过的所有写执行记录下来,在下次redis重新启动时,只要把这些写指令从前到后在重复执行一遍,就可以实现数据恢复了。

复制

当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:

  • 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键。
  • 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
  • 从服务器只有在接到主服务器发来的DEL命令之后,才会删除过期键。

重点回顾

  • redis服务器的所有数据库都保存在redisServer.db数组中,而数据库的数量则是由redisServer.dbnum属性保存。
  • 客户端通过修改目标数据库指针,让它指向redisServer.db数组中的不同元素来切换不同的数据库。
  • 数据库主要由dict和expires两个字典构成,其中dict字典负责保存键值对,而expires字典则负责保存键的过期时间。
  • 因为数据库由字典构成,所以对数据库的操作都是建立在字典操作之上的。
  • 数据库的键总是一个字符串对象,而值则可以是任意一种redis对象类型,包括字符串对象、哈希表对象、集合对象、列表对象和有序集合对象,分别对应字符串键、哈希表键、集合键、列表键和有序集合键。
  • expires字典的键指向数据库中的某个键,而值则记录了数据库键的过期时间,过期时间是一个以毫秒为单位的unix时间戳。
  • redis使用惰性删除和定时删除两种策略来删除过期的键:惰性删除策略只在碰到过期键时才进行删除操作,定期删除策略则每隔一段时间主动查找并删除过期键。
  • 执行save命令或者bgsave命令所产生的新RDB文件不会包含已经过期的键。
  • 执行bgrewriteaof命令所产生的重写AOF文件不会包含已经过期的键。
  • 当一个过期键被删除之后,服务器会追加一条DEL命令到现有AOF文件的末尾,显式地删除过期键。
  • 当主服务器删除一个过期键之后,它会向所有从服务器发送一条DEL命令,显式地删除过期键。
  • 从服务器即使发现过期键也不会自作主张的删除它,而是等待主节点发来DEL命令。这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。
  • 当redis命令对数据库进行修改之后,服务器会根据配置向客户端发送数据库通知。

第10章 RDB持久化

RDB文件的创建与载入

有两个redis命令可以用于生成RDB文件,一个save,另一个bgsave
save命令会阻塞redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
bgsave命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。
RDB文件的载入是在服务器启动时自动执行的,只要redis检测到RDB文件存在就会自动载入RDB文件。

自动间隔性保存

用户可以通过save选项设置多个保存条件,但只要其中人一个条件被满足,服务器就会自动执行bgsave命令保存。如:
save 900 1 (900秒内,至少1次修改)
save 300 10 (300秒内,至少10次修改)
save 60 10000 (60秒内,至少10000次修改)
redis是通过dirty计数器(修改次数)和lastsave(上次保存的时间)属性来检查保存条件是否被满足。

重点回顾

  • RDB文件用于保存和还原redis服务器所有数据库中的所有键值对数据。
  • save命令有服务器进程直接执行保存操作,所以该命令会阻塞服务器。
  • bgsave命令由子进程执行保存操作,所以该命令不会阻塞服务器。
  • 服务器状态中会保存所有用save选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行bgsave命令。
  • RDB文件是一个经过压缩的二进制文件,由多个部分组成。
  • 对于不同类型的键值对,RDB文件会使用不同的方式来保存它们。

第11章 AOF持久化

AOF持久化是通过保存redis服务器所执行的写命令来记录数据库状态的。

AOF持久化的实现

AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤

AOF重写

因为AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝,AOF文件中的内容会越来越多,文件的体积也会越来越大。
如果不加以控制很可能对redis服务器、甚至整个宿主计算机造成影响,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。
AOF重写是通过读取数据库内容,生成对应的存命令写入AOF文件中。最后替换掉原有的AOF文件实现AOF重写。

AOF后台重写

redis把AOF重写程序放到子进程里执行。可以达到服务器进程(父进程)可以继续处理命令请求。
但是还有一个问题需要解决,因为子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新命令可能对现有的数据库状态进行修改,从而使得当前数据库与重写的AOF文件所保存的数据库不一致。
为了解决这个问题,redis服务器设置了一个AOF重写缓冲区,当redis服务器执行完一个写命令之后,他会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区。
这样当子进程完成AOF重写后,它会向父进程发送一个信号,父进程在接收到这个信号后,将AOF重写缓冲区中的所有内容写入到新AOF文件中,实现了数据库状态一致。在对新AOF文件进行改名,覆盖现有的AOF文件,实现替换。

重点回顾

  • AOF文件通过保存所有修改数据库写命令请求来记录服务器的数据库状态。
  • AOF文件中的所有命令都以redis命令请求协议的格式保存。
  • 命令请求会先保存到AOF缓冲区里面,之后再定期写入并同步到AOF文件。
  • appendfsync选项的不同值对AOF持久化功能的安全性以及redis服务器的性能有很大的影响。
  • 服务器只要载入并重新执行保存在AOF文件中的命令,就可以还原数据库本来的状态。
  • AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
  • AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读入,分析或者写入操作。
  • 在执行bgrewriteaof命令时,redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。

当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,是的新旧两个AOF文件所保存的数据库状态一致。
最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作。

第12章 事件

redis服务器是一个事件驱动程序,服务器需要处理两类事件:文件事件、时间事件。
文件事件:redis服务器通过套接字与客户端(或者其他redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
时间事件:redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。

重点回顾

  • redis服务器是一个时间驱动程序,服务器处理的事件分为时间事件和文件事件两类。
  • 文件事件处理器是基础Reactor模式实现的网络通信程序。
  • 文件事件是对套接字操作的抽象:每次套接字变为可应答(acceptable)、可写(writable)或者可读(readable)时,相应的文件事件就会产生。
  • 文件事件分为AE_READABLE事件(读事件)和AE_WRITABLE事件(写事件)两类。
  • 时间事件分为定时事件和周期事件:定时事件只在指定的时间到达一次,而周期性事件则每隔一段时间到达一次。
  • 服务器在一般情况下只执行serverCron函数一个时间事件,并且这个事件是周期性事件。
  • 文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件的过程中也不会进行抢占。
  • 时间事件的实际处理时间通常会比设定的到达时间晚一些。

第13章 客户端

执行client list命令可以列出目前所有连接到服务器的不同客户端,命令输出中的fd域显示了服务器连接客户端所使用的套接字描述符。

重点回顾

  • 服务器状态结构使用clients链表连接起多个客户端状态,新添加的客户端状态会被放到链表的末尾。
  • 客户端状态的flags属性使用不同标志来表示客户端的角色,以及客户端当前所处的状态。
  • 输入缓冲区记录了客户端发送的命令请求,这个缓冲区的大小不能超过1GB。
  • 命令的参数和参数个数会被记录在客户端状态的argv和argc属性里面,而cmd属性则记录了客户端要执行命令的实现函数。
  • 客户端有固定大小缓冲区和可变大小缓冲区两种缓冲区可用,其中固定大小缓冲区的最大大小为16KB,而可变大小缓冲区的最大大小不能超过服务器设置的硬性限制值。
  • 输出缓冲区限制值有两种,如果输出缓冲区的大小超过了服务器设置的硬性限制,那么客户端会被立即关闭;除此之外,如果客户端在一定时间内,一直超过服务器设置的软性限制,那么客户端也会被关闭。
  • 当一个客户端通过网络连接连上服务器时,服务器会为这个客户端创建相应的客户端状态。网络连接关闭、发送了不合协议格式的命令请求,成为client kill命令的目标、空转时间超时、输出缓冲区的大小超出限制,以上这些原因都会造成客户端被关闭。
  • 处理Lua脚本的伪客户端在服务器初始化时创建,这个客户端会一直存在,直到服务器关闭。
  • 载入AOF文件时使用的伪客户端在载入工作开始时动态创建,载入工作完毕之后关闭。

第14章 服务器

命名请求的执行过程

  • 发送命令请求
  • 读取命令请求
    读取命令请求调用命令执行器执行如下步骤:
    1)查找命令实现
    2)执行预备操作
    3)调用命令的实现函数
    4)执行后续工作

重点回顾

  • 一个命令请求从发送到完成主要包括以下步骤:
    1)客户端命令请求发送给服务器;
    2)服务器读取命令请求,并分析出命令参数;
    3)命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;
    4)服务器将命令回复返回给客户端;
  • serverCron函数默认每隔100毫秒执行一次,它的工作主要包括更新服务器状态信息,处理服务器接收的sigterm信号,管理客户端资源和数据库状态,检查并执行持久化操作等等。
  • 服务器从启动到能够处理客户端的命令请求需要执行以下步骤:
    1)初始化服务器状态;
    2)载入服务器配置;
    3)初始化服务器数据结构;
    4)还原数据库状态;
    5)执行时间循环;

第15~17章

由于这3章讲的是多机数据库的实现,我还暂时没使用到过,所以先放一放。之后再来研究研究。

第18章 发布与订阅

重点回顾

  • 服务器状态在pubsub_channels字典保存了所有频道的订阅关系:
  1. subscribe命令负责将客户端和被订阅的频道关联到这个字典里面,
  2. unsubscribe命令则负责解除客户端和被退订频道之间的关联。
  • 服务器状态在pubsub_patterns链表保存了所有模式的订阅关系:
  1. psubscribe命令负责将客户端和被订阅的模式记录到这个链表中。
  2. punsubscribe命令则负责移除客户端和被退订模式在链表中的记录。
  • publish命令通过访问pubsub_channels字典来向频道的所有订阅者发送消息,通过访问pubsub_patterns链表来向所有匹配频道的模式的订阅者发送消息。
  • pubsub命令的三个子命令都是通过读取pubsub_channels字典和pubsub_patterns链表中的信息来实现的。
  1. pubsub channels pattern pattern参数是可选的,来返回服务器目前被订阅的频道
  2. pubsub numsub子命令可以接受任意多个频道作为输入参数,并返回这些频道的订阅数量
  3. pubsub numpat子命令用于返回服务器当前被订阅模式的数量。

第19章 事务

事务的实现

一个事务从开始到结束通常会经历以下三个阶段:
1)事务开始。
2)命令入队。
3)事务执行。

WATCH命令

watch命令是一个乐观锁,他可以在exec命令执行之前,监视任意数量的数据库键,并在exec命令执行时,检查被监视的键是否至少有一个已经被修改过了,
如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
当服务器接收到一个客户端发来的EXEC命令时,服务器会根据这个客户端是否打开了redis_dirty_cas标识来决定是否执行事务。

事务的ACID性质

在传统的关系式数据库中,常常用ACID性质来检验事务功能的可靠性和安全性。
在redis中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),
并且当redis运行在某种特定的持久化模式下时,事务也具有耐久性(Durability)。

重点回顾

  • 事务提供了一种将多个命令打包,然后一次性、有序地执行的机制。
  • 多个命令会被入队到事务队列中,然后按先进先出(FIFO)的顺序执行。
  • 事务在执行过程中不会被中断,当事务队列中的所有命令都被执行完毕之后,事务才会结束。
  • 带有watch命令的事务会将客户端和被监视的键在数据库的watched_keys字典中进行关联,当键被修改时,程序会将所有监视被修改键的客户端的redis_dirty_cas标志打开。
  • 只有在客户端的redis_dirty_cas标志未被打开时,服务器才会执行客户端提交的事务,否则的话,服务器将拒绝执行客户端提交的事务。
  • redis的事务总是具有ACID中的原子性、一致性和隔离性,当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。

第20章 Lua脚本

  • redis服务器在启动时,会对内嵌的Lua环境执行一系列修改操作,从而确保内嵌的Lua环境可以满足redis在功能性、安全性等方面的需要。
  • redis服务器专门使用一个伪客户端来执行Lua脚本中包含的redis命令。
  • redis使用脚本字典来保存所有被eval命令执行过,或者被script load命令载入过的Lua脚本,这些脚本可以用于实现script exists命令,以及实现脚本复制功能。
  • eval命令为客户端输入的脚本在Lua环境中定义一个函数,并通过调用这个函数来执行脚本。
  • evalsha命令通过直接调用Lua环境中已定义的函数来执行脚本。
  • script flush命令会清空服务器lua_scripts字典中保存的脚本,并重置Lua环境。
  • script exists命令接受一个或多个sha1校验和为参数,并通过检查lua_scripts字典来确认校验和对应的脚本是否存在。
  • script load命令接受一个Lua脚本为参数,为该脚本在Lua环境中创建函数,并将脚本保存到lua_scripts字典中。
  • 服务器在执行脚本之前,会为Lua环境设置一个超时处理钩子,当脚本出现超时运行情况时,客户端可以通过向服务器发送script kill命令来让钩子停止正在执行的脚本,或者发送shutdown nosave命令来让钩子关闭整个服务器。
  • 主服务器复制eval、script flush、script load三个命令的方法和复制普通redis命令一样,只要将相同的命令传播给从服务器就可以了。
  • 主服务器在复制evalsha命令时,必须确保所有从服务器都已经载入了evalsha命令指定的sha1校验和所对应的Lua脚本,如果不能确保这一点的话,主服务器会将evalsha命令转换成等效的eval命令,并通过传播eval命令来获得相同的脚本执行效果。

第21章 排序

  • sort命令通过将被排序键包含的元素载入到数组里面,然后对数组进行排序来完成对键进行排序的工作。
  • 在默认情况下,sort命令假设被排序键包含的都是数字值,并且以数字值的方式来进行排序。
  • 如果sort命令使用了alpha选项,那么sort命令假设被排序键包含的都是字符串值,并且以字符串的方式来进行排序。
  • sort命令的排序操作由快速排序算法实现。
  • sort命令会根据用户是否使用了desc选项来决定是使用升序对比还是降序对比来比较被排序的元素,升序对比会按值的大小从小到大排列,降序对比会按值的大小从大到小排列。
  • 当sort命令使用了by选项时,命令使用其他键的值作为权重来进行排序操作的。
  • 当sort命令使用了limit选项时,命令只保留排序结果集中limit选项指定的元素。
  • 当sort命令使用get选项时,命令会根据排序结构集中的元素,以及get选项给定的模式,查找并返回其他键的值,而不是返回被排序的元素。
  • 当sort命令使用了store选项时,命令会将排序结果集保存在指定的键里面。
  • 当sort命令同时使用多个选项时,命令先执行排序操作(可用的选项为alpha、asc或desc、by),然后执行limit选项,之后执行get选项,再之后执行store选项,最后才将排序结果集返回给客户端。
  • 除了get选项之外,调整选项的摆放位置不会影响sort命令的排序结果。

第22章 二进制位数组

redis提供了setbit、getbit、bitcount、bitop四个命令用于处理二进制位数组(bit array,又称“位数组”)。
setbit命令用于为位数组指定偏移量上的二进制位设置值,位数组的偏移量从0开始计数,而二进制位的值则可以是0或者1.
getbit命令则用于获取位数组指定偏移量上的二进制位的值
bitcount命令用于统计位数组里面,值为1的二进制位的数量
bitop命令既可以对多个位数组进行按位与(and)、按位或(or)、按位异或(xor)运算

重点回顾

  • redis使用SDS来保存位数组。
  • SDS使用逆序来保存位数组,这种保存顺序简化了setbit命令的实现,使得setbit命令可以在不移动现有二进制位的情况下,对位数组进行空间扩展。
  • bitcount命令使用了查表算法和variable-precision SWAR算法来优化命令的执行效率。
  • bitop命令的所有操作都使用C语言内置的位操作来实现。

第23章 慢查询日志

redis的慢查询日志功能用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志来监视和优化查询速度。

重点回顾

  • redis的慢查询日志功能用于记录执行时间超过指定时长的命令。
  • redis服务器将所有的慢查询日志保存在服务器状态的slowlog链表中,每个链表节点都包含一个slowlogEntry结构,每个slowlogEntry结构代表一条慢查询日志。
  • 打印和删除慢查询日志可以通过遍历slowlog链表来完成。
  • slowlog链表的长度就是服务器所保存慢查询日志的数量。
  • 新的慢查询日志会被添加到slowlog链表的表头,如果日志的数量超过slowlog-max-len选项的值,那么多出来的日志会被删除。

第24章 监视器

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

重点回顾

  • 客户端可以通过执行monitor命令,将客户端转换成监视器,接收并打印服务器处理的每个命令请求的相关信息。
  • 当一个客户端从普通客户端变为监视器时,该客户端的redis_monitor标识会被打开。
  • 服务器将所有监视器都记录在monitors链表中。
  • 每次处理命令请求时,服务器都会遍历monitors链表,将相关信息发送给监视器。

文章作者: heixinxin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 heixinxin !
评论
  目录