note-book-the-design-and-implementation-of-redis
本文不适合学习,偏向复习用
相关文章:Redis笔记——Redis设计与实现
数据结构与对象
看前须知
对象章节的有些内容最好提前了解下,比如以下内容:
在 Redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)
对于 Redis 数据库保存的键值对来说, 键总是一个字符串对象, 而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种
当我们称呼一个数据库键为“字符串键”时, 我们指的是“这个数据库键所对应的值为字符串对象”
当我们称呼一个键为“列表键”时, 我们指的是“这个数据库键所对应的值为列表对象”
简单动态字符串(Simple Dynamic String,SDS)
在Redis中,有两种字符串,一种是传统的C字符串,通常仅用于无需修改的地方(例如日志中)。
而如果Redis需要一个可以修改的字符串,就会使用SDS(键值对的字符串都是使用SDS)
SDS定义
1 | struct sdshdr { |
常数时间复杂度获取字符串长度
SDS 遵循 C 字符串以空字符(‘\0’)结尾的惯例,空字符占用1的空间,但不会计算在len属性中
SDS获取字符串的时间复杂度是O(1),而C字符串则需要O(n)遍历
杜绝缓冲区溢出
缓冲区溢出:<string.h>/strcat 函数可以将 src 字符串中的内容拼接到 dest 字符串的末尾。C 字符串不记录自身的长度,假设程序里有两个在内存中紧邻着的 C 字符串 s1 和 s2,执行strcat(s1, s3)时,s1 的数据将溢出到 s2 所在的空间中
SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性,SDS修改前会检查空间是否满足需要,如果不满足会先进行空间拓展,然后进行修改操作
内存预分配
对于C字符串来说,每次修改都会导致内存重分配(因为每次都是占用固定空间,没有冗余空间)
而SDS的内存空间可以由未分配的字节,由free属性记录。每次SDS进行空间拓展时,不仅会给分配修改所需要的空间,还会给分配额外的未使用空间(free)。
- 如果len小于1MB,那么free和len的值相同,即已使用空间等于未使用空间
- 如果大于等于1MB,那么free的值等于1MB
SDS 的 len 将变成 13 字节, 那么程序也会分配13 字节的未使用空间, SDS 的 buf 数组的实际长度将变成 13 + 13 + 1 = 27 字节(额外的一字节用于保存空字符)。
惰性空间释放策略(或者叫不释放策略?)
sdstrim 函数接受一个 SDS 和一个 C 字符串作为参数, 从 SDS 左右两端分别移除所有在 C 字符串中出现过的字符。
SDS通过sdstrim函数释放出来的空间会被保留在free中,供以后使用。如果想要释放未使用空间,SDS也提供了相应的API。
保存二进制数据
C字符串在字符串的结尾由空字符,而在中间不能包含空字符。
而SDS可以在字符串中间包含空字符,所以SDS可以用于保存二进制数据,SDS的API都是二进制安全的,这也是SDS的buf被称为字节数组的原因
在处理二进制数据时,特别是在存储图像、音频文件或其他二进制格式的数据时,'\0’的存在是很常见的。
兼容部分C字符串函数
SDS遵循C字符串以空字符串结尾的惯例,所以可以重用部分<string.h>库函数,比如strcasecmp、strcat等
链表
用处
链表被广泛用于实现 Redis 的各种功能, 比如列表键, 发布与订阅, 慢查询, 监视器
实现
1 | typedef struct listNode { |
- 双端: 链表节点带有 prev 和 next 指针, 获取某个节点的前置节点和后置节点的复杂度都是 O(1) 。
- 无环: 表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL , 对链表的访问以 NULL 为终点。
- 带表头指针和表尾指针: 通过 list 结构的 head 指针和 tail 指针, 程序获取链表的表头节点和表尾节点的复杂度为 O(1) 。
- 带链表长度计数器: 程序使用 list 结构的 len 属性来对 list 持有的链表节点进行计数, 程序获取链表中节点数量的复杂度为 O(1)。
- 多态: 链表节点使用 void* 指针来保存节点值, 并且可以通过 list 结构的 dup 、 free 、 match 三个属性为节点值设置类型特定函数, 所以链表可以用于保存各种不同类型的值。
字典
用于保存键值对
Redis 的数据库就是使用字典来作为底层实现的
字典还是哈希键的底层实现之一: 当一个哈希键包含的键值对比较多, 又或者键值对中的元素都是比较长的字符串时, Redis 就会使用字典作为哈希键的底层实现
实现
1 | typedef struct dict { |
1 | typedef struct dictType { |
哈希算法
计算索引
1 | hash = dict->type->hashFunction(key); |
当字典被用作数据库的底层实现, 或者哈希键的底层实现时, Redis 使用 MurmurHash2 算法来计算键的哈希值。
MurmurHash 算法最初由 Austin Appleby 于 2008 年发明, 这种算法的优点在于, 即使输入的键是有规律的, 算法仍能给出一个很好的随机分布性, 并且算法的计算速度也非常快。
MurmurHash 算法目前的最新版本为 MurmurHash3 , 而 Redis 使用的是 MurmurHash2
rehash (重新散列)
随着操作的不断执行, 哈希表保存的键值对会逐渐地增多或者减少, 为了让哈希表的负载因子(load factor)维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩。
负载因子 load_factor = ht[0].used / ht[0].size
- 为字典的
ht[1]
哈希表分配空间- 拓展操作时,空间大小等于第一个大于等于的空间
- 收缩操作时,空间大小等于第一个大于等于的空间
- 将保存在
ht[0]
中的所有键值对rehash
到ht[1]
上面 - 释放
ht[0]
, 将ht[1]
设置为ht[0]
,并在ht[1]
新创建一个空白哈希表
- 扩展操作(以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行):
- 服务器目前没有在执行
BGSAVE
命令或者BGREWRITEAOF
命令, 并且哈希表的负载因子大于等于1
; - 服务器目前正在执行
BGSAVE
命令或者BGREWRITEAOF
命令, 并且哈希表的负载因子大于等于5
;
- 服务器目前没有在执行
- 收缩操作:当哈希表的负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作。
在执行 BGSAVE 命令或BGREWRITEAOF 命令的过程中, Redis 需要创建当前服务器进程的子进程, 而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率, 所以在子进程存在期间, 服务器会提高执行扩展操作所需的负载因子, 从而尽可能地避免在子进程存在期间进行哈希表扩展操作, 这可以避免不必要的内存写入操作, 最大限度地节约内存
渐进式rehash
为了避免当数据量大时一次性rehash导致停止服务的情况,rehash需要分多次,渐进式。
- 为字典的
ht[1]
哈希表分配空间 - 维持一个索引计数器变量
rehashidx
(初始值为0) rehash
过程中,将ht[0]
哈希表在rehashidx
索引上的所有键值对rehash
到ht[1]
,完成映射rehashidx
值加一- 最后,所有键值对被映射到
ht[1]
,这是rehashidx
的值设为-1
,完成rehash
操作
在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。
新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作
跳跃表
有序的链式结构,在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的
跳跃表中的节点按照分值大小进行排序, 当分值相同时, 节点按照成员对象的大小进行排序
在大部分情况下, 跳跃表的效率可以和平衡树相媲美, 并且因为跳跃表的实现比平衡树要来得更为简单, 所以有不少程序都使用跳跃表来代替平衡树。
Redis 只在两个地方用到了跳跃表, 一个是实现有序集合键, 另一个是在集群节点中用作内部数据结构
实现
表头节点header类似于dummy的性质,没有具体的成员对象和分值
表头节点和其他节点的构造是一样的: 表头节点也有后退指针、分值和成员对象, 不过表头节点的这些属性都不会被用到
1 | typedef struct zskiplist { |
幂次定律(power law):越大的数出现的概率越小
整数集合
当一个集合只包含整数值元素, 并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现
实现
可以保存类型为 int16_t 、 int32_t 或者 int64_t 的整数值, 并且保证集合中不会出现重复元素
1 | typedef struct intset { |
升级策略:每当我们要将一个新元素添加到整数集合里面, 并且新元素的类型比整数集合现有所有元素的类型都要长时, 整数集合需要先进行升级(upgrade),将底层数组现有的所有元素都转换成与新元素相同的类型,对底层数组进行空间重分配, 然后才能将新元素添加到整数集合里面。
因为每次向整数集合添加新元素都可能会引起升级, 而每次升级都需要对底层数组中已有的所有元素进行类型转换, 所以向整数集合添加新元素的时间复杂度为 O(N)
因为引发升级的新元素的长度总是比整数集合现有所有元素的长度都大, 所以这个新元素的值要么就大于所有现有元素, 要么就小于所有现有元素(负数)
整数集合的升级策略有两个好处, 一个是提升整数集合的灵活性, 另一个是尽可能地节约内存。
整数集合不支持降级操作, 一旦对数组进行了升级, 编码就会一直保持升级后的状态。
压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现之一。
zlbytes | zltail | zllen | entry1 | entry2 | … | entryN | zlend |
---|
- zlbytes:记录整个压缩列表占用的字节数
- zltail:记录压缩俩表尾节点(entryN)的距离(通过zltail就可以直接得到表尾节点的地址)
- zzlen:记录节点数量
- entryX:列表节点
- zlend:压缩列表的末端标记,是特殊值0xFF
节点
每个压缩列表节点可以保存一个字节数组或者一个整数值
- 字节数组
- 长度小于等于 63 (2^6-1)
- 长度小于等于 16383 (2^14-1)
- 长度小于等于 4294967295 (2^32-1)
- 整数值
- 4bit(0-12的无符号整数)
- 1byte的有符号整数
- 3byte的有符号整数
- int16_t
- int32_t
- int64_t
previous_entry_length | encoding | content |
---|
- previous_entry_length:记录上一个节点的长度(通过它就可以获得上一个节点的地址,从而实现从尾到头的遍历)
- 如果前一节点的长度小于 254 字节, 那么 previous_entry_length 属性的长度为 1 字节: 前一节点的长度就保存在这一个字节里面
- 如果前一节点的长度大于等于 254 字节, 那么 previous_entry_length 属性的长度为 5 字节: 其中属性的第一字节会被设置为 0xFE(十进制值 254), 而之后的四个字节则用于保存前一节点的长度。
- encoding:记录了节点的 content 属性所保存数据的类型以及长度,按照前两个bit进行划分
- 00bbbbbb(1byte):长度小于等于 63 字节的字节数组(长度就是bbbbbb,下面两个数组同理)
- 01bbb…b(2byte):长度小于等于 16383 字节的字节数组
- 10bbb…b(5byte): 长度小于等于 4294967295 的字节数组
- 11bbbbbb(1byte):各种类型的整数(具体类型还要按照接下来的bit继续划分)
- content:content就是上面的字节数组或者整数值
连锁更新: 每个节点的 previous_entry_length 属性都记录了前一个节点的长度。previous_entry_length用1byte或者5byte记录,如果在上一个节点插入一个大于254byte的节点,那么当前节点的previous_entry_length就可能从1byte变成5byte。导致当前节点也扩张,有可能进一步导致下一个节点也要扩张。
实际上连锁更新的情况并不多见
对象
在 Redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)
对于 Redis 数据库保存的键值对来说, 键总是一个字符串对象, 而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种
当我们称呼一个数据库键为“字符串键”时, 我们指的是“这个数据库键所对应的值为字符串对象”
当我们称呼一个键为“列表键”时, 我们指的是“这个数据库键所对应的值为列表对象”
TYPE 命令的实现方式也与此类似, 当我们对一个数据库键执行 TYPE 命令时, 命令返回的结果为数据库键对应的值对象的类型, 而不是键对象的类型
1 | typedef struct redisObject { |
- type:对象的类型
- REDIS_STRING,字符串对象
- REDIS_LIST,列表对象
- REDIS_HASH,哈希对象
- REDIS_SET,集合对象
- REDIS_ZSET,有序集合对象
汇总一个类型对应的多种编码
- REDIS_STRING,字符串对象
- REDIS_ENCODING_INT
- 要求字符串对象保存的是整数值, 并且这个整数值可以用 long 类型来表示
- ptr属性里面(将 void* 转换成 long ), 并将字符串对象的编码设置为 int
- 通过 APPEND 命令, 向一个保存整数值的字符串对象追加了一个字符串值,对象的编码就会从int变成raw
- REDIS_ENCODING_RAW
- 字符串对象保存的是一个字符串值,且长度大于 39 字节
- ptr指针指向一个SDS对象(sdshdr)
- REDIS_ENCODING_EMBSTR
- 字符串对象保存的是一个字符串值,且长度小于等于 39 字节
- 和raw一样,ptr指针指向一个SDS对象,但它的redisObject和sdshdr在空间上是连续的,所以内存分配和释放都只需要一次就可以完成(raw因为两者空间不连续,所以需要分别分配)
- Redis 没有为 embstr 编码的字符串对象编写任何相应的修改程序,所以对embstr的任意修改,都会导致对象的编码从embstr转为raw
long double类型的浮点数也是作为字符串值来保存的,按长度分为raw和embstr保存
- REDIS_ENCODING_INT
- REDIS_LIST,列表对象
- REDIS_ENCODING_ZIPLIST
- 使用压缩列表作为底层实现, 每个压缩列表节点(entry)保存了一个列表元素(可以是字符数组或者int)
- 需要满足列表对象保存的所有字符串元素的长度都小于 64 字节,并且列表对象保存的元素数量小于 512 个;否则转换为linkedlist
- REDIS_ENCODING_LINKEDLIST
- 使用双端链表作为底层实现, 每个双端链表节点(node)都保存了一个字符串对象, 而每个字符串对象都保存了一个列表元素。
- REDIS_ENCODING_ZIPLIST
- REDIS_HASH,哈希对象
- REDIS_ENCODING_ZIPLIST
- 使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾
- 保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
- 需要满足哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节,并且哈希对象保存的键值对数量小于 512 个;否则转换为hashtable来实现
- 使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾
- REDIS_ENCODING_HT
- 使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存
- REDIS_ENCODING_ZIPLIST
- REDIS_SET,集合对象
- REDIS_ENCODING_INTSET
- 使用整数集合作为底层实现, 集合对象包含的所有元素都被保存在整数集合里面
- 需要满足集合对象保存的所有元素都是整数值,并且集合对象保存的元素数量不超过 512 个;否则转换为hashtable实现
- REDIS_ENCODING_HT
- 使用字典作为底层实现, 字典的每个键都是一个字符串对象, 每个字符串对象包含了一个集合元素, 而字典的值则全部被设置为 NULL
- REDIS_ENCODING_INTSET
- REDIS_ZSET,有序集合对象(每个节点有成员member和分值score)
- REDIS_ENCODING_ZIPLIST
- 使用压缩列表作为底层实现, 每个集合元素使用两个紧挨在一起的压缩列表节点来保存, 第一个节点保存元素的成员(member), 而第二个元素则保存元素的分值(score),压缩列表的集合元素按照分值从小到大排序
文中似乎没有说这里的排序方式,如何保持有序性呢?压缩列表中的元素是按照插入顺序存储的,所以新添加的元素会按照其分数值的大小插入到合适的位置,以保持有序性。这样,即使有新元素的加入,Redis 仍然能够在压缩列表中维护有序集合的顺序。
- 需要满足有序集合保存的元素数量小于 128 个,并且有序集合保存的所有元素成员的长度都小于 64 字节;否则转换为skiplist实现
- REDIS_ENCODING_SKIPLIST
- 使用 zset 结构作为底层实现, 一个 zset 结构同时包含一个字典dict和一个跳跃表zskiplist
- 跳跃表按分值从小到大保存了所有集合元素, 每个跳跃表节点都保存了一个集合元素。通过这个跳跃表, 程序可以对有序集合进行范围型操作, 比如 ZRANK 、ZRANGE 等命令就是基于跳跃表 API 来实现的。
- 字典为有序集合创建了一个从成员到分值的映射, 字典中的每个键值对都保存了一个集合元素: 字典的键保存了元素的成员, 而字典的值则保存了元素的分值。 通过这个字典, 程序可以用 O(1) 复杂度查找给定成员的分值
- 使用 zset 结构作为底层实现, 一个 zset 结构同时包含一个字典dict和一个跳跃表zskiplist
- REDIS_ENCODING_ZIPLIST
类型检查
- SET 、 GET 、 APPEND 、 STRLEN 等命令只能对字符串键执行;
- HDEL 、 HSET 、 HGET 、 HLEN 等命令只能对哈希键执行;
- RPUSH 、 LPOP 、 LINSERT 、 LLEN 等命令只能对列表键执行;
- SADD 、 SPOP 、 SINTER 、 SCARD 等命令只能对集合键执行;
- ZADD 、 ZCARD 、 ZRANK 、 ZSCORE 等命令只能对有序集合键执行;
命令的多态性质
可以认为 LLEN 命令是多态(polymorphism)的: 只要执行 LLEN 命令的是列表键, 那么无论值对象使用的是 ziplist 编码还是 linkedlist 编码, 命令都可以正常执行。
对象共享
对象的引用计数属性还带有对象共享的作用,共享对象机制对于节约内存非常有帮助
让多个键共享同一个值对象骤:将数据库键的值指针指向一个现有的值对象;将被共享的值对象的引用计数增一。
只有在共享对象和目标对象完全相同的情况下, 程序才会将共享对象用作键的值对象, 而一个共享对象保存的值越复杂, 验证共享对象和目标对象是否相同所需的复杂度就会越高, 消耗的 CPU 时间也会越多
所以Redis只共享int字符串对象,并且共享值为 0 到 9999
单机数据库的实现
1 | struct redisDb { |
数据库
过期键删除策略
一个键过期了,什么时候会被删除呢?
- 定时删除:为每个键创建一个定时器,在键过期时立即执行删除操作。缺点:开销大
- 惰性删除:不主动检查,而是每次从键空间获取键时,检查键是否过期,过期则删除该键。缺点:如果一个键过期,并且一直没有访问到,那么将不会被删除
- 定期删除:每个一段时间,对数据库进行检查,删除过期键
实际上,Redis服务器使用惰性删除和定期删除两种策略结合
数据库通知
可以让客户端通过订阅给定的频道或者模式,获取数据库中键的变化以及数据库的命令执行情况
比如,通过SUBSCRIBE _ _keyspace@0_ _message
命令,可以获取到0号数据库中的message键的所有执行命令
又如,通过SUBSCRIBE _ _keyevent@0_ _:del
命令,可以获取0号数据库所有执行del命令的键
RDB持久化
用于备份数据
RDB文件的创建和载入
- 创建
- SAVE命令会阻塞Redis服务器进程,执行过程中,所有其它命令被阻塞
- BGSAVE命令会派生一个子进程,由子进程进行文件创建
- 执行期间,其它命令不会被阻塞,但是相关备份命令执行会有所不同
- SAVE命令和BGSAVE命令会被拒绝
- BGSAVE命令执行时,发送的BGREWRITEAOF命令会被阻塞
- BGREWRITEAOF命令执行时,发送的BGSAVE命令会被拒绝
- 载入
- 载入期间,服务器处于阻塞状态
AOF持久化的优先级比RDB高,服务器优先使用AOF
RDB文件结构
REDIS | db_version | databases | EOF | check_sum |
---|
- REDIS:RDB文件标志位,保存这5个字符,用于快速检查文件类型是否为RDB
- db_version:RDB版本号
- databases:各个数据库的键值对数据
- EOF:结束标志
- check_sum:8字节的校验和
databases保存多个数据库的数据,每个数据库的内容如下所示:
SELECTDB | db_number | key_value_pairs |
---|---|---|
数据库标志位 | 数据库号码 | 键值对数据 |
其中,key_value_paris由键值对组成,结构如下所示:
EXPIRETIME_MS | ms | TYPE | key | value |
---|---|---|---|---|
过期键标志位(如果有) | 过期时间戳(如果有) | value的类型 | 键(字符串) | 值数据 |
每种value的保存方式和RDB二进制文件的分析方法,这里不展开说了
AOF持久化
通过保存服务器执行的写命令来记录数据库状态
AOF持久化的实现
- 命令追加(append)
- AOF持久化打开时,服务器执行完一个写命令后,会将写命令追加到aof_buf缓冲区的末尾
- 写入(write)
- 将aof缓冲区的内容写入程序缓冲区
- 同步(fsync)
- 将程序缓冲区的内容写入aof文件,根据appendfsync选项的值决定同步方式
- always:将aof缓冲区所有内容写入并同步到aof文件
- everysec:将aof缓冲区所有内容写入到aof文件,如果上次aof同步时间超过1秒钟,那么将对AOF文件进行同步,这个同步由一个单独的线程负责
- no:将aof缓冲区所有内容写入到aof文件,但并不对aof文件执行同步,何时同步由操作系统决定。
- 将程序缓冲区的内容写入aof文件,根据appendfsync选项的值决定同步方式
AOF文件的载入与数据还原
- 创建一个不带网络连接的伪客户端
- 对AOF文件进行分析,并读取一条写命令
- 通过伪客户端执行被读出的写命令
- 一直重复步骤2和3直至所有写命令都被处理完
AOF重写
从数据库中读取键当前的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是AOF重写的实现原理
AOF后台重写
通过一个子进程进行AOF重写,这样重写期间,服务器就可以继续处理请求
这样有个问题,继续处理请求会有新的数据写入,导致AOF重写的数据库状态不一致
为了解决这个问题,Redis设置了一个AOF重写缓冲区,将写命令同时发送给AOF缓冲区和AOF重写缓冲区,保证了:
- AOF缓冲区的内容会定期被写入和同步到AOF文件,对现有AOF文件的处理工作会如常进行
- 从创建子进程开始,服务器执行的所有写命令也都会被记录到AOF重写缓冲区里面
当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:
- 将AOF重写缓冲区中的所有内容写入到新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致
- 对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两 个AOF文件的替换。
Redis服务重启的数据恢复流程优先考虑AOF,如果没有AOF才会使用RDB持久化文件恢复数据。如果都没有,那么直接启动Reids。
RDB恢复数据功能优于AOF,但如果数据集很大,RDB持久化就会很耗时
AOF文件以文本格式保存所有写操作命令,所以同一数据下,AOF文件通常大于RDB文件
方式 | RDB | AOF |
---|---|---|
启动优先级 | 低 | 高 |
文件体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢失(断电) | 根据策略配置 |
事件
Redis服务器需要处理以下两种事件:
- 文件事件(file event) : Redis服务器通过套接字与客户端(或者其他Redis服务器) 进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其 他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来 完成一系列网络通信操作。
- 时间事件(time event) : Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。
文件事件
Redis基于Reactor模式开发了自己的网络事件处理器:文件事件处理器,使用IO多路复用来同时监听多个套接字
- 文件事件处理器的构成
- 套接字
- 客户端对套接字执行write或close操作,套接字变得可读,产生AE_READABLE事件
- 客户端对套接字执行read操作,套接字变得可写,产生AE_WRITEABLE事件
- IO多路复用程序
- 服务器通过IO多路复用程序监听客户端的套接字,允许同时监听AE_READABLE和AE_WRITEABLE事件,优先处理AE_READABLE事件
- 文件事件分派器
- 根据IO多路复用程序传来的套接字,调用相应的事件处理器
- 事件处理器
- 连接应答处理器:对连接服务器监听套接字的客户端进行应答
- 命令请求处理器:从套接字中读入客户端发送的命令请求内容
- 命令回复处理器:将服务器执行命令后得到的回复通过套接字返回给客户端
- 套接字
时间事件
时间事件分为两类:
- 定时事件:让程序在指定的时间之后执行一次。事件处理器返回ae.h/AE_NOMORE,该事件到达一次后就会被删除,不会再到达
- 周期性时间:让程序每隔一段时间就执行一次。事件处理器返回整数值,服务器根据这个值,对时间事件的when属性更新,让事件在一段时间后再次到达
正常模式下,Redis服务器只是用serverCron这一个时间事件,serverCron函数主要工作内容包括:
- 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等
- 清理数据库中的过期键值对
- 关闭和清理连接失效的客户端
- 尝试进行AOF或RDB持久化操作
- 如果服务器是主服务器,那么对从服务器进行定期同步
- 如果处于集群模式,对集群进行定期同步和连接测试
这里顺带总结了服务器的运行主函数:
1 | def main(): |
ae.c/aeApiPll函数接受一个sys/time.h/struct timeval结构为参数,并在指定的时间内,阻塞并等待所有被aeCreateFileEvent函数设置为监听状态的套接字产生文件事件,当有至少一个事件发生,或者等待超时后,函数返回
客户端
Reids服务器时典型的一对多服务器程序,使用单线程单进程处理命令请求,并与多个客户端进行网络通信
redisClient对象和redisServer对象都保存在redis服务器中
1 | struct redisClient { |
服务器使用两种模式来限制客户端输出缓冲区的大小:
- 硬性限制(hard limit):输出缓冲区超过硬性限制的大小,立即关闭客户端
- 软性限制(soft limit):输出缓冲区超过软性限制的大小,服务器根据客户端的obuf_soft_limit_reached_time属性记录的客户端到达软性限制的时间,如果时间超过了client-output-buffer-limit选项规定的时间,才关闭客户端
服务器
命令请求的执行过程
当我们发送一条指令时,对于服务端其大体执行了如下流程:
- 客户端将命令转换成协议格式(二进制),并将其发送给服务端
- 服务端调用命令请求处理器对输入缓冲区中的命令进行解析,得到命令与参数等信息
- 调用命令对应的命令执行器执行命令
- 通过redisCommand结构查找命令
- 执行预备操作,检查cmd指针,服务器状态,客户端是否通过验证等,确保命令可以正确顺利执行
- 调用命令的实现函数
- 执行后续工作,记录慢日志,更新服务器状态,持久化,同步从服务器等操作
- 将协议格式的执行结果返回给客户端
- 客户端接受并打印命令回复给我们
1 | struct redisCommand { |
serverCron
serverCron函数在redisServer结构中相关属性:
1 | struct redisServer { |
除此之外,serverCron还有以下功能:
- 管理客户端资源
- 释放超时的客户端
- 重新分配超过一定长度的客户端缓冲区
- 管理数据库资源
- 删除过期键
- 字典rehash
- 将AOF缓冲区中的内容写入AOF文件
- 关闭输出缓冲区大小限制的客户端
初始化服务器
- 初始化服务器状态结构:设置服务器运行id, Redis 服务器的运行架构等
- 载入配置选项:Redis服务器加载用户定义的配置选项
- 初始化服务器数据结构
- 还原数据库状态:RDB和AOF的初始化
- 执行事件循环:Redis服务器的事件循环初始化,等待客户端发送命令请求
多机数据库的实现
复制
主服务器和从服务器互相建立关系后,互为对方的客户端,以此来发送消息
在Redis中,用户可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制(replicate)另一个服务器,我们称呼被复制的服务器为主服务器(master),而对主服务器进行复制的服务器则被称为从服务器(slave)。
旧版复制:SYNC
- 从服务器向主服务器发送SYNC命令。
- 收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。
- 当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。
- 主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。
在主服务器执行客户端发送的写命令时,主服务器的数据库就有可能会被修改。主服务器需要对从服务器执行命令传播操作:主服务器会将自己执行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器执行,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态
旧版复制的缺陷:如果从服务器断线,那么需要与主从服务器重连成功后,主服务器都需要再次执行BGSAVE,并将RDB文件发给从服务器,如果主服务器当前数据库保存数据量比较大,则需要频繁的IO操作(磁盘IO和网络IO)
新版复制:PSYNC
分为两种同步:
- 完全重同步:与我们刚才介绍的SYNC相同,主服务器向从服务器发送BGSAVE后的RDB文件,并将缓冲区的内容也发给从服务器
- 部分重同步:对于一些短时间的断线重连情况,从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。
- 部分重同步的实现
- 复制偏移量
- 在主从服务器复制初始时,主服务器会向从服务器发送一个复制偏移量,这个偏移量由主从服务器分别维护。在命令传播阶段,每次主服务器向从服务器传播N个字节数据时,就会将自己的复制偏移量+N,同时从服务器接收到主服务器传播来的N个字节时,从服务器也会将自己的复制偏移量+N。因此我们可以通过对比主从服务器的复制偏移量,程序可以很容易的知道主从服务器是否处于一致状态。
- 复制积压缓冲区
- 复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认大小为1MB。当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里面,也即主服务器的复制积压缓冲区里面会保存着一部分最近传播的写命令,并且复制积压缓冲区会为队列中的每个字节记录相应的复制偏移量
- 当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量(offset)发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作。如果offset偏移量之后的数据(也即是偏移量offset+1开始的数据)仍然存在于复制积压缓冲区里面,那么主服务器将对从服务器执行部分重同步操作。相反,如果offset偏移量之后的数据已经不存在于复制积压缓冲区,那么主服务器将对从服务器执行完整重同步操作。
- 服务器ID
- 每个Redis服务器,不论主服务器还是从服务器,都会有自己的运行ID,这个ID由服务器在启动时自动生成,由40个随机16进制字符组成
- 当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器则会将这个运行ID保存起来。
- 当从服务器断线并重新连上一个主服务器时,从服务器将向当前连接的主服务器发送之前保存的运行ID
- 如果从服务器保存的运行ID和当前连接的主服务器的运行ID相同,那么说明从服务器断线之前复制的就是当前连接的这个主服务器,主服务器可以继续尝试执行部分重同步操作。
- 相反地,如果从服务器保存的运行ID和当前连接的主服务器的运行ID并不相同, 那么说明从服务器断线之前复制的主服务器并不是当前连接的这个主服务器,主服务器将对从服务器执行完整重同步操作。
- 复制偏移量
复制的实现
1 | struct redisServer { |
复制的过程:
- 从服务器发送SLAVEOF命令给主服务器端后,从服务器会在自己的结构上记录主服务器的地址
- 建立套接字:根据IP和PORT建立双方的套接字
- 发送PING命令:检查通信状态
- 身份验证:主服务器验证从服务器的密码(如果配置了
- 同步:通过PSYNC进行数据同步
- 命令传播:主服务器执行了写命令,将命令传播给从服务器
心跳机制
从服务器每秒向主服务器发送:REPLCONF ACK <replication_offset>
- 检测主从服务器的网络连接状态
- 辅助实现min-slaves选项
- min-slaves-to-write 3:从服务器数量小于3,主服务器就拒绝执行写操作
- min-slaves-max-lag 10:超过3个从服务器的延迟大于3秒,主服务器就拒绝执行写操作
- 检测命令丢失:在保持连接(未断开)的情况下,从服务器也可能出现命令丢失的情况,所以通过心跳检测复制偏移量。如果不同,则主服务器发送缺失的命令给从服务器
Sentinel
Sentinel用于管理主服务器和从服务器,本质上是一个运行在特殊模式下的Redis服务器
Sentinel(哨冈、哨兵)是Redis的高可用性(high availability)解决方案:由一个或多个Sentinel 实例(instance)组成的 Sentinel系统(system)可以监视任意多个主服务器, 以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
当server1(主节点)的下线时长超过用户设定的下线时长上限时,Sentinel系统就会对 server1执行故障转移操作:
- 首先,Sentinel系统会挑选server1属下的其中一个从服务器,并将这个被选中的从服务器升级为新的主服务器。
- 之后,Sentinel系统会向server1属下的所有其他从服务器发送新的复制指令,让它们成为新的主服务器的从服务器,当所有从服务器都复制新的主服务器时,故障转移操作执行完毕。
- 另外,Sentinel还会继续监视已下线的server1,并在它重新上线时,将它设置为新的主服务器的从服务器。
启动并初始化Sentinel
启动Sentinel命令:redis-server sentinel.conf --sentinel
或 $ redis-sentinel sentinel.conf
启动过程:
- 初始化服务器:和正常Reids服务器一样需要初始化,不过不需要加载数据库文件和数据库相关命令
- 将普通Redis代码换为Sentinel专用代码
- 状态初始化:sentinelstate结构
- 初始化sentinel的监视主服务器列表:初始化sentinel结构中的masters
- 创建向主服务器的连接:Sentinel服务器作为客户端向主服务器建立连接
1 | struct sentinelstate { |
获取主服务器信息
Sentinel服务器通过发送INFO命令获取主服务器的当前信息,包括连接这个主服务器的所有从服务器信息
获取从服务器信息后,Sentinel服务器还会建立向从所有服务器的连接
默认情况下,Sentinel会以两秒一次的频率向所有被监视的主服务器和从服务器发送以下信息:(其中s代表sentinel,m代表master)
1 | PUBLISH _sentinel_:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>" |
每个sentinel还会通过_sentinel_:hello频道发送自己的信息,其次每个sentinel又会订阅_sentinel_:hello频道,因此一个sentinel通过_sentinel_:hello频道发送的信息会被其他sentinel收到(包括它自己也会收到),这样一来,sentinel就能获得其他sentinel的信息
获得的信息会被用于更新主服务器中sentinels字典,字典的键为ip:port,值为一个指向sentinelRedisInstance结构的指针 ,sentinel会判断当前实例是否存在,如果存在就更新对应的sentinel实例的信息,否则就创建一个新的sentinel实例并加入到字典中
获取到其它sentinel服务器后,当前setntinel服务器会创建向其它sentinel服务器的连接,用于sentinel服务器之间的信息交换(比如用于判断主服务器下线状态)
主观下线
默认情况下sentinel会以每秒1次的频率向所有建立的网络连接发送PING命令(包括主从服务器和其他sentinel),通过实例的PING命令回复来判断实例是否在线,主观下线是一个sentinel单独的判断结果
通过配置down-after-milliseconds
参数设置服务器响应超时时间,每个sentinel的这个参数配置可能不同,所以每个sentinel服务器对同个主服务器的主观下线状态判定可能不一样
客观下线
在得出主观下线后,会询问其他sentinel是否也认为实例已经下线,当超过半数的sentinel服务器认为主服务器下线后,则可以认为是客观下线
sentinel服务器通过发送SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
指令询问其它sentinel服务器
选举领头Sentinel进行故障转移
- 配置纪元就是代表着一次执行,如果本次选举不成功,那么就将配置纪元+1,进行下一次的选举。
- 每个sentinel如果监测到了主观下线都可以通过
SENTINEL is-master-down-by-addr
要求其他sentinel给自己投票。 - 投票规则是先到先得,且每人只有一票,只有票数过半的sentinel才会被选举为领头。
- 通过
SENTINEL is-master-down-by-addr
的返回结果中的leader_epoch和leader_runid是否和自己相同,可以知道返回的sentinel有没有为自己投票。
故障转移
- 在从服务器中选出一个服务器作为新的主服务器(选取条件:在线,五秒内有回复,优先级高,复制偏移量最新,ID最小)
- 将其他从服务器改为复制新的主服务器
- 将已下线的旧主服务器设为新主服务器的从服务器,当旧主服务器重新上线后,会成为新主服务器的从服务器。
集群
Redis集群允许多个 Redis 服务器通过分布式的方式存储数据,通过数据分片(sharding)和节点复制来实现高可用性和可扩展性。
1 | typedef struct clusterState { |
redisClient 结构和 clusterLink 结构都有自己的套接字描述符和输入、输出缓冲区, 这两个结构的区别在于, redisClient 结构中的套接字和缓冲区是用于连接客户端的, 而 clusterLink 结构中的套接字和缓冲区则是用于连接节点的。
通过向节点 A 发送 CLUSTER MEET
命令,并指定节点 B 的 IP 地址和端口时, 客户端可以让接收命令的节点 A 将另一个节点 B 添加到节点 A 当前所在的集群里面
槽指派
集群的整个数据库被分为16384个槽(slot),集群中的每个节点都可以处理0个或16384个槽;数据库中的每个键都属于这16384个槽的其中一个
节点可以使用 CLUSTER ADDSLOTS <slot> [slot ...]
命令将指定的槽指派给自己
当集群的所有槽都有节点指派后,集群就会进入上线状态
MOVED 错误
当节点发现键所在的槽并非由自己负责时,节点会向客户端返回一个MOVED错误,指引客户端转向负责这个槽的节点
集群节点也是通过保存键值对过期时间的方式实现过期键
集群节点只能使用0号数据库,而单机数据库则没有这个限制
重新分片
Reids集群的重新分片操作可以将任意数量已经指派给某个节点的槽改为指派给另一个节点,并且相关槽的键值对也会从源节点移到到目标节点
- 让目标节点准备好接收
- 让源节点准备好键值对迁移
- 向源节点发送最多count个属于槽slot的键值对(分批发送)
- 对于上个步骤的每个键值对,让源节点迁移键值对到目标节点
- 重复3和4步骤,直到发送完毕
- 广播消息给所有节点
ASK 错误
通过migrating_slots_to属性可以知道正在进行迁移操作的槽,如果当前节点收到一个键key的请求,它首先会尝试在自己的数据中找这个key,如果找到则直接返回。
如果没找到,那么检查自己的migrating_slots_to属性,查看键key所在的槽是否正在进行迁移,如果是,那么返回ASK错误,指引客户端前往迁移的目标节点。
于是客户端前往目标节点请求这个key,但是要注意,这个key所在的槽并没有完全迁移完成,还属于源节点。所以,客户端向目标节点请求这个key时,默认情况下不会被正常处理,而是返回MOVED 错误。
所以在这里,Redis让客户端先发送一个ASKING命令给目标节点,开启目标节点的REDIS_ASKING标识,带有这个标识的客户端将破例执行这个槽的命令一次。
MOVED 错误代表槽的负责权已经从一个节点转移到了另一个节点
ASK错误只是两个节点在迁移槽的过程中的一种临时措施
复制与故障转移
集群中每个主节点定期向集群的其他主节点发送PING消息,如果其它主节点没有在规定时间内返回PONG消息,那么主节点会在自己的clusterState.nodes字典标志目标主节点的clusterNode结构进入疑似下线状态
同样是超过半数标记疑似下线状态,则会认为目标主节点下线,并向集群广播目标主节点下线的FAIL消息,之后便开始对目标主节点进行故障转移
- 目标主节点(下线)的所有从节点执行SLAVEOF no one命令,称为新的主节点,最终一个从节点得到一半以上投票,胜出选举
- 将目标主节点的所有槽指派转移给新的主节点
- 新的主节点广播PONG消息,宣告自己称为新的主节点
消息
1 | typedef struct { |
- MEET 消息:当一个节点需要加入集群时,它会通过 MEET 消息告诉集群中的其他节点自己的地址
- PING 消息:集群节点会周期性地向其他节点发送 PING 消息,以检查它们是否仍然处于活动状态
- PONG 消息:节点在接收到 PING 消息后,会回复一个 PONG 消息,表示它仍然是活动的
- FAIL消息:当节点检测到其他节点失效时,会发送 FAIL 消息通知集群,触发集群对失效节点的处理
- PUBLISH 消息: 当一个节点需要向整个集群广播某个消息时,它会通过 PUBLISH 消息将消息发送给其他节点,所有接收到PUBLISH消息的节点都会执行相同的PUBLISH命令
独立功能的实现
发布与订阅
1 | struct redisServer { |
- 订阅
- SUBSCRIBE命令:订阅一个或多个频道
- PSUBSCRIBE命令:订阅一个或多个模式(一个模式通过正则匹配多个频道)
- 发布消息
- PUBLISH命令:向频道发送消息
- 查看订阅消息
- PUBSUB CHANNELS [pattern]
- 返回所有或者符合pattern模式的频道
- PUBSUB NUMSUB [channel-1 channel-2…]
- 从pubsub_channels字典中找到频道对应的订阅者链表,返回这些链表的长度
- PUBSUB NUMPAT
- 返回服务器当前被订阅模式的数量,及pubsub_patterns链表的长度
- PUBSUB CHANNELS [pattern]
事务
事务提供一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而该去执行其他客户端的命令请求
1 | typedef struct redisClient{ |
- 事务的实现
- 事务开始
- 一个事务首先以一个 MULTI 命令开始
- MULTI命令会打开客户端flags属性的REDIS_MULTI标识
- 命令入队
- 接着将多个命令放入事务中
- 事务执行
- 最后又 EXEC 命令将这个事务提交给服务器执行
- 事务开始
1 | typedef struct redisDb{ |
-
WATCH 命令是一个乐观锁(optimistic locking)
- WATCH可以在事务执行前监视Redis中任意数量的数据库键,如果在EXEC命令执行时,被监视的键改动了,那么事务就拒绝执行,否则就正常执行
- Redis客户端通过打开flags属性的
REDIS_DIRTY_CAS
标识 实现,如果客户端监视的键被修改了,那么这个标识就会被打开
-
Reids的ACID
- 原子性
- 要求:数据库将事务中的多个操作当作一个整体来执行,服务器要么执行事务中的所有操作,要么就一个操作也不执行
- Redis事务的执行是原子执行的,由于Redis是单线程的,因此某个事务执行的时候,不会执行其他客户端的指令,直到事务执行完。
- Redis事务不支持回滚。也即如果事务队列中任何一条指令失败,Redis会不会回滚这条事务之前的操作。Redis的作者在事务功能的文档中解释说,不支持事务回滚是因为这种复杂的功能和 Redis追求简单高效的设计主旨不相符,并且他认为,Redis事务的执行时错误通常都是编程错误产生的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现, 所以他认为没有必要为Redis开发事务回滚功能。
-
当事务中有语法错误时,Redis会检测出,并且不会执行事务中的任何命令;如果事务出现执行错误时,比如incr一个字符串,则会顺序执行所有命令并且这个incr命令报错
- 一致性
- 要求:数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然是一致的
- Redis对于错误命令和命令执行错误的情况,会拒绝执行;对于停机的情况,通过持久化来进行恢复
- 隔离性
- 要求:数据库中有多个事务并发地执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同
- Redis使用单线程的方式执行事务,总是以串行的方式运行事务,所以事务总是具有隔离性的
- 持久性
- 要求:一个事务执行完毕时,执行这个事务所得到的结果已经被保存到永久性存储介质(比如硬盘)中了,即使服务器在事务执行完毕后停机,执行事务所得到的结果也不会丢失
- Redis通过AOF和RDB进行持久化,但只有AOF模式处于appendfsync为always时,保证每个命令执行完数据被持久化到硬盘,Redis才具有持久性
- 原子性
Lua脚本
通过在服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,实现原子地执行多个Redis命令
- 创建Lua环境
- 载入函数库:基础库、表格库、字符串库等
- 创建redis全局表格,包含一些函数:Redis命令执行函数、Redis日志函数等
- 使用Redis自制的随机函数来替换Lua原有的随机函数:对于相同的sedd,产生相同的结果(避免了原有的副作用)
- 创建排序辅助函数
- 创建redis.pcall错误报告辅助函数
- 保护Lua环境,保护Lua全局变量
- 将Lua环境保存到服务器状态的lua属性中
- Lua环境协作组件
- 伪客户端:Redis服务器为Lua环境创建了一个伪客户端,执行Lua脚本中的Redis命令
- lua_script字典:key是Lua脚本的SHA1校验和,value是Lua脚本
- EVAL命令
- 定义脚本函数:在Lua环境中创建这个脚本对应的Lua函数
- 将脚本保存到lua_scripts字典
- 执行脚本函数
- EVALSHA命令:执行SHA1校验和对应的Lua脚本
- 脚本管理命令
- SCRIPT FLUSH:清除所有和Lua脚本有关的信息
- SCRIPT EXISTS:检验SHA1校验和对应的Lua脚本是否存在于服务器之中
- SCRIPT LOAD:和EVAL命令的前两个步骤一样,但是不执行
- SCRIPT KILL:停止Lua脚本运行
- 脚本复制
- 在服务器运行在复制模式下时,具有写性质的脚本命令也会被复制到从服务器中,包括:EVAL、SCRIPT FLUSH、SCRIPT LOAD
- EVALSHA命令比较特殊,主服务器执行EVALSHA命令,可以找到对应的Lua脚本,而在从服务器中可能找不到
- 主服务器使用repl_scriptcache_dict字典记录已经将哪些Lua脚本传播给了所有服务器,以此来判断此次EVALSHA命令是否安全
- 如果执行的EVALSHA命令的校验和在字典中,那么直接将EVALSHA复制到从服务器中即可
- 否则,需要将EVALSHA命令替换成对应的EVAL命令,再复制到给从服务器中
- 主服务器使用repl_scriptcache_dict字典记录已经将哪些Lua脚本传播给了所有服务器,以此来判断此次EVALSHA命令是否安全
排序
1 | typedef struct _redisSortObject { |
- SORT命令
- 默认对数字值的键key进行排序
- ALPHA选项:对包含字符串值的键进行排序
- ASC和DESC选项:升序和降序
- BY选项:使用被排序键包含的元素作为排序的权重,可用于使用value排序字典的key
- LIMIT选项:返回限制数量的元素个数
- GET选项:对返回结果的二次“加工”
- STORE选项:保存排序结果
二进制位数组
- 位数组的表示
- 使用字符串对象表示位数组,
- 使用SDS结构保存位数组
- 每个字符是一个字节,包含8个位
- 末尾是一个空字符
- SETBIT:设置指定偏移量上的一个或多个二进制位的值
- GETBIT:获取指定偏移量上的一个或多个二进制位的值
- BITCOUNT:统计值为1的二进制位的数量
- BITOP:执行AND、OR、XOR等操作
- 使用字符串对象表示位数组,
慢查询日志
1 | struct redisServer { |
- 配置
- slowlog-log-slower-than选项指定执行时间超过多少微秒的命令会被记录到日志上
- slowlog-max-len选项指定服务器最多保存多少条慢查询日志
- 命令
- SLOWLOG GET:打印指定数量或者所有的慢日志
- SLOWLOG LEN:获取慢日志的数量
- SLOWLOG RESET:删除所有慢日志
监视器
通过MONITOR命令,客户端可以将自己变成一个监视器,实时地接收并打印服务器当前处理请求的相关信息(会开启flags属性的REDIS_MONITOR标志)
当一个客户端发送命令给服务器时,服务器处理命令后,将这条命令请求的信息发送给所有监视器