Redis

1.Redis的简单介绍

Redis 诞生于 2009 年,全称是 Remote Dictionary Server , 远程词典服务器, 是一个基于内存的键值型 NoSQL 数据库。Redis是一个key-value的数据库

特征:

  • 键值( key-value ) 型, value 支持多种不同数据结构, 功能丰富
  • 单线程, 每个命令具备原子性
  • 低延迟, 速度快(基于内存、 I O 多路复用、 良好的编码) 。
  • 支持数据持久化
  • 支持主从集群、 分片集群
  • 支持多语言客户端

2.常用命令

2.1 KEYS

1
2
3
4
5
6
127.0.0.1:6379> help keys

KEYS pattern
summary: Find all keys matching the given pattern #查找所有符合给定模式的key
since: 1.0.0
group: generic

比如 keys * 表示查找所有的key,keys a*表示查找所有以a开头的key

2.2 DEL

1
2
3
4
5
6
127.0.0.1:6379> help del

DEL key [key ...]
summary: Delete a key # 删除一个key
since: 1.0.0
group: generic

比如key age表示删除名为age的key,返回删除的key的个数

2.3 EXISTS

1
2
3
4
5
6
127.0.0.1:6379> help exists

EXISTS key [key ...]
summary: Determine if a key exists #判断一个key是否存在,存在则返回1,不存在返回0
since: 1.0.0
group: generic

2.4 EXPIRE & TTL

1
2
3
4
5
6
127.0.0.1:6379> help expire

EXPIRE key seconds
summary: Set a key's time to live in seconds # 给key设置一个有效期,到期自动删除
since: 1.0.0
group:generic

1
2
3
4
5
6
127.0.0.1:6379> help ttl

TTL key
summary: Get the time to live for a key #返回key的剩余有效时间,如果到期,会变为-2,如果返回-1表示永久有效
since: 1.0.0
group: generic

2.5 SET

1
2
3
4
5
6
127.0.0.1:6379> help set

SET key value [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|KEEPTTL] [NX|XX] [GET]
summary: Set the string value of a key
since: 1.0.0
group: string

设置键值对

3.Redis中的数据结构与对象

3.1 简单动态字符串(SDS)

简单动态字符串(SDS,simple dynamic string):Redis的默认字符串表示,包含字符串值的键值对在底层都是由SDS实现的。

一个SDS结构由三部分组成:

  • free 记录buf数组中未使用字节的数量
  • len 记录buf数组中已使用字节的数量
  • buf 字节数组,用于保存字符串,为一个char类型的数组

SDS遵循C字符串以空字符(\0)结尾的习惯,这个空间不算在SDS的len属性中。redis会为空字符分配额外的1字节空间,这个空字符对SDS的使用者来说是完全透明的。


SDS相较于C字符串的好处

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

    在C语言中普通的字符串如果字符数组的有效长度是N,那实际长度是N+1,最后一个字符是空字符\0,表示字符串的结束。并没有额外记录数组的长度,所以当每次获取字符串长度时,都需要遍历整个字符串,整个遍历操作的时间复杂度是O(N)。而SDS字符串中,使用len属性实时记录了字符串的长度,所以可以直接获取,时间复杂度是O(1)。

  2. 有效防止缓冲区内存溢出

    如果没有为字符串分配足够的空间,当进行字符串扩展拼接时,多出来的部分就会溢出,可能会覆盖这个内存地址中的其他内容,造成内存溢出。而SDS的API进行字符串扩展拼接时,会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小,继续执行,所以这样也不会内存溢出。

  3. 减少修改字符串时带来的内存分配次数

    由于内存重分配涉及复杂的算法,并且可能需要执行系统调用,比较耗时,所以尽量减少内存分配的次数。在C字符串中,每次拼接/裁剪字符串都需要重新进行内存的分配。在SDS中,有一个记录空闲空间的字段free,通过未使用空间,SDS实现了空间预分配惰性空间释放两种优化策略。

    • 空间预分配

      当SDS的API对一个SDS进行修改,并需要对SDS进行空间扩展时,程序会为SDS分配足够的空间,以及一些额外的空间,为下次扩展做准备(这个策略可以减少连续执行字符串增长操作所需的内存重分配次数)。

      额外分配的未使用空间的大小:

      • 对SDS修改后,它的长度(len)小于1MB,那程序会分配和len一样大的空间,保存到free字段中。
      • 对SDS修改后,它的长度(len)大于1MB,那么程序会分配1MB的未使用空间(free)
    • 惰性空间释放

      用于优化SDS的字符串缩短操作,当需要缩短字符串时,并不直接释放要删除的空间,而是使用free记录,这样避免了缩短字符串时所需的内存重分配操作,并为将来有可能的增长操作提供了优化。

    修改字符串长度N次最多需要执行N次内存重分配。

  4. 二进制安全

    C字符串中的字符必须符合某种编码(如ASCII),并且只有字符串末尾可以是空字符\0,所以C中只能保存文本数据,而不能保存图片、音频等二进制数据。而SDS使用len记录字符串的结尾,byte字节数组记录真实的数据(存放的是二进制数据,而不是一位一位的字符)

  5. 兼容部分C字符串

    因为SDS同样在字符串末尾加上\0,所以对于C中的部分函数,SDS也可以使用。

3.2 链表

C语言中没有这种数据结构,因此Redis构建了自己的链表实现。由 list 结构和 listNode 节点组成的链表。

ListNode的结构:

1
2
3
4
5
6
7
8
typedef struct listNode {
// 保存前驱节点
struct listNode *prev;
// 保存后继节点
struct listNode *next;
// 保存值
void *value;
} listNode;

list的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct list {
// 头结点
listNode *head;
// 尾节点
listNode *tail;
// 复制函数
void *(*dup)(void *ptr);
// 释放函数
void (*free)(void *ptr);
// 匹配函数
int (*match)(void *ptr, void *key);
// 链表长度
unsigned long len;
} list;

双端节点,无环(头结点的prev和尾结点的next都指向NULL),head指向头结点,tail指向尾结点,len记录链表节点长度,还有三个函数使链表节点可以保存不同类型的值。

dup、free和match成员则是用于实现多态链表所需的类型特定函数

  • dup函数用于复制 链表节点所保存的值
  • free函数用于释放 链表节点所保存的值
  • match函数则用于对比链表节点所保存的值和另一个输入值是否相等

3.3 字典

字典,又称为符号表、关联数组、映射,是一种用于保存键值对的抽象数据结构。

Redis的数据库使用字典来作为底层实现,对数据库的增删改查操作也是构建在对字典的操作之上的。而字典使用哈希表作为底层实现。


3.3.1 Redis中字典的结构

1
2
3
4
5
6
7
8
9
10
typedef struct dict {
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引,没有进行rehash时,值为-1
long rehashidx;
} dict;

(1) type

一个指向dictType结构的指针,每个dictType结构保存了一些用于操作特定类型键值对的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct dictType {
// 计算哈希值的函数
uint64_t (*hashFunction)(const void *key);

// 复制键的函数
void *(*keyDup)(void *privdata, const void *key);

// 复制值的函数
void *(*valDup)(void *privdata, const void *obj);

// 对比键的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2);

// 销毁键的函数
void (*keyDestructor)(void *privdata, void *key);

// 销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
} dictType;

(2) privdata

保存了需要传给类型特定函数的可选参数。

(3) dictht ht[2]

字典中保存两个哈希表,哈希表的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//哈希表的结构:
typedef struct dictht {
// 哈希表数组
dictEntry **table;

// 哈希表大小
unsigned long size;

// 哈希表掩码,大小为size-1,用于计算索引值
unsigned long sizemask;

// 哈希表中已有的节点数
unsigned long used;
} dictht;
  • table table为一个dictEntry类型的一维数组,数组中的每个元素都是一个指向dictEntry结构的指针,每个dictEntry中保存了一个键值对。

    dictEntry的结构为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //每个dictEntry节点
    typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union {
    void *val;
    uint64_t u64;
    int64_t s64;
    double d;
    } v;
    // 指向下一个哈希节点,形成链表
    struct dictEntry *next;
    } dictEntry;

    key属性保存键值对中的键,v属性保存键值对中的值,它的类型可以是这几种。

    next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,来解决键冲突,这是链地址法

  • size 哈希表大小

  • sizemask 哈希表掩码,大小为size-1,用于计算索引值(哈希值&sizemark)

  • used 哈希表中已有的节点数

(4) rehashidx

rehash索引,没有进行rehash时,值为-1

3.3.2 哈希算法

当要将一个新的键值对添加到字典里时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表的指定桶上。

步骤:

  1. 计算哈希值 hash=dict->type->hashFunction(key); 在这个函数里面,Redis使用的是MurmurHash2算法来计算键的哈希值。
  2. 计算索引值(桶下标) index=hash & dict->ht[0].sizemark; 当有两个或两个以上的键被分配到了哈希表数组的同一个索引上面,则这些键发生了哈希冲突(collision)。Redis的哈希表使用链地址法来解决键冲突,单向链表,新的节点被插入到链表的头部(这是因为链表没有指向链表尾结点的指针,所以为了提高速度,每次就将新节点插入表头位置)

3.3.3 渐进式rehash(扩容或缩容)

哈希表的负载因子=哈希表已保存节点数量/哈希表大小

扩容/缩容的目标容量的规定:

即要为ht[1]分配的空间大小。

  • 扩容,ht[1]的大小为第一个大于等于(ht[0].used*2)的2的n次方,比如ht[0]中有4个节点,则ht[0].used×2=8,则第一个大于等于8,并且需要是2的n次方的数就是8,所以容量就是8。
  • 缩容,ht[1]的大小为第一个大于等于ht[0].used的2的n次方,比如ht[0]中有4个节点,则ht[0].used=4,则第一个大于等于4,并且是2的n次方的数是4,所以ht[1]的容量是4。

以下情况,程序会自动对哈希表进行rehash操作:

  • 服务器目前没有在执行BGSAVE或BGREWRITEAOF命令,并且哈希表的负载因子>=1 扩容
  • 服务器目前正在执行BGSAVE或BGREWRITEAOF命令,并且哈希表的负载因子>=5 扩容
  • 哈希表的负载因子<0.1 缩容

步骤:

  1. 为ht[1]分配空间(指定容量),此时字典同时拥有两个哈希表:ht[1]和ht[0]
  2. 在字典中维护一个索引计数器变量rehashidx,初值为0,表示从ht[0]的数组的0位置开始。
  3. 在rehash期间,用户依旧可以对redis数据库中的数据进行增删改查更新等操作,数据库在响应用户请求的同时,会将ht[0]哈希表的rehashidx索引上的所有键值对rehash到ht[1],这里需要重新计算索引下标。当这次的rehash完成后,rehashidx的值加1,然后继续执行后面的桶。
  4. 当ht[0]上的所有键值对都被rehash到了ht[1]后,rehashidx的值被设为-1,表示rehash完成。
  5. 现在ht[0]是空表, 释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个哈希表,为下次rehash做准备。

渐进式rehash

rehash不是一次性完成的,而是渐进式完成的。用户操作(增删改查更新)和节点迁移是并发执行的。

字典会同时使用ht[0]和ht[1],对字典的删改查会在这两个哈希表上进行,比如要在字典中查找一个键,程序会现在ht[0]中查找,如果没有,就到ht[1]中查找。对于新添加到字典的键值对,都会被直接保存到ht[1]中,ht[0]中不再进行任何的添加操作。

3.4 跳跃表(skiplist)

跳跃表是一个有序并联链表,它以随机化数据结构为基础,通过在每个节点中维持多个指向其他不同的节点的的指针,来达到快速访问的目的。

Redis在实现有序集合键和在集群节点中用作内部数据结构。

Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,或者有序集合中元素的成员是比较长的字符串时,redis就会使用跳跃表来作为有序集合键的底层实现。

——-《redis的设计与实现》

1、结构

相同颜色的是同一层。

Redis的跳跃表是由zskiplistNodezskiplist两个结构定义,zskiplisNode是一个跳跃表节点,跳跃表由许多个跳跃表节点构成,然后有一个zskiplist来保存跳跃表节点的相关信息,就类似于虚拟头结点dummyNode,有指向链表的头结点和尾结点的指针,以及其他信息。

1
zadd key score1 obj1 score2 obj2 ...

zskiplistNode结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct zskiplistNode {
//成员对象
robj *obj;

// 分值,就是语句中的score
double score;

// 指向前驱节点prev
struct zskiplistNode *backward;

// 层,每个节点有1~32个层,除头结点外(32层),其他节点的层数是随机的
struct zskiplistLevel {
// 每个层都保存了该节点的后继节点
struct zskiplistNode *forward;

// 跨度,用于记录该节点的前进指针所指向节点和当前节点的距离
//即指向的节点编号(自己取的)-当前节点的编码
unsigned long span;
} level[];

} zskiplistNode;
  • level[]数组

    是一个结构体数组,结构体有两个成员,forward和span。数组的每个元素代表这个节点所在的层,level[0]是第一层,level[1]是第二层,以此类推。

    数组的大小:每次创建一个新跳跃表节点时,程序都根据幂次定律随机生成一个1-32之间的值作为level数组的大小,这个大小就是节点的层高。

    • 前进指针forward

      指向后面的某个节点,用于快速访问节点(可以跳着访问)

    • 跨度

      用于记录两个节点之间的距离,这两个节点,一个是当前节点,一个是forward指针指向的节点,跨度的值等于两节点中间相隔的节点个数+1。forward指针指向NULL,则跨度为0。跨度是用来计算当前节点在跳跃表中的排位的。第一个节点(头节点后的节点)的排名为1,第三个节点的排位为2….

  • 后退指针backward

后退指针没有跨度,所以只能挨个访问。访问方法是先通过tail找到尾结点,然后访问后退指针指向的节点,一个一个倒着访问。

  • 分值和成员

跳跃表中的所有结点都按照分值从小到大来排序,double类型的浮点数。

节点的成员对象obj是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值,即简单动态字符串

关于排序问题:在同一个跳跃表中,各个结点保存的成员对象必须是唯一的,但分值可以相同。如果两个节点的分值不同,则按照分值从小到大排序,如果分值相等,则按照成员对象在字典序中的大小进行排序,小的排在前面。


zskiplist

多个跳跃表节点组成了一个跳跃表list,用zskiplist来维护这个表

1
2
3
4
5
6
7
8
9
10
typedef struct zskiplist {
// 头尾指针,用于保存头结点和尾节点
struct zskiplistNode *header, *tail;

// 跳跃表的长度,即除头结点以外的节点数
unsigned long length;

// 最大层数,保存了节点中拥有的最大层数(不包括头结点)
int level;
} zskiplist;

2、跳表的核心操作

N为跳跃表的长度,即节点个数,不包括头结点。

  • 插入一个数据;平均O(logN)
  • 删除一个数据;平均O(logN)
  • 查找一个数据;平均O(logN)
  • 按照区间查找数据(比如查找值在[100, 356]之间的数据);平均O(logN)

新加入的节点,层数是随机的,假如随机生成的层数是2,则level0、level1三层有这个节点。

如果要实现这个效果,我们要分两步去做:

  • 找到新节点在每一层的上一个节点(即:对于level0,应该先找到节点9;对于level1,应该先找到节点8)
  • 将新节点插入到每一层的上一个节点和下一个节点之间

redis的跳跃表在插入节点时,会随机生成节点的层数,通过控制每一层的概率,控制每一层的节点个数,也就是保证第一层的节点个数,之后逐层增加。生成n+1的概率是生成n的概率的4倍。我觉得这是为了避免最差情况下退化为链表的结构,所以尽可能的要多生成几层。

引用大佬文章:https://djqueue.blog.csdn.net/article/details/105476382

3、相关问题

为什么Redis用跳跃表而不用链表?

储存数据时,数组容量有限且有序数组增加元素时效率比较低,所以存储数据时常选用链表。但是因为查询链表中某个节点的时间复杂度是O(N),无论链表中的元素是否有序。

但是使用红黑树,二叉搜索树,B树,B+树,实现他们又过于复杂。所以使用跳表,利用空间换时间的方式,提高查询效率。

为什么Redis用跳跃表而不用B+树?

从内存占用、对范围查找的支持和实现难易程度这三方面总结的原因

MySQL是从磁盘中读取数据。所以MySQL要解决的问题是尽可能的减少磁盘IO的次数,在MySQL中,叶子节点存储数据,即数据库中的每条记录,非叶子结点存储索引,每个节点可以存储多条记录,MySQL将节点大小设置为磁盘页的大小,所以每次读取磁盘页就会读取整个叶子节点,为的是最大限度的降低磁盘IO次数,B+树只需要查到最后一层的某些页面,然后逐个遍历(记录与记录之间是单向链表的形式,页面和页面之间是双向链表的形式)。

b+树一个节点可以存多个keyvalue结构,其他很多数据结构,这也保证了B+树的稳定性。

而Redis是从内存中读取数据,不涉及IO,从内存读取比从磁盘读取快得多,所以不必那么麻烦,就使用跳跃表。

部分引用https://nyimac.gitee.io/2020/11/08/Redis%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/#3%E3%80%81Redis%E4%B8%AD%E7%9A%84%E8%B7%B3%E8%B7%83%E8%A1%A8

为什么Redis使用单线程

redis本身是基于内存的,所以redis的性能瓶颈更多的是在于内存和网络带宽,而不是CPU。而单线程的实现更加简单和经济,指令串行,不用维护额外的锁机制,资源竞争,避免了不必要的上下文切换和竞争条件,减少了CPU的消耗。即Redis是基于内存的操作,没有I/O操作,所以单线程执行效率更高。

单线程下串行更快的原因:

对于单CPU来说,线程的串行比并行更快,因为只有一个CPU,只能单线程,此时的并行相当于先执行A一段时间,再执行B一段时间,再执行A,再执行B,….,在切换线程时,会导致上下文切换,CPU需要保存切换线程的信息,这个操作也很耗时,所以对于单线程 串行比较快。


对于I/O操作,并发执行效率更高

因为I/O操作主要有以下两个过程

  • 等待I/O准备就绪
  • 真正操作I/O资源

等待I/O准备就绪这个阶段,CPU是空闲的,这时便可以去执行其他任务,这样也就提高了CPU的利用率

3.5 整数集合(intset)

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

整数集合可以保存的整数类型是int16_t、int32_t、int64_t,集合中元素从小到大有序排列,并且没有重复的元素。

命令:SADD numbers 1 3 5 7 //创建整数集合

smumbers numbers //查看整数集合

1、结构

1
2
3
4
5
6
7
8
typedef struct intset {
// 编码方式
uint32_t encoding;
// contents数组的长度
uint32_t length;
// 保存元素的数组,也就是set集合
int8_t contents[];
} intset;
  • encoding

    集合采用的编码方式,可以是INSERT_ENC_INT16(int16_t)、INSERT_ENC_INT32(int32_t)、INSERT_ENC_INT64(int64_t)

  • length

    集合的长度,即数组contents的长度

  • contents[]

    数组的真正类型取决于encoding的值,而不是int8_t

2、升级

当我们要将一个新元素添加到整数集合中,且新元素的类型比整数集合现有的所有元素类型都长,整数集合需要先进行升级,然后再添加这个新的元素。

比如现在整数集合的类型是int16_t,但是要添加的元素的长度类型是int_32t,所以需要先将其升级为int_32t类型。


当向一个包含三个 int16_t 类型的值**(每个元素占16位)**的整数集合添加一个 int32_t 类型的值时, 整数集合的扩展和转换过程如下。(注意! 在转换过程中底层数组的有序性不变)

初始数组状态:

这时,需要将65535添加到整数集合里面,因为int16_t能够表示的范围为(-32768~32767),无法容纳该数字,所以需要升级

1、扩展content的分配的内存空间,由3x16 扩展为 4x32

2、按照元素索引由高到低的顺序将16位的元素移动至新的位置(32位)。最后添加新插入的元素。

最后,改变intset中encoding和length的值

  • encoding由INTSET_ENC_INT16改为INTSET_ENC_INT32
  • lentg由3改为4

最终结果:

升级后新元素的位置

因为新元素的长度大于数组中所有其他元素的长度,所以新元素的值要么大于所有现有元素,要么就小于所有现有元素。

  • 若为最小值,放在数组开头
  • 若为最大值,放在数组末尾

升级好处:

  1. 提高灵活性。整数集合可以通过自动升级底层数组来适应新元素,即你可以存放多种类型大小的整数值。
  2. 节约内存。自适应,只有在有大元素需要添加时才会升级为大类型数组。

整数集合不支持降级操作,一旦升级,就会一直保持这个状态,直到下次升级。

3.6 压缩列表

压缩列表(ziplist)是列表键(list)和哈希键(hash)的底层实现之一。前提是一个列表键只包含少量的列表项,并且每个列表项要么是小的整数值,要么是长度比较短的字符串;或哈希键中只包含少量键值对,并且每个键值对的键和值要么是小整数值,要么是长度比较短的字符串,才会使用压缩列表来实现列表键。

否则,压缩列表的查询效率会很低。那时就会选择跳表或其他方法来实现列表键和哈希键了。

ziplist使用紧凑的连续内存块顺序存储数据,在list或者hash结构中,未使用listNode(24字节)和dictEntry(24字节)结构体来存储元素项(这些内存占用存储的并不是数据,而是一些定义和指针),因此会节省内存。

1、结构

  • zlbytes unit32_t 4字节,用来记录整个压缩列表占用的内存字节数。

  • zltail unit32_t 4字节,尾结点到列表起始节点的距离(偏移量),一个指向压缩列表起始地址的指针p,则p + zltail就是表尾结点的地址。

  • zlen unit16_t 2字节,记录压缩列表中的节点数量,小于65535(2的16次方)时是真实节点的数量,大于等于时,则需要遍历整个列表才可以得到总的节点数量。

  • entryX 列表节点,每个压缩列表节点可以保存一个整数值或者一个字节数组

    • previous_entry_length

      单位是字节,记录压缩列表中前一个节点的长度。

      如果前一个节点长度小于254(2的8次方)字节,那该属性的长度为1字节(8bit),里面保存前一个节点 长度。

      如果前一个节点长度大于等于254字节,则该属性的长度为5字节,属性的第一个字节为0xFE(254),后面4字节用来保存前一个字节的长度。

      压缩列表的访问是从表尾到表头的顺序,因为每个节点中记录了前一个节点的长度,所以当指针指p向当前结点时,则前一个节点的起始地址为p - previous_entry_length

    • encoding

      1.记录节点的content属性保存的数据的类型(字节数组还是整数值)和长度。

      最高位为00 or 01 or 10,则content保存的是字节数组,除去这两位,剩下的位表示数组的长度。

      2.最高位是11,则content保存的是整数值,去除这两位,剩余的位数决定整数的类型。

    • content

      保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。

  • zlend unit8_t 1字节,永远等于0xFF,标记压缩列表的末尾

2、连锁更新

连锁更新就是指添加一个节点导致所有节点的previous_entry_length都要更新的现象。

一个压缩列表的所有节点的长度在250~253之间,previous_entry_length都是1字节,如果现在添加一个长度大于253字节的节点到表头,那后面的节点的previous_entry_length长度变成5字节,则这个节点的长度超过254字节,则它后面的节点也需要修改,引发连锁更新

添加新节点到压缩列表或者从压缩列表中删除节点,可能会引发连锁更新操作,不过这种操作出现的几率不高。

不过由于这种情况很少见,而且假如出现了,只要更新的节点个数不多,也不会产生什么太大的影响。


Redis
https://vickkkyz.fun/2022/05/22/Redis/redis/
作者
Vickkkyz
发布于
2022年5月22日
许可协议