【Reids数据类型】

Redis数据类型


前言

最近复习redis的相关知识,温故而知新,记录并分享redis的学习成果,欢迎大家一起交流。


一、Redis数据结构类型

1 . 简单动态字符串

在走进redis的基本数据类型前,先介绍一下redis的简单动态字符串,在redis里,没有直接使用C语音传统的字符串表示,而是构建了名为简单动态字符串(SDS)的抽象类型,C字符串只会作为字符串字面量用在一些无需对字符串值进行修改的地方,如打印日志;当在一些需要修改字符串值场景时,Redis就会使用SDS来表示字符串值,如

set msg “Hello

此时键值对的键是一个字符串对象,对象的底层实现是一个保存着字符串“msg”的SDS,与此同时,它的值也是一个字符串对象,保存着“hello”;
除此之外,SDS还被用作缓冲区,:AOF模块中的AOF缓冲区,以及客户端状态的输入缓冲区:

‌1.AOF 缓冲区的角色‌ ‌写入加速‌:AOF(Append Only File)持久化过程中,Redis先将写命令追加到内存中的 ‌AOF 缓冲区‌(SDS 实现,再异步刷盘到磁盘。
2.性能优化‌:通过 SDS 的‌动态扩容‌和‌二进制安全‌特性,避免频繁磁盘 I/O,提升写入吞吐量;
‌3.客户端输入缓冲区‌:存储客户端发送的未解析命令

下面是sds的构造:

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

字符串S
上图定义了一个sds例子:free属性为0表示这个sds没有分配任何未使用空间;len的属性值为5,表示其保存了一个5字节长的字符串;buf是一个char类型的数组,保存了Redis5个字符,最后一个字节则保存了空字符‘\0’。这跟c类似,所以可以直接重用c字符串函数库里的函数。
同时,SDS因为维护了len属性,所以获取一个sds长度的复杂度仅为O(1),而c语言里获取长度是依赖字符串结尾的空字符为止,所以为O(n);
此外,在c语言里,初始化数组时要给数组初始化一定的长度,而SDS则是当API需要对SDS进行修改时,API会先检查SDS的空间是否满足其修改需求,不满足,则自动扩容,然后再执行修改操作,这样就完全杜绝了缓冲区溢出的可能;如sdscat(s,“Cluster”):它会分配13的未使用字节空间,同时拼接的字符串也正好是13字节,它是基于SDS的空间分配策略:

‌1、空间预分配:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展时,程序不仅会为SDS分配修改所需的空间,还会为SDS分配额外的未使用空间;
‌分配规则‌
‌扩容后长度 < 1MB‌
分配双倍所需空间(新长度 = 2 × (len + append_len)),避免频繁扩容。
示例:原字符串长度5,追加3字符,则新分配空间为 2 × (5+3)+1 = 17 字节(含\0)。
‌扩容后长度 ≥ 1MB‌
固定多分配 ‌1MB‌ 空间(新长度 = (len + append_len) + 1MB),防止过度浪费内存。
示例:原字符串长度2MB,追加0.5MB,则新分配空间为 2 + 0.5 + 1 +1 byte = 3.5MB(注意\0)

‌2、惰性空间释放:‌
延迟内存回收‌
当缩短 SDS 字符串时(如 SET key “hello” 改为 SET key “hi”),程序不会立即释放多余的内存,而是通过 free 字段记录未使用的空间(如上例中 free = 3。
后续若需扩展字符串(如追加 “world”),可直接复用 free 空间,避免频繁重分配。
‌主动释放接口‌
SDS 提供 API(如 sdsclear)可手动释放未使用空间,避免长期内存浪费。
‌3、‌二进制安全
‌长度独立存储‌
SDS通过len字段显式记录字符串长度(而非依赖\0终止符),即使字符串中包含\0字符也不会被截断。
示例:字符串"1234\0123"在C语言中strlen返回4(因\0截断),而SDS通过len=7正确识别完整长度8。
‌兼容二进制数据‌
SDS的buf[]数组可存储任意字节(包括空字符、非文本数据),适用于图片、序列化对象等二进制场景。

加上兼容C部分函数的特性,SDS通过长度存储‌、‌二进制安全‌和智能内存管理‌三大设计,成为 Redis 高性能字符串操作的核心基础。

2 . 链表

Redis的链表(Linked List)是一种双向链表数据结构,广泛应用于 List 类型的底层实现以及其他内部功能模块。

1、双向链表节点‌:

每个节点(listNode)包含前驱指针(prev、后继指针(next)和值指针(value,支持双向遍历。

typedef struct listNode {
    //前置节点
    struct listNode *prev;
    //后置节点
    struct listNode *next;
    void *value;  // 存储任意类型数据
} listNode;

‌链表结构体‌:通过list结构管理链表,包含头尾指针、长度计数器及操作函数(如复制、释放、比较)。

typedef struct list {
	//表头节点
    listNode *head;
    //表尾节点
    listNode *tail;
    //链表所包含的节点数量
    unsigned long len;
    void *(*dup)(void *ptr);  // 节点值复制函数
    void (*free)(void *ptr);   // 节点值释放函数
    int (*match)(void *ptr, void *key);  // 节点值比较函数

这样的好处是‌:

高效增删:支持 O(1) 时间复杂度的头尾插入/删除操作,中间节点操作则为 O(n)。 示例:LPUSH/RPUSH 命令可直接操作头尾节点。

顺序访问:支持双向遍历(从左到右或从右到左),但随机访问性能较差,需要 O(n) 时间遍历。

大容量支持:最大可存储 2³²-1 个节点(约 40 亿)。

无环结构:表头节点的 prev 指针和表尾节点的 next 指针均指向 NULL,链表访问以 NULL 为终点。

链表长度计数器:通过 list 结构的 len 属性实现 O(1) 时间复杂度的节点计数。

多态支持:链表节点使用 void* 指针存储节点值,并通过 list 结构的 dup、free、match 属性设置类型特定函数,支持存储多种数据类型。然而,链表在 64 位系统中存在额外空间开销:prev 和 next 指针各占 8 字节,共 16 字节,且节点内存单独分配,可能加剧内存碎片化,影响内存管理效率。
高效增删‌:头尾插入/删除操作时间复杂度为 O(1),中间节点插入/删除为 O(n)。 示例:LPUSH/RPUSH 命令直接操作头尾节点。
‌顺序访问‌:支持从左到右或从右到左遍历,但随机访问性能较差(需 O(n) 遍历)。
‌大容量支持‌:最多可存储 2³²-1 个节点(约 40 亿)。
无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问以 NULL 为终点;
链表长度计数器:通过 list 结构的 len 属性来对 list 的链表节点进行计数,获取节点数量的复杂度为O(1);
多态:链表节点使用 void* 指针来保存节点值,并通过 list 结构的 dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。使用链表的附加空间相对太高,因为 64bit 系统中指针是 8个字节,所以 prev 和 next 指针需要占据16 个字节,且链表节点在内存中单独分配,会加剧内存的碎片化,影响内存管理效率。

2 、压缩列表:

Redis 的压缩列表(ZipList)‌是一种为优化内存设计的高效线性数据结构,主要用于存储小规模整数或短字符串。ZipList是一种特殊的“双端链表”(并非链表),由一系列特殊编码的连续内存块组成,像内存连续的数组。可以在任意一端进行压入/弹出操作,并且该操作的时间复杂度为O(1)。
压缩列表ZIPLIST
1)zlbytes:uint32_t类型,4个字节,记录整个ziplist占字节数,当对ziplist内存重分配,或者计算zlend时使用。
2)zltail:uint32_t类型,4个字节,记录表尾节点(entry)距离表头节点(entry)多少字节,通过此偏移量,不用遍历表就可以确定表尾节点位置。
3)zllen:uint16_t类型,2个字节,记录ziplist包含的节点(entry)数量,当该值小于65535时,其值就是节点数量,但是当其大于该值,则要遍历表才可以知道节点真实数量。
4)entry:列表节点类型,长度不确定。每个entry是压缩列表的节点,长度由其保存的内容确定。
5)zlend:uint8_t类型,1个字节,特殊值0xFF(十进制255),标记ziplist的结尾。

struct ziplist {
    uint32_t zlbytes;  // 整个列表占用的字节数(含头部)
    uint32_t zltail;   // 尾节点偏移量(相对起始位置)
    uint16_t zllen;    // 节点数量(若≥65535需遍历统计)
    Entry[] entries;   // 动态长度的节点数组
    uint8_t zlend;     // 结束标记(固定值0xFF)
};

// 节点存储格式
struct entry {
    uint8_t prevlen;      // 前驱节点长度(1或5字节)
    uint8_t encoding;     // 数据类型编码(1字节)
    union {
        int64_t integer;  // 存储的整数值
        char[] string;    // 字节数组(长度由encoding决定)
    } content;
};

prevlen:

字节的previous_entry_length属性,以字节为单位,记录ziplist中前一个节点的长度。该属性的长度是1字节或5字节。
当前一个节点长度小于254字节,则该属性是1字节;如果前一个节点大于254,则该属性是5字节(2的8次方): 示例:若前一节点长度为 100
字节,存储为 0x64(1字节)而非 0x00000064(4字节) 示例:长度 1000 字节存储为 0xFE000003E8(5字节)

encodeing:(小端序字节。低在前,高在后)
ziplistEncoding
ziplist整数

保存字符串“ab”:
ziplist保存字符串
再加个bc:
ziplist表示abbc
保存数字:
ziplist保存数字

1 .连锁更新(Cascade Update)‌ ‌触发条件‌:当插入或删除节点导致相邻节点的 previous_entry_length 字段长度变化(如从 1 字节扩展为 5 字节),可能引发后续节点的连续调整。
‌性能影响‌:最坏情况下需重新分配内存并更新所有后续节点,时间复杂度达 O(n²)。
‌2 . 查询效率低‌:线性遍历‌:由于节点通过偏移量定位而非指针,查找需逐个解析 previous_entry_length 和 encoding字段,平均时间复杂度为 O(n)。 ‌对比链表‌:普通双向链表(如 linkedlist)通过指针直接跳转,查询效率更高。
3.申请内存必须是连续内存,如果内存占用很多,大容量数据可能导致内存碎片或分配失败。

在3.2版本后,redis统一采用快速链表来实现List:

3 、快速链表:

本质是一个双端链表,只不过每一个节点都是一个zipList,核心思想是通过分片解决Ziplist内存问题;

快速链表QuickList
(1)压缩机制‌:中间节点可压缩(LZF 算法),头尾节点常驻内存以保证高频访问性能

typedef struct quicklistNode {
    struct quicklistNode *prev;  // 前驱节点指针
    struct quicklistNode *next;  // 后继节点指针
    unsigned char *zl;           // 指向 ZipList 或压缩后的 LZF 数据
    unsigned int sz;             // ZipList 的字节大小
    unsigned int count : 16;     // ZipList 的元素数量
    unsigned int encoding : 2;   // 编码类型(RAW=1 未压缩,LZF=2 压缩)
    unsigned int recompress : 1; // 临时解压标记(读取后需重新压缩)
} quicklistNode;
typedef struct quicklist {
    quicklistNode *head;          // 头节点
    quicklistNode *tail;          // 尾节点
    unsigned long count;          // 所有 ZipList 的元素总数
    unsigned long len;            // 节点数量(ZipList 分片数)
    int fill : 16;                // 单个 ZipList 的最大容量(由 list-max-ziplist-size 配置)
    unsigned int compress : 16;   // 压缩深度(0=不压缩,n=头尾 n 节点不压缩)
} quicklist;

为了避免QuickList中的每个ZipList中entry过多,Redis提供了一个配置项:list-max-ziplist-size来限制。
如果值为正,则代表ZipList的允许的entry个数的最大值 如果值为负,则代表ZipList的最大内存大小,分5种情况:
①-1:每个ZipList的内存占用不能超过4kb
②-2:每个ZipList的内存占用不能超过8kb
③-3:每个ZipList的内存占用不能超过16kb
④-4:每个ZipList的内存占用不能超过32kb
⑤-5:每个ZipList的内存占用不能超过64kb

快速链表1

除了控制ZipList的大小,QuickList还可以对节点的ZipList做压缩。通过配置项list-compress-depth来控制。因为链表
一般都是从首尾访问较多,所以首尾是不压缩的。这个参数是控制首尾不压缩的节点个数:
0:特殊值,代表不压缩
1:标示QuickList的首尾各有1个节点不压缩,中间节点压缩
2:标示QuickList的首尾各有2个节点不压缩,中间节点压缩
快速链表2
应用场景‌
‌消息队列‌:支持高效的头尾操作(LPUSH/RPOP)。
‌大数据列表‌:分片存储避免单 ziplist 过大的性能问题

QuickList 通过 ‌分片+压缩‌ 在内存和性能间取得平衡,成为 Redis 列表的高效实现

4 、跳表:

跳表(Skip List)是一种基于概率平衡的数据结构,结合了链表和二分查找的思想,常用于实现有序集合(如 Redis 的 ZSET)。
特点:元素按照升序排列存储;节点包含多个指针,指针跨度不同。
‌跳跃表节点(zskiplistNode)‌:

typedef struct zskiplistNode {
    sds member;                      // 成员(存储节点的值)(如有序集合的元素)
    double score;                    // 排序分值
    struct zskiplistNode *backward;  // 后退指针(用于双向遍历)
    struct zskiplistLevel {          // 层级结构
        struct zskiplistNode *forward; // 前进指针
        unsigned int span;            // 跨度(记录到下一个节点的距离)
    } level[];                       // 柔性数组,动态分配层级
} zskiplistNode;

‌跳跃表结构(zskiplist)‌

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 头尾节点
    unsigned long length;                // 节点总数
    int level;                           // 当前最大层高
} zskiplist;

最多允许32层level。
跳表结构
跳表查询平均时间复杂度‌:查找、插入、删除操作的平均时间复杂度为 O(log n),接近平衡树的效率;
分层索引‌:通过多级链表(高层为稀疏索引,底层为完整数据)实现快速跳跃式查找;
无需复杂平衡‌:插入/删除时仅需调整局部指针,无需像平衡树(如 AVL、红黑树)进行全局旋转;
内存局部性‌:连续访问的节点可能缓存在 CPU Cache 中,提升实际性能。

3 . 整数集合

整数集合是 Redis 用于存储‌有序、不重复整数‌的紧凑结构,适用于元素较少且均为整数的场景

整数集合

‌动态编码升级‌: ‌自动升级‌:当插入的整数超出当前编码范围(如原为 int16_t,插入 int32_t 值),集合会自动升级编码并重新分配内存。 ‌不可降级‌:一旦升级,即使删除大整数,编码也不会降级,升级扩容是倒序扩容
内存优化‌: ‌紧凑存储‌:contents 数组直接存储整数值,无额外指针开销。 ‌
节约内存‌:相比哈希表(hashtable),整数集合在少量整数场景下内存占用更低。

为了方便查找,redis会将数据有序存入contents数组中(二分查找,时间复杂度 O(log n)),
整数集合1

4 . 字典

1、数据结构

当我们向Dict添加键值对时,Redis首先根据key计算出hash值(h),然后利用h&sizemask来计算元素应该存储到数组中的哪个索引位置。
Redis字典由以下三部分核心结构组成:
‌哈希表(dictht)‌:

typedef struct dictht {
    dictEntry **table;          // 哈希桶数组(存储 dictEntry 指针)
    unsigned long size;         // 哈希表大小(始终为 2 的幂次方)
    unsigned long sizemask;     // 掩码(等于 size-1,用于计算索引)
    unsigned long used;         // 当前存储的键值对数量
} dictht;

‌功能‌:存储实际数据,通过 size 和 sizemask 快速定位桶位置;

  • table:存储键值对的数组,每个元素为dictEntry指针。
  • size:哈希表大小,始终为2的幂次方。
  • sizemask:用于计算索引(hash & sizemask)。
  • used:当前存储的键值对数量。

‌哈希节点(dictEntry)‌:
包含key、value(支持多种类型)及next指针(解决哈希冲突)。

typedef struct dictEntry {
    void *key;                  // 键(支持任意类型)
    union {                     // 值(支持多种类型)
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;     // 指向下一个节点(解决哈希冲突)
} dictEntry;

功能‌:存储键值对,通过 next 指针实现链式冲突解决(next将多个哈希值相同的键值对连接在一起);
‌字典(dict)‌:
维护两个哈希表(ht[0]和ht[1]),用于渐进式Rehash。
rehashidx:标记Rehash进度,未进行时为-1。

typedef struct dict {
    dictType *type;             // 类型特定函数(如哈希计算、键值操作)
    void *privdata;             // 私有数据,做特殊hash运算时使用(供 type 函数使用)
    dictht ht[2];               // 两个哈希表,一个是空,一个是当前数据,rehash时使用(用于渐进式 rehash)
    long rehashidx;             // rehash 进度(-1 表示未进行)
    int16_t pauserehash;        // rehash 暂停标记(如迭代时暂停;1则暂停,0则继续)
} dict;

‌功能‌:管理哈希表,支持动态扩容/缩容和渐进式 rehash。

插入dict

2、 Redis 字典键的哈希值计算过程‌

‌1. 哈希函数的选择‌
Redis 根据字典类型(dictType)中定义的哈希函数计算键的哈希值:
‌默认哈希函数‌:
Redis 4.0 前使用 ‌MurmurHash2‌ 算法。
Redis 4.0 后改用 ‌SipHash‌(抗哈希碰撞攻击更安全)。
‌自定义哈希函数‌:可通过 dictType 结构指定其他哈希函数。
2. 计算步骤‌
‌调用哈希函数‌:
若键为整数,可能直接转换为无符号整数作为哈希值。
若键为字符串,调用 SipHash 或 MurmurHash2 计算。

hash = dict->type->hashFunction(key);  // 根据键计算哈希值

计算索引位置‌:
使用哈希表的sizemask属性和哈希值,计算出索引值,

index = hash & dict->ht[x].sizemask;  // 通过掩码定位桶位置:

3.、Redis 解决键冲突

当有两个或两个以上数量的键被分配到了同一个索引上面,这些键就发生了冲突。
Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都要一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这样就解决了键冲突的问题。
新增等操作

4、 Rehash

1‌. Rehash 的定义与作用‌

Rehash 是 Redis 字典(dict)在扩容或缩容时重新分配哈希槽的过程,目的是维持哈希表的高效性。

‌扩容‌:当负载因子(used/size)≥1 时触发,防止哈希冲突导致链表过长,影响查询性能(O(1)→O(n))。
‌缩容‌:当负载因子<0.1 时触发,减少内存浪费。

2.渐进式Rehash
如果哈希表内保存的键值对是四百万、四千万的键值对时,要一次性将这些键值对全部rehash到ht[1]的话,庞大的计算量会导致服务器在一段时间内停止服务,这显然是不可接受的。

所以Redis 采用渐进式 Rehash 避免一次性迁移数据导致的性能阻塞:
‌双哈希表‌

  • 维护 ht[0](旧表)和 ht[1](新表)
  • 新表大小为旧表的 2 倍(扩容)或 1/2(缩容)。

‌分批迁移‌

  • 通过 rehashidx 记录当前迁移的桶索引。
  • 每次操作(如查询、插入)时迁移一个桶的数据,直到完成。

‌并发访问‌

  • 读操作同时查询新旧表。
  • 写操作直接写入新表,确保数据一致性。

详细步骤:

  1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表;
  2. 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作开始;
  3. 在rehash期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定操作外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx的属性值+1;
  4. 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash到ht[1],这是程序的rehashidx的属性值会设为-1,表示rehash操作已完成。

‌3. Rehash 的触发条件‌

  • ‌扩容条件‌
    负载因子 ≥1 ‌且‌ 未执行 BGSAVE或BGREWRITEAOF(避免子进程内存竞争);
    既服务器目前没有在执行
    BGSAVE
    命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1.
    负载因子≥5且执行BGSAVE或者BGREWRITEAOF
    既服务器目前在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5.
    负载因子可以通过:
 load_factor = ht [0].used / ht[0].size

得出。如:对于一个大小为4,包含键值对的哈希表,其负载因子为:4/4=1.

  • ‌缩容条件‌:负载因子 <0.1。

5 . 对象集合

redis的任意数据类型都会被封装为一个redis Object对象,

1.、数据结构

typedef struct redisObject {
    unsigned type:4;      // 数据类型(如 REDIS_STRING、REDIS_HASH)
    unsigned encoding:4;  // 编码方式(如 REDIS_ENCODING_INT、REDIS_ENCODING_HT)
    unsigned lru:24;      // LRU/LFU 缓存淘汰信息
    int refcount;         // 引用计数
    void *ptr;            // 指向实际数据的指针
} robj;

Type:对象类型,占4个bit位;

type-redis
encoding:底层编码方式,占4个比特位;
encoding-redis0
encoding-reids1

encoding-redis2
LRU:缓存淘汰信息,占用24个bit位,
Object idletime key可以查看空转时间:
LRU-redis
refcount:引用计数;

  • 在创建一个新对象时,引用计数的值会被初始化位1;
  • 当一个对象被一个新程序引用时,它的引用计数值会+1;
  • 当对象不再被一个程序使用时,它的引用计数值会-1;
  • 当对象的引用计数值变为0时,对象所占用的内存会被回收;

2、数据类型

1. 字符串对象(string)

基本编码方式是RAW,基于简单动态字符串(SDS)实现,存储上限位512mb。

redis-string

如果字符串长度小于44字节,则使用embstr编码的方式来保存这个字符串值,此时RedisObject和SDS是一段连续空间申请内存时只需要调用一次内存分配函数。

表示一个44字节的数,Redis内存分配会以2的n次方去分配,SDS头占3个字节,'\0’占用1个字节,buf数组占用44字节,总共48字节,然后RedisObject占16个字节,正好64字节=2的6次方;

如果存储的字符串是整数值,并且大小在LONG_MAX范围内,则采用INT编码,直接将数据保存在RedisObject的prt指针里;

int-string-raw

2. 列表对象(list)

‌Redis 3.2 之前‌: ziplist(压缩列表):元素较少且较小时使用,内存连续紧凑,存储上限低。linkedlist(双向链表):内存占用较高,内存碎片多,元素较多或较大时使用,支持高效头尾操作。当元素小于512且元素大小小于64字节时采用Ziplist,超过则采用linklist;

‌Redis 3.2 及之后‌:quicklist:默认实现,结合 ziplist 和 linkedlist 的优点,分段存储以平衡内存和性能。

‌Redis 7.0及之后‌: quicklist 底层 ziplist 替换为 listpack(更安全的压缩列表结构)

1

应用场景‌

‌消息队列‌: 生产者通过 LPUSH 插入任务,消费者通过 RPOP 获取任务(FIFO)。
缺陷:无持久化保证,可能丢失数据4。
‌最新动态‌: 结合 LPUSH 和 LTRIM 实现固定长度的时间线(如最新 100 条消息)

3. 集合对象(set)

‌ 基本特性‌
‌无序性‌:集合中的元素无固定顺序,不支持索引访问。
‌唯一性‌:元素不允许重复,自动去重。
‌二进制安全‌:元素可以是字符串或整数。
‌容量上限‌:最多存储 2³² - 1 个元素(约 40 亿)

数据结构实现及转换规则
Set对查询元素的效率要求非常高,使用Hashtable实现,采用HT编码,Dict的key用来存储元素,value统一为null,
当存储的所有数据为整数,并且元素数量不超过set-max-intset-entires,set会采用Intset编码,以节省内存;

2
3
4

应用场景

‌标签系统‌:
存储用户标签(如 SADD user:1:tags “tech” “python”)。
‌去重统计‌:
记录独立访客 IP(SADD unique_ips 192.168.1.1)。
‌社交关系‌:
计算共同关注(SINTER user:1:follows user:2:follows)
在这里插入图片描述

4. 有序集合对象(zset)

zset即:SortedSet,其中每一个元素都需要指定一个score值和member值;
基本特性‌

  • ‌有序性‌:元素按 score(分数)排序,score 相同时按字典序排列。
  • ‌唯一性‌:元素(member)不可重复,但 score可重复,可以根据member查分数。
  • 高效操作‌:增删查时间复杂度为 O(logN),基于跳表实现。 ‌
  • 容量上限‌:最多存储 2³² - 1 个元素(约 40亿)。

因此zset底层数据必须满足键值存储、键唯一、可排序这几个需求,而sikplist可以排序,并且可以同时score、ele值(member);HT(dict)支持键值存储,并且可以根据key找value。
底层编码‌
‌ziplist(压缩列表)‌

适用于元素较少且较小时,内存连续紧凑。

触发条件

元素数量 ≤ zset-max-ziplist-entries(默认 128)
元素大小 ≤ zset-max-ziplist-value(默认 64 字节)


‌skiplist(跳表) + dict(字典)‌

默认实现,跳表维护有序性,字典实现 O(1) 的 member 查询。

数据结构

// 跳表节点结构
typedef struct zskiplistNode {
    sds ele;                      // 元素值(如 "Alice")
    double score;                 // 分数(如 100)
    struct zskiplistNode *backward; // 后退指针
    struct zskiplistLevel {
        struct zskiplistNode *forward; // 前进指针
        unsigned long span;        // 跨度
    } level[];                    // 多层索引
} zskiplistNode;

5
当元素数量不多时,上述结构优势不明显,且更耗内存,因此zset还采用Ziplist结构来节省内存,不过需要满足:

1、元素数量小于zset-max-Ziplist-entires,默认值128;
2、每个元素都小于zset-max-Ziplist-value字节,默认值64。

zset1

附上源码:
6
7
8

应用场景‌

‌排行榜‌: 游戏积分榜(ZADD 更新分数,ZREVRANGE 获取 TopN)。
‌延时队列‌: 用时间戳作为score,ZRANGEBYSCORE 获取到期任务。
‌带权重调度‌: 优先级任务队列(score 表示优先级)。

5. 哈希集合对象(hset)
// 哈希表结构
typedef struct dictEntry {
    void *key;              // 字段名(如 "name")
    union {
        void *val;          // 字段值(如 "Alice")
        uint64_t u64;
        int64_t s64;
    } v;
    struct dictEntry *next; // 解决哈希冲突
} dictEntry;

哈希对象编码可以是ziplist和hashtable:
ziplist
HT

3

编码转换函数

void hashTypeConvert(robj *o, int enc) {
    if (o->encoding == OBJ_ENCODING_ZIPLIST) {
        // 1. 创建新字典(hashtable)
        dict *d = dictCreate(&hashDictType, NULL);
        
        // 2. 遍历 ziplist,将字段和值插入字典
        unsigned char *zl = o->ptr;
        unsigned char *field, *value;
        unsigned int field_len, value_len;
        long long field_ll, value_ll;
        
        ziplistGetPairs(zl, &field, &field_len, &value, &value_len);
        while (field != NULL) {
            // 插入字段到字典
            dictAdd(d, field, value);
            ziplistGetPairs(zl, &field, &field_len, &value, &value_len);
        }
        
        // 3. 释放原 ziplist,更新对象编码
        zfree(o->ptr);
        o->ptr = d;
        o->encoding = OBJ_ENCODING_HT;
    }
}
robj *hashTypeLookupWriteOrCreate(client *c, robj *key)
		// 查找key
		robj *o = _lookupKeyWrite(c->db,key);
		if (checkType(c,o,OBJ_HASH)) return NULL;
		//不存在,则创建新的
		if (o == NULL) {
			o = createHashObject();
			dbAdd(c->db,key,o);
		}
		return o;
}

robj *createHashObject(void){
		// 默认采用ZipList编码,申请ZipList内存空间
		unsigned char *zl = ziplistNew();
		robj *0 = createObject(OBJ_HASH,zl);
		//设置编码
		o->encoding = OBJ_ENCODING_ZIPLIST;
		return o;
}

触发转换的逻辑判断

void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
    	int i;
   		size_t sum = 0;
    	if(o->encoding != OBJ_ENCODING_ZIPLIST) return;
    	//依次遍历命令中的field、value参数,// 依次遍历命令中的field、value参数
    	for (i = start; i <= end; i++) {
    	if (!sdsEncodedobject(argv[i]))
    		continue;
    		size_t len = sdslen(argv[i]->ptr);
    	// 如果field或value超过hash_max_ziplist_value,则转为HT
    	if (len > server.hash_max_ziplist_value){
    		hashTypeConvert(o, OBJ_ENCODING_HT);
    		return;
		}
		sum += len;
		}//ziplist大小超过1G,也转为HT
		if (!ziplistSafeToAdd(o->ptr, sum))
			hashTypeConvert(o, OBJ_ENCODING_HT);
}

应用场景‌

‌对象存储‌: 用户信息(如 HSET user:1 name “Alice” age 30)。
‌缓存更新‌:支持部分字段修改,避免序列化整个对象。
‌配置管理‌: 存储动态配置项,支持快速读写。

总结

以上是redis数据结构类型的学习总结,RedisObject中的refcount和lru属性设计到Reids底层的内存回收机制,将在下节展开,瑞思拜!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值