redis 基本数据结构

官网:redis.io/

String

SDS(Simple Dynamic String)的buf阵列用来保存字符串的实际内容,len字段记录了buf阵列中已使用的字节数量,free字段记录了buf阵列中未使用的字节数量。这种设计使得SDS能够高效地处理字符串的增长和缩小,而被填充地进行内存的重新分配。

Redis的String类型使用SDS来存储字符串数据。当你在Redis中设置一个String类型的键值对时,Redis会根据字符串的长度来动态分配内存空间,把字符串的内容存储在SDS的buf内存中。当你对String类型的值进行修改时,Redis会根据需要自动扩展或收缩SDS的内存空间。

Redis的字符串类型还提供了一系列的命令和操作,可以对字符串进行拼接、截取、替换等操作。这些操作都是基于SDS数据结构的特点来实现的。

\0会从头到为遍历长度,去掉了\0作为识别字符串结束的方式,能够高效地处理字符串的增长和缩小。

SDS 的结构:

c 复制代码
struct sdshdr {

    // 记录 buf 数组中已使用字节的数量
    // 等于 SDS 所保存字符串的长度
    int len;

    // 记录 buf 数组中未使用字节的数量
    int free;

    // 字节数组,用于保存字符串
    char buf[];

};

使用场景

Redis中的String数据类型是最常用和灵活的数据类型之一,它可以用于多种场景。以下是一些Redis String的使用场景:

  1. 存储:Redis的String类型可以用于存储数据。你可以将经常访问的数据存储在Redis中,并设置过期时间,这样可以减少数据存储的负载并提高系统性能。例如,你可以经常查询的数据数据库结果、API调用结果或计算结果存储在Redis中,接下来需要时直接从Redis中读取,避免重复计算或查询。
  2. 计数器:Redis的字符串类型可以组成计数器。你可以使用Redis提供的INCR和DECR命令对字符串类型的值进行递增或递减操作。这在统计网站访问次数、计算资源使用量或用户积分等场景中非常有用有用。

具体场景有:验证码(自动过期),存放json,计数器(签到,阅读量等)。 这些只是Redis String的一些常见使用场景,实际上,Redis的String类型非常灵活,可以根据具体需求进行更多的应用和扩展。

List

List是一种节点链表数据结构,用于存储多个数组的元素。List的底层实现是由一系列的节点组成的节点链表。

c 复制代码
typedef struct listNode {

    // 前置节点
    struct listNode *prev;

    // 后置节点
    struct listNode *next;

    // 节点的值
    void *value;

} listNode;

每个节点包含了一个指向前一个节点的指针(prev)、一个指向后一个节点的指针(next),以及一个指向存储节点值的指针(value)。

Redis的List使用一个list结构体来管理整个链表,结构如下:

c 复制代码
typedef struct list {

    // 表头节点
    listNode *head;

    // 表尾节点
    listNode *tail;

    // 链表所包含的节点数量
    unsigned long len;

    // 节点值复制函数
    void *(*dup)(void *ptr);

    // 节点值释放函数
    void (*free)(void *ptr);

    // 节点值对比函数
    int (*match)(void *ptr, void *key);

} list;

list结构体中包含了指向链表头和尾部指针的指针(head和tail)、链表的长度(len),以及一些用于操作链表的函数指针。

通过链表的结构,Redis的List可以进行元素的插入、删除和删除操作。在头部或尾部插入元素的时间复杂度为O(1),在任意位置插入元素的时间复杂度为O (N),N为链表长度。

Redis的List还提供了一系列的命令和操作,可以对链表进行元素的增删改查、范围查询、阻止式弹出等操作。这些操作都是基于实体链表的特点来实现的。

使用场景

  1. 消息队列:Redis的List可以初始化简单的消息队列。生产者可以将消息添加到List的尾部,而消费者可以从List的头部弹出消息。这种方式可以实现简单的消息发布和订阅功能。
  2. 实时聊天:列表可以用于实现实时聊天应用程序中的消息存储和传输。每个用户的聊天记录可以存储在一个列表中,新的消息可以添加到列表的尾部,而用户可以从列表的头部开始获取最新的消息。
  3. 任务队列:List可以用于实现任务队列,例如后台任务的调度和执行。任务可以添加到List的尾部,而工作线程可以从List的头部获取任务进行处理。

Hash

Redis的集合(Set)是一种无序、不重复的数据结构,它在Redis内部被广泛应用,例如用于实现集合数据类型

  • dicttht结构体定义了哈希表的属性,包括哈希表带宽、哈希表大小、哈希表大小、缓存和已使用的节点数量。
c 复制代码
typedef struct dictht {

    // 哈希表数组
    dictEntry **table;

    // 哈希表大小
    unsigned long size;

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

    // 该哈希表已有节点的数量
    unsigned long used;

} dictht;

dictEntry结构体定义了哈希表中的每个节点的属性,包括键、值和指向下一个节点的指针。这样,多个节点可以通过链表的形式连接在一起。

c 复制代码
typedef struct dictEntry {

    // 键
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;

} dictEntry;

dict结构体定义了字典的属性,包括类型特定函数、存储数据、两个哈希表(ht[0]和ht[1])和rehash索引。其中,哈希表使用两个dicttht结构体,用于实现rehash操作,即扩展或收缩哈希表的过程。

c 复制代码
typedef struct dict {

    // 类型特定函数
    dictType *type;

    // 私有数据
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

} dict;

rehash

随着不断的操作,hash表中的键值对可能会增多或减少,为了让哈希表的负载因子保持在一个范围内,需要对 hash表进行扩容或收缩,收缩和扩容的过程就叫 rehash。rehash 过程如下:

为什么要有两个dictht

在Redis中,使用两个dictht结构体来实现哈希表是为了进行哈希表的扩展和收缩操作,这个过程称为rehash。

Redis在进行rehash时,会同时使用两个哈希表。其中一个哈希表(ht[0])是当前正在使用的哈希表,而另一个哈希表(ht[1])是扩展或收缩后的新哈希表。

使用两个哈希表的好处是可以在进行rehash时,不影响对字典的读取和写入操作。具体来说,当进行rehash时,Redis会将旧哈希表中的键值对逐个迁移到新哈希表中。这样,在rehash过程中,旧哈希表和新哈希表会同时存在,而不会中断对字典的访问。

一旦rehash完成,Redis会将新哈希表设置为当前使用的哈希表,并丢弃旧哈希表,节省内存空间。

因此,通过使用两个dictht结构体,Redis能够实现动态扩展和收缩哈希表的功能,同时保持对字典的稳定访问。这是为了在字典的运行过程中,能够高效地处理大量的键值对,同时保持较低的内存占用。

使用场景

  1. 唯一标识符和索引:哈希表可以用于唯一生成标识符,例如在全局系统中生成全局唯一的ID。它还可以用于索引数据,例如构建索引结构以优化搜索和排序操作。
  2. 字典和关联数据库:哈希表常用于实现字典和关联数据库的功能,其中每个元素都有一个唯一的键关联。这使得可以通过键快速查找、插入和删除元素。

Set

Set底层用两种数据结构存储,一个是hashtable,一个是inset。

其中hashtable的key为set中元素的值,而value为null

c 复制代码
typedef struct redisZSet {  
    double min;  
    double max;  
    unsigned long keys;  
    struct redisZEntry {  
        double score;  
        robj *member;  
    } *zarray;  
} redisZSet;

Redis的Set数据类型底层使用哈希表来存储元素,每个元素都会被转换为一个唯一的哈希值,这个哈希值用来确定元素在哈希表中的位置。如果不同的元素被哈希到同一个位置,它们会被存储在一个链表中。当查找一个元素时,Redis会先计算元素的哈希值,然后查找该位置的链表,以确定元素是否存在。添加和删除元素时,Redis会相应地更新哈希表和链表。这种实现方式使得Redis Set的查找、添加和删除操作都非常快速。

  1. encoding:一个 uint32_t 类型的变量,用来存储编码方式。
  2. length:一个 uint32_t 类型的变量,用来存储集合包含的元素数量。
  3. contents:一个 int8_t 类型的数组,用来存储集合中的元素。这个数组的长度会在使用时动态确定。

这种结构体通常用于实现动态数组或集合,可以存储多种类型的数据。在这里,它被用来存储整数(int8_t),但是可以通过修改 contents 数组的类型来存储其他类型的数据。同时,通过使用 uint32_t 类型来存储编码方式和元素数量,可以保证数据的一致性和准确性。

c 复制代码
typedef struct intset {
    
    // 编码方式
    uint32_t encoding;

    // 集合包含的元素数量
    uint32_t length;

    // 保存元素的数组
    int8_t contents[];

} intset;

好友关注-共同关注 点赞功能

Sorted Set

在Redis中,ZSet(有序集合)的数据结构被设计成包含一个字典(dict)是为了实现更高效的数据访问和操作。以下是使用字典的一些原因:

  1. 快速查找:在ZSet中,字典用于存储成员到分值的映射关系。通过使用字典,我们可以以O(1)的复杂度直接获取给定成员的分值。这在需要频繁查找成员分值的场景下非常高效。
  2. 成员的唯一性:在ZSet中,每个成员只能有一个分值。因此,使用字典可以确保成员的唯一性,避免重复插入相同成员的情况。
  3. 排序与范围查找:虽然跳跃表(zsl)用于维护元素的顺序和进行范围查找,但字典在排序和范围查找中起到辅助作用。字典可以快速定位给定分值范围内的成员,然后通过跳跃表进一步获取具体的成员信息。

当我们在Redis中执行数据同步操作时,例如复制或持久化数据,我们通常会用到以下命令:

  • ZADD:这个命令用于向ZSet添加新的成员和分值。在内部,这个命令首先会将新的成员和分值存储在字典中,然后会根据分值在跳跃表中插入新的节点。
  • ZINCRBY:这个命令用于增加或减少ZSet中某个成员的分值。这个命令首先会在字典中查找成员对应的分值,然后更新这个分值,并在跳跃表中相应地更新节点。
  • ZREM:这个命令用于删除ZSet中的某个成员。这个命令会在字典中查找成员对应的分值,并从跳跃表中删除对应的节点。

zset结构体:

  • zset是部落集合的主要结构体,包含了一个字典和一个跳转表。
  • 字典用于存储成员和对应的分值,实现了按成员取分值的 O(1) 复杂度操作。
  • 跳跃表用于按分值排序成员,并支持O(log N)复杂度的按分值定位成员操作和范围操作。
c 复制代码
/*
 * 有序集合
 */
typedef struct zset {

    // 字典,键为成员,值为分值
    // 用于支持 O(1) 复杂度的按成员取分值操作
    dict *dict;

    // 跳跃表,按分值排序成员
    // 用于支持平均复杂度为 O(log N) 的按分值定位成员操作
    // 以及范围操作
    zskiplist *zsl;

} zset;
  • zskiplistNode结构体:

    • zskiplistNode是跳转表中的节点,包含了后退指针、分值、成员对象和层信息。

    • 后退指针指向前一个节点,用于支持当前目录。

    • 分值表示成员的排序评分。

    • 成员对象是具体的数据对象,可以是字符串、整数等。

    • 层信息是一个可变容量的吞吐量,每个元素都包含前进指针和跨度。

      • 前进指针指向下一个节点,用于快速跳过多余的节点。
      • 跨度表示当前节点到下一个节点的距离,用于计算范围操作的区间大小。
c 复制代码
typedef struct zskiplistNode {

    // 后退指针
    struct zskiplistNode *backward;

    // 分值
    double score;

    // 成员对象
    robj *obj;

    // 层
    struct zskiplistLevel {

        // 前进指针
        struct zskiplistNode *forward;

        // 跨度
        unsigned int span;

    } level[];

} zskiplistNode;
  • zskiplist结构体:

    • zskiplist是跳转表的主要结构体,包含了表头节点、表尾节点、节点数量和最大层数。
    • 表头节点是最小的节点,表尾节点是最大的节点。
    • 节点数量表示跳转表中节点的个数。
    • 最大层数表示跳转表中节点的最大层数。
c 复制代码
typedef struct zskiplist {

    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;

    // 表中节点的数量
    unsigned long length;

    // 表中层数最大的节点的层数
    int level;

} zskiplist;

应用场景

  1. 排行榜: 社群集合可以用来实现排行榜功能。每个成员可以表示一个用户,而分值可以表示用户的积分、得分等。通过社群集合的分值排序功能,可以方便地获取排行榜的前几名或者某个用户的排名。
  2. 去重集合:群体集合的成员是唯一的,可以用来实现去重集合。通过将成员设置为相同,但分值不同,可以实现去重集合的效果。
  3. 分页: 通过区间实现分页

GEO

BloomFilter

参考文章

Redis 哈希Hash底层数据结构

Redis 五种数据结构以及三种高级数据结构解析

相关推荐
程序员大金1 小时前
基于SSM+Vue+MySQL的酒店管理系统
前端·vue.js·后端·mysql·spring·tomcat·mybatis
程序员大金2 小时前
基于SpringBoot的旅游管理系统
java·vue.js·spring boot·后端·mysql·spring·旅游
Pandaconda2 小时前
【计算机网络 - 基础问题】每日 3 题(十)
开发语言·经验分享·笔记·后端·计算机网络·面试·职场和发展
程序员大金3 小时前
基于SpringBoot+Vue+MySQL的养老院管理系统
java·vue.js·spring boot·vscode·后端·mysql·vim
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS网上购物商城(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
Ylucius3 小时前
JavaScript 与 Java 的继承有何区别?-----原型继承,单继承有何联系?
java·开发语言·前端·javascript·后端·学习
ღ᭄ꦿ࿐Never say never꧂4 小时前
微服务架构中的负载均衡与服务注册中心(Nacos)
java·spring boot·后端·spring cloud·微服务·架构·负载均衡
.生产的驴4 小时前
SpringBoot 消息队列RabbitMQ 消息确认机制确保消息发送成功和失败 生产者确认
java·javascript·spring boot·后端·rabbitmq·负载均衡·java-rabbitmq
海里真的有鱼4 小时前
Spring Boot 中整合 Kafka
后端