《redis 设计与实现》读书笔记

大家好,我是烤鸭:
    《redis 设计与实现》,读书笔记。

第一部分 数据结构与对象

第2章 简单动态字符串

Redis 使用SDS 作为字符串表示。

O(1) 复杂度获取字符串长度。

杜绝缓冲区溢出。

减少修改字符串长度时所需的内存重分配次数。

二进制安全。

兼容部分C字符串函数。

第3章 链表

每个链表节点由一个listNode结构标识,每个节点都有一个指向前置接点和后置节点的指针,实现是双端链表。

每个链表用list结构标识,结构带有表头节点指针、表尾节点指针,链表长度等。

第4章 字典

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

哈希表使用链地址法来解决键冲突,被分配到同一个索引上的多个键值对会连接成一个单向链表。

对哈希表进行扩展或者收缩操作时,程序需要现有哈希表包含的现有键值对rehash到新哈希表里面,rehash过程是渐进式的。

dict中的rehashidx( -1 表示不在进行rehash)

rehash的index = hash & ht[0].size - 1

哈希表扩展操作的条件:

  • 没有执行BGSAVE或者BGREWRITEAOF命令,哈希表的负载因子大于等于1。
  • 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子(ht[0].used/ht[0].size)大于等于5。

第5章 跳跃表

跳表效率跟平衡树差不多,实现更简单,平均O(logN),最坏O(N)复杂度处理节点。

每次创建新跳表节点,随机生成1-32之间的一个数作为层高。

Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而

zskiplistNode则用于表示跳跃表节点。

跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。

第6章 整数集合

整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证

集合中不会出现重复元素,底层实现是数组。

新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级。

不支持降级。

第7章 压缩列表

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

压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可

以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。

ziplistPush等命令的平均复杂度仅为O(N),最坏复杂度为O(N2)。

第8章 对象

Redis使用对象来表示数据库中的键和值,叫做键对象和值对象。

类型有 字符串、列表、哈希、集合、有序集合。

字符串编码:int、raw或者embstr。

  • int编码使用整数集合作为底层实现。
  • 释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数。

列表的编码:ziplist或者linkedlist。

  • ziplist编码使用压缩列表作为底层实现,每个压缩列表节点(entry)保存了一个列表元素
  • linkedlist编码使用双端链表作为底层实现。

哈希的编码:ziplist或者hashtable(使用字典作为底层实现)。

集合的编码:intset或者hashtable。

有序集合的编码:ziplist或者skiplist。有序集合每个元素的成员都是一个字符串对象,而每个元素的分值都是一个double类型的浮点数。

相同对象类型各个编码之间会转换,字符串对象是Redis五种类型的对象中唯一一种会被其他四种类型对象嵌套的对象。

服务器在执行某些命令之前,会先检查给定键的类型能否执行指定的命令,而检查一个键的类型就是检查键的值对象的类型。

Redis的对象系统带有引用计数实现的内存回收机制,当一个对象不再被使用时,该对象所占用的内存就会被自动释放。

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

对象会记录自己的最后一次被访问的时间,这个时间可以用于计算对象的空转时间。(LRU的实现)

第二部分

第9章 数据库

redis默认创建16个数据库,客户端默认是0号数据库。

数据库主要由dict和expires两个字典构成,其中dict字典负责保存键值对,而expires字典则负责保存键的过期时间。

Redis使用惰性删除和定期删除两种策略来删除过期的键:惰性删除策略只在碰到过期键时才进行删除操作,定期删除策略则每隔一段

时间主动查找并删除过期键。

执行SAVE命令或者BGSAVE命令所产生的新RDB文件和执行BGREWRITEAOF命令所产生的重写AOF文件不会包含已经过期的键。

当一个过期键被删除之后,服务器会追加一条DEL命令到现有AOF文件的末尾,显式地删除过期键。

当主服务器删除一个过期键之后,它会向所有从服务器发送一条DEL命令,显式地删除过期键。

从服务器即使发现过期键也不会自作主张地删除它,而是等待主节点发来DEL命令,这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。

当Redis命令对数据库进行修改之后,服务器会根据配置向客户端发送数据库通知。

第10章 RDB持久化

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

SAVE命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器,BGSAVE令由子进程执行保存操作,所以该命令不会阻塞服务器。

服务器状态中会保存所有用save选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行BGSAVE命令。

第11章 AOF持久化

AOF文件通过保存所有修改数据库的写命令请求来记录服务器的数据库状态。

命令请求会先保存到AOF缓冲区里面,之后再定期写入并同步到AOF文件。只要载入并重新执行保存在AOF文件中的命令,就可以还原数据库本来的状态。

AOF持久化的效率和安全性:

  • 当appendfsync的值为always时,效率最慢最安全,宕机只会丢失一个事件循环中所产生的命令数据。
  • 当appendfsync的值为everysec时,宕机只会丢失一秒中所产生的命令数据。
  • 当appendfsync的值为no时,效率最高最不安全,同步由操作系统控制,宕机会丢失上次同步之后所产生的命令数据。

在执行BGREWRITEAOF命令时,Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作。

第12章 事件

Redis服务器是一个事件驱动程序,服务器处理的事件分为时间事件和文件事件两类。虽然文件事件处理器以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字。

文件事件处理器基于Reactor模式实现的网络通信程序,由四个组成部分,它们分别是套接字、I/O多路复用程序、文件事件分派器(dispatcher),以及事件处理器。

文件事件分为AE_READABLE事件(读事件)和AE_WRITABLE事件(写事件)两类。

时间事件分为定时事件和周期性事件:定时事件只在指定的时间到达一次,而周期性事件则每隔一段时间到达一次。

文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件的过程中也不会进行抢占。(对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占,因此,不管是文件事件的处理器,还是时间事件的处理器,它们都会尽可地减少程序的阻塞时间,并在有需要时主动让出执行权,从而降低造成事件饥饿的可能性。比如说,在命令回复处理器将一个命令回复写入到客户端套接字时,如果写入字节数超过了一个预设常量的话,命令回复处理器就会主动用break跳出写入循环,将余下的数据留到下次再写;另外,时间事件也会将非常耗时的持久化操作放到子线程或者子进程执行。)

第13章 客户端

服务器状态结构使用clients链表连接起多个客户端状态,新添加的客户端状态会被放到链表的末尾。

输入缓冲区记录了客户端发送的命令请求,这个缓冲区的大小不能超过1GB。

命令的参数和参数个数会被记录在客户端状态的argv和argc属性里面,而cmd属性则记录了客户端要执行命令的实现函数。

客户端的输出缓冲区有固定大小缓冲区和可变大小缓冲区两种缓冲区可用,其中固定大小缓冲区的最大大小为16KB,而可变大小缓冲区的最大大小不能超过服务器设置的硬性限制值。

当一个客户端通过网络连接连上服务器时,服务器会为这个客户端创建相应的客户端状态。网络连接关闭、发送了不合协议格式的命令请求、成为CLIENT KILL命令的目标、空转时间超时、输出缓冲区的大小超出限制(硬性限制和软性限制),以上这些原因都会造成客户端被关闭。

处理Lua脚本的伪客户端在服务器初始化时创建,这个客户端会一直存在,直到服务器关闭。

载入AOF文件时使用的伪客户端在载入工作开始时动态创建,载入工作完毕之后关闭。

第14章 服务器

一个命令请求从发送到完成主要包括以下步骤:1)客户端将命令请求发送给服务器;2)服务器读取命令请求,并分析出命令参数;3)命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;4)服务器将命令回复返回给客户端。

serverCron函数默认每隔100毫秒执行一次,它的工作主要包括更新服务器状态信息,处理服务器接收的SIGTERM信号,管理客户端资源和数据库状态,检查并执行持久化操作等等。

服务器从启动到能够处理客户端的命令请求需要执行以下步骤:1)初始化服务器状态;2)载入服务器配置;3)初始化服务器数据结构;4)还原数据库状态;5)执行事件循环。

第三部分

第15章 复制

部分重同步可以解决断线重新同步的问题,是通过复制偏移量、复制积压缓冲区、服务器运行ID三个部分来实现。

在复制操作刚开始的时候,从服务器会成为主服务器的客户端,并通过向主服务器发送命令请求来执行复制步骤,而在复制操作的后期,主从服务器会互相成为对方的客户端。

主服务器通过向从服务器传播命令来更新从服务器的状态,保持主从服务器一致,而从服务器则通过向主服务器发送命令来进行心跳检测,以及命令丢失检测。

第16章 Sentinel

Sentinel哨兵模式,高可用的解决方案之一,由一个或多个Sentinel实例(instance)组成的Sentinel系统(system)可以监视任意多个主从服务器。在主节点下线情况下,自动将从节点升级。

Sentinel通过向主服务器发送INFO命令来获得主服务器属下所有从服务器的地址信息,并为这些从服务器创建相应的实例结构,以及连向这些从服务器的命令连接和订阅连接。

Sentinel默认以每十秒一次的频率向被监视的主服务器和从服务器发送INFO命令,当主服务器处于下线状态,或者Sentinel正在对主服务器进行故障转移操作时,Sentinel向从服务器发送INFO命令的频率会改为每秒一次。

Sentinel只会与主服务器和从服务器创建命令连接和订阅连接,Sentinel与Sentinel之间则只创建命令连接。

Sentinel以每秒一次的频率向实例发送ping做心跳检测。

当Sentinel将一个主服务器判断为主观下线时,它会向同样监视这个主服务器的其他Sentinel进行询问,看它们是否同意这个主服务器已经进入主观下线状态。

当Sentinel收集到足够多的主观下线投票之后,它会将主服务器判断为客观下线,并发起一次针对主服务器的故障转移操作。

第17章 集群

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

节点通过握手来将其他节点添加到自己所处的集群当中。

集群中的16384个槽可以分别指派给集群中的各个节点,每个节点都会记录哪些槽指派给了自己,而哪些槽又被指派给了其他节点。

CRC16(key)语句用于计算键key的CRC-16校验和,而&16383语句则用于计算出一个介于0至16383之间的整数作为键key的槽号。

def slot_number(key):
	return CRC16(key) & 16383

节点在接到一个命令请求时,会先检查这个命令请求要处理的键所在的槽是否由自己负责,如果不是的话,节点将向客户端返回一个MOVED错误,MOVED错误携带的信息可以指引客户端转向至正在负责相关槽的节点。

重新分片由redis-trib或者redis-cli负责执行。

# 5.0 之前
redis-trib.rb create --replicas
# 5.0 之后
redis-cli -a redis-pw --cluster create --cluster-replicass

迁移过程中键被查找,返回 ASK 指引到相关节点。

集群里的从节点用于复制主节点,并在主节点下线时,代替主节点继续处理命令请求。

集群中的节点通过发送和接收消息来进行通信,常见的消息包括MEET、PING、PONG、PUBLISH、FAIL五种。

第四部分

第18章 发布与订阅

频道订阅和模式订阅:

  • 服务器状态在pubsub_channels字典保存了所有频道的订阅关系:SUBSCRIBE命令负责将客户端和被订阅的频道关联到这个字典里面,而UNSUBSCRIBE命令则负责解除客户端和被退订频道之间的关联。

  • 服务器状态在pubsub_patterns链表保存了所有模式的订阅关系:PSUBSCRIBE命令负责将客户端和被订阅的模式记录到这个链表中,而PUNSUBSCRIBE命令则负责移除客户端和被退订模式在链表中的记录。

PUBLISH命令通过访问pubsub_channels字典来向频道的所有订阅者发送消息,通过访问pubsub_patterns链表来向所有匹配频道的模式的订阅者发送消息。

PUBSUB命令的三个子命令都是通过读取pubsub_channels字典和pubsub_patterns链表中的信息来实现的。

ps:一般流量不大的可以使用redis的发布订阅做IM,比如聊天室。

第19章 事务

redis的事务是提供打包执行命令,先进先出(FIFO)顺序执行。

事务在执行过程中不会被中断,当事务队列中的所有命令都被执行完毕之后,事务才会结束。

带有WATCH命令的事务会将客户端和被监视的键在数据库的watched_keys字典中进行关联,当键被修改时,程序会将所有监视被修改键的客户端的REDIS_DIRTY_CAS标志打开,此时服务器将拒绝执行客户端提交的事务(乐观锁CAS的方式)。

Redis的事务总是具有ACID中的原子性、一致性和隔离性,当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。(如果其中某一条命令出错,其他命令会继续执行,这个跟DB有点差异,redis作者认为这种场景生产环境很少出现且和redis高效的设计理念不相符,所以没有回滚功能)

第20章 Lua脚本

Redis服务器在启动时,会对内嵌的Lua环境,使用一个伪客户端来执行Lua脚本中包含的Redis命令。

命令相关:

  • 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命令来让钩子关闭整个服务器。

主服务器在复制EVALSHA命令时,如果从服务器返回脚本未找到,主服务器会将EVALSHA命令转换成等效的EVAL命令,并通过传播EVAL命令来获得相同的脚本执行效果。

第21章 排序

Redis的SORT命令可以对列表键、集合键或者有序集合键的值进行排序。通过将被排序键包含的元素载入到数组里面,然后对数组进行排序来完成对键进行排序的工作。

在默认情况下,排序键是数字值,如果使用了ALPHA选项,排序键是字符串。SORT命令的排序操作由快速排序算法实现。默认升序,DESC降序。

redis> RPUSH numbers 3 2 1 4 5
redis> SORT numbers DESC

当SORT命令使用了BY选项时,命令使用其他键的值作为权重来进行排序操作。

redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> SORT fruits ALPHA
1) "apple"
2) "banana"
3) "cherry"

当SORT命令使用了LIMIT选项时,命令只保留排序结果集中LIMIT选项指定的元素。

redis> SADD alphabet a b c d e f
(integer) 6
#集合中的元素是乱序存放的
redis> SMEMBERS alphabet
1) "d"
2) "c"
3) "a"
4) "b"
5) "f"
6) "e"
#对集合进行排序,并返回所有排序后的limit元素
redis> SORT alphabet ALPHA LIMIT 0,4
1) "a"
2) "b"
3) "c"
4) "d"

当SORT命令使用了GET选项时,命令会根据排序结果集中的元素,以及GET选项给定的模式,查找并返回其他键的值,而不是返回被排序的元素。

#设置peter、jack、tom的全名
redis> SET peter-name "Peter White"
OK
redis> SET jack-name "Jack Snow"
OK
redis> SET tom-name "Tom Smith"
OK
# SORT命令首先对students集合进行排序,得到排序结果
# 1) "jack"
# 2) "peter"
# 3) "tom"
#然后根据这些结果,获取并返回键jack-name、peter-name和tom-name的值
redis> SORT students ALPHA GET *-name
1) "Jack Snow"
2) "Peter White"
3) "Tom Smith"

当SORT命令使用了STORE选项时,命令会将排序结果集保存在指定的键里面。

当SORT命令同时使用多个选项时,命令先执行排序操作(可用的选项为ALPHA、ASC或DESC、BY),然后执行LIMIT选项,之后执行GET选项,再之后执行STORE选项,最后才将排序结果集返回给客户端。除了GET选项之外,调整选项的摆放位置不会影响SORT命令的排序结果。

第22章 二进制位数组

Redis提供了SETBIT、GETBIT、BITCOUNT、BITOP四个命令用于处理二进制位数组(bit array,又称“位数组”)。

SDS使用逆序来保存位数组,这种保存顺序简化了SETBIT命令的实现,使得SETBIT命令可以在不移动现有二进制位的情况下,对位数组进行空间扩展。(顺序的话,高位添加,需要移动原有的数组位置)

BITCOUNT命令使用了查表算法和variable-precision SWAR算法来优化命令的执行效率。(汉明重量)

  • 未处理的二进制位大于等于128位,variable-precision SWAR算法。反之查表算法。

BITOP命令的所有操作都使用C语言内置的位操作来实现。

第23章 慢查询日志

Redis的慢查询日志功能用于记录执行时间超过指定时长的命令。

  • slowlog-log-slower-than:执行时间超过多少微秒(1秒等于1000 000微秒)的命令请求会被记录到日志上。(默认 10000,10毫秒)
  • slowlog-max-len:服务器最多保存多少条慢查询日志。(默认 128)

Redis服务器将所有的慢查询日志保存在服务器状态的slowlog链表中,每个链表节点都包含一个slowlogEntry结构,每个slowlogEntry结构代表一条慢查询日志。

新的慢查询日志会被添加到slowlog链表的表头,打印和删除慢查询日志可以通过遍历slowlog链表来完成。

第24章 监视器

monitor 命令,服务器实时监控当前处理的命令。

redis> MONITOR
OK
1378822099.421623 [0 127.0.0.1:56604] "PING"
1378822105.089572 [0 127.0.0.1:56604] "SET" "msg" "hello world"

当一个客户端从普通客户端变为监视器时,该客户端的REDIS_MONITOR标识会被打开。

服务器将所有监视器都记录在monitors链表中。

每次处理命令请求时,服务器都会遍历monitors链表,将相关信息发送给监视器。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烤鸭的世界我们不懂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值