Redis数据结构原理总结

本文详细介绍了Redis中的五大数据结构:String、List、Set、Sorted Set和Hash,包括它们的底层实现、优缺点及使用场景。字符串类型支持存储二进制数据,列表和集合支持有序和无序的字符串元素,有序集合则根据分数排序,哈希存储键值对。Redis为这些数据结构使用了多种编码方式,如IntSet、ZipList、ListPack、HashTable和SkipList,以优化内存管理和查询效率。文章还探讨了Redis 6.0及7.0版本对数据结构的改进,如ListPack替代ZipList和SDS结构的优化。

一、Redis简介

        Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API,目前最新版本7.0。Redis提供了五种不同的数据结构来存储数据,分别是字符串(String)、列表(List)、集合(Set)、有序集合(Sorted Set)和哈希(Hash)。

  • 字符串(String):字符串是最基本的数据类型,可以存储任何类型的字符串,包括二进制、序列化的对象等。
  • 列表(List):列表是一个双向链表,支持从两端进行push和pop操作,内部是按照插入顺序排序的。
  • 集合(Set):集合是一个无序的、不允许有重复成员的字符串集合。
  • 有序集合(Sorted Set):有序集合是一个不允许有重复成员的字符串集合。不同的是每个成员都会关联一个分数(score),根据分数对成员进行从小到大的排序。
  • 哈希(Hash):哈希是一个键值对集合,适合存储小型的数据表。

  二、Redis底层结构

        Redis的数据结构有字符串、双端链表、字典、压缩列表、整数集合等,但是Redis为了加快读写速度,并没有直接使用这些数据结构,而是在此基础上又包装了一层称之为RedisObject,RedisObject 有五种对象:字符串(String)、列表(List)、集合(Set)、有序集合(Sorted Set)和哈希(Hash)。

①Redis内部统一实现 RedisObject有啥优点

  • 简化数据结构:RedisObject是Redis数据库的基本构建块,它将不同类型的值(字符串、列表、集合等)封装为一个统一的对象结构,简化了数据结构的设计。
  • 内存管理:RedisObject通过引用计数和内存回收机制管理内存,避免了额外的内存分配和释放开销。
  • 快速访问:通过使用RedisObject,Redis可以快速直接访问和操作不同类型的数据,提高了数据访问的效率。
  • 兼容性:RedisObject允许Redis支持不同的数据类型,并提供了一种在不同数据类型之间转换的机制。
  • 灵活性:RedisObject允许开发者根据需要扩展Redis的数据类型,以支持新的应用场景。

 ②RedisObject底层设计

typedef struct redisObject {
    // 类型
    unsigned type:4个bit;
    // 编码
    unsigned encoding:4个bit;
    // 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* LRU_BITS为24bit*/
//引用计数
    int refcount;4个字节
    // 指向实际值的指针
    void *ptr;8个字节
} robj;

下面分别解释一下各个字段的含义:

Ⅰ、type 记录了对象的类型占4个bit位,目前Redis支持的对象类型如下:

类型常量

常量对应的对象类型

OBJ_STRING

字符串(String)

OBJ_LIST

列表(List)

OBJ_SET

集合(Set)

OBJ_ZSET

有序集合(Sorted Set)

OBJ_HASH

哈希(Hash)

Ⅱ、ptr 指向对象的底层实现数据结构,即为具体对象值,占8个字节,这个后面会具体分析。

Ⅲ、encoding表示 ptr 指向的具体数据结构的编码方式,占4个bit位。Redis支持的编码如下:

编码常量

编码所对应的底展数据结构

OBJ_ENCODING_INT

long 类型的整数

OBJ_ENCODING_EMBSTR

embstr编码的简单动态字符串

OBJ_ENCODING_RAW

简单动态字符申

OBJ_ENCODING_HT

字典

OBJ_ENCODING_LINKEDLIST  

双端链表,Redis3.2之前使用

OBJ_ENCODING_ZIPLIST

压缩列表

OBJ_ENCODING_QUICKLIST

双端链表与压缩列表结合,Redis3.2之后使用

OBJ_ENCODING_INTSET

整数集合

OBJ_ENCODING_SKIPLIST

跳跃表

OBJ_ENCODING_ZIPMAP

压缩Map,Redis2.6之前使用

OBJ_ENCODING_STREAM

Stream流

         上面提到的type 用于标识 String、Hash、List、Set、Sorted Set五种数据类型、encoding 用于标识底层数据结构。通过这两个字段的组合,同一种数据类型也有多种实现方式,一个完整的映射关系如下表:

类型type

编码encoding

描述

OBJ_STRING

OBJ_ENCODING_INT

整数实现字符串对象

OBJ_STRING

OBJ_ENCODING_EMBSTR

embstr编码实现字符串对象

OBJ_STRING

OBJ_ENCODING_RAW

sds实现字符串对象

OBJ_LIST

OBJ_ENCODING_LINKEDLIST  

双端链表实现列表对象

OBJ_LIST

OBJ_ENCODING_ZIPLIST

压缩链表实现列表对象

OBJ_LIST

OBJ_ENCODING_QUICKLIST

双端链表+压缩链表相结合

OBJ_LIST

OBJ_ENCODING_LISTPACK

紧凑链表取代了压缩链表

OBJ_SET

OBJ_ENCODING_INTSET

整数集合实现集合对象

OBJ_SET

OBJ_ENCODING_HT

字典实现集合对象

OBJ_ZSET

OBJ_ENCODING_ZIPLIST

压缩链表实现有序集合对象

OBJ_ZSET

OBJ_ENCODING_SKIPLIST

跳跃表实现有序集合对象

OBJ_HASH

OBJ_ENCODING_ZIPLIST

压缩表实现Hash对象

OBJ_HASH

OBJ_ENCODING_HT

字典实现Hash对象

Ⅳ、lru:表示该对象最后一次被访问的时间,其占用24个bit位。便于判断空闲时间太久的key。
Ⅴ、refcount 表示引用计数,占四个字节,由于 C 语言并不具备内存回收功能,Redis 在自己的对象系统中添加了这个属性,当一个对象的引用计数为0时,则表示该对象已经不被任何对象引用,则可以进行垃圾回收了。

三、字符串(String)

        String是Redis中最常见的数据存储类型,它存储的是二进制安全数据,可以是数字、字符串或二进制数据。由于 Redis 的字符串是二进制安全的,因此可以用来存储图片、视频等二进制数据。其基本编码方式是有OBJ_ENCODING_INT、OBJ_ENCODING_EMBSTR、OBJ_ENCODING_RAW基于简单动态字符串SDS(simple dynamic string)实现,存储上限为512MB。如果存储的SDS长度小于44字节,则会采用EMBSTR编码,此时RedisObject与SDS是一段连续空间,申请内存时只需要调用一次内存分配函数,效率更高。

①使用场景

  • 对象存储,存储图片、视频等二进制数据;
  • 缓存数据,提高访问速度和降低数据库压力。
  • 计数器,利用 incr 和 decr 命令实现原子性的加减操作。
  • 分布式锁,利用 setnx 命令实现互斥访问。
  • 限流,利用 expire 命令实现时间窗口内的访问控制。

②SDS结构

        Redis的String类型的实际储存结构都是简单动态字符串SDS(Simple Dynamic Strings),OBJ_ENCODING_INT、OBJ_ENCODING_EMBSTR、OBJ_ENCODING_RAW三种编码方式只是负责储存元数据(字符长度、SDS指针等)

SDS包含3种编码类型

  • OBJ_ENCODING_EMBSTR:占用64Bytes的空间,存储44Bytes的数据
  • OBJ_ENCODING_RAW:存储大于44Bytes的数据
  • OBJ_ENCODING_INT:存储整数类型

        Redis 6.0版本相比Redis 5.0版本在SDS底层数据结构上进行了一些改进和优化。Redis 6.0中的SDS仍然包含三个成员变量len、free和buf,但是buf不再是一个字符数组,而是一个unsigned char类型的数组。此外,在Redis 6.0中新增了四个SDS类:sdsHdr5、sdsHdr8、sdsHdr16和sdsHdr32。这四个类分别代表SDS字符串的头部数据结构,用于存储SDS字符串的长度和空闲空间,以及标记SDS字符串的类型。

        Redis 6.0版本中的这些改进和优化可以提高SDS的效率和灵活性。通过使用unsigned char类型的数组来存储SDS字符串,可以更好地处理二进制数据和字符编码。而新增的四个SDS类可以更灵活地处理不同长度的SDS字符串,减少内存浪费。此外,为了提高效率,Redis 6.0版本中还对SDS的内存管理进行了优化,避免了频繁的内存分配和释放操作。

SDS类对象

typedef struct sdshdr {
//buf已保存的字符串字节数,不包含结束标示
uint32_t len; 
//buf申请的总的字节数,不包含结束标示
uint32_t alloc;  
//不同SDS的头类型,用来控制SDS的头大小
unsigned char flags; 
//用于存储字符串数据
char buf[];  
};

  • SDS_TYPE_5:这个比较特殊,注释上说未被使用。
  • SDS_TYPE_8: 当len 小于 1 << 8,也就是小于256时,变量len和alloc用uint8类型。
  • SDS_TYPE_16: 当len 小于 1 << 16,也就是小于65536时,变量len和alloc用uint16类型。
  • SDS_TYPE_32: 当len 小于 1 << 32,也就是小于4294967296时,变量len和alloc用uint32类型。
  • SDS_TYPE_64: 当len 大于等于 1 << 32,也就是大于等于4294967296时,变量len和alloc用uint64类型。

③EMBSTR底层数据结构

        当String的长度小于等于44字节时(旧版本39),Redis使用EMBSTR储存,此时RedisObject与SDS是一段连续空间,申请内存时只需要调用一次内存分配函数。

QA:为啥Redis底层使用44字节作为限制编码方式呢?

        对于RedisObject包装的统一对象结构大小是16字节,SDS对象头是3字节,字符串大小是44字节,对象尾是1字节,加起来是64字节,而Redis默认使用jemalloc而不是glibc的malloc内存分配算法,这种算法是2的幂次方去分配内存的,所以44字节正好符合Redis内存分配大小,避免了内存碎片的产生。

④INT底层数据结构

        当String存储的字符串是整数值,并且大小在LONG MAX范围内,则会采用INT编码: 直接将数据保存在RedisObject的ptr指针位置(刚好8字节),不再需要SDS了。

⑤RAW底层数据结构

        当String的长度大于44字节时,Redis使用RAW储存,此时RedisObject与SDS是两端内存,需要分配2次内存,性能较差,其中最大存储上限是512MB。

注:一旦转为RAW,就算进行删减操作使字节小于44字节,也不会再转回EMBSTR。

四、列表(List)

        列表是一种基于字符串的线性表数据结构,可以存储多个有序的字符串元素。列表适用于需要按照插入顺序排序的数据。从一开始早期版本使用 LinkedList(双端列表)和 ZipList(压缩列表)作为 List 的底层实现,到 Redis 3.2 引入了由 LinkedList + ZipList 组成的 QuickList,再到 7.0 版本的时候使用 ListPack 取代 ZipList。

①使用场景

  • Redis List用来创建消息队列。生产者可以使用LPUSH将任务推入列表,消费者可以使用BRPOP或BLPOP取出任务进行处理。
  • Redis List来记录用户的行为,比如浏览历史、消息通知等。通过LPUSH将动作推入列表,通过LRANGE获取用户的动作记录。
  • Redis List用来实现高效的分页。
  • Redis List用来实现有序的排行榜,通过ZRANGE获取排行榜。
  • Redis List用来实现时间轴功能。比如微博应用中用户发表一条微博就会被添加到自己的时间轴上,这个功能可以通过将每个用户的时间轴作为一个List来实现。
  • Redis List用来 延迟任务队列

②LINKEDLIST底层数据结构

        LinkedList是一个双向链表,链表中每个节点都会存储指向上一个节点和指向下一个节点的指针。LinkedList因为每个节点的空间是不连续的,遍历的效率低下,同时当存储数据很小的情况下,指针占用的空间会超过数据占用的空间,因此可能会造成过多的空间碎片。

当列表对象满足以下两个条件的时候,List 将使用 ZipList 存储,否则使用 LinkedList。

  • List 的每个元素的占用的字节小于 64 字节。
  • List 的元素数量小于 512 个。

 Ⅰ、LINKEDLIST类对象

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;
  • head指向链表的头节点
  • tail指向链表的尾节点
  • len表示这个链表共有多少个节点,这样就可以在O(1)的时间复杂度内获得链表的长度
  • dup函数,用于链表转移复制时对节点value拷贝的一个实现,一般情况下使用等号足以,但在某些特殊情况下可能会用到节点转移函数,默认可以给这个函数赋值NULL,即表示使用等号进行节点转移
  • free函数,用于释放一个节点所占用的内存空间,默认赋值NULL的话,可使用Redis自带的zfree()函数进行内存空间释放
  • match函数,用来比较两个链表节点的value值是否相等,相等返回1,不等返回0

Ⅱ、LINKEDLIST中链表中每个节点类对象

typedef struct listNode {    

// 前驱节点    
struct listNode *prev;    

// 后驱节点    
struct listNode *next;    

// 指向节点的值   
 void *value;

} listNode;

Ⅲ、LINKEDLIST底层数据结构

③ZIPLIST底层数据结构

        ZipList是经过特殊编码的双向链接列表,对于上面提到的LinkedList链表结构,由于内存中不是连续的,LinkedList多使用指针导致浪费内存空间、内存使用率都较低。为了解决这个问题,引入了 ZipList这种数据结构。 ZipList是一种有顺序、内存连续的数据结构。具备节省内存空间、提升内存使用率,适用于元素数量少且长度比较小的场景。在Redis 7.0版本之前是List、Hash、ZSet底层实现之一,但是自身也存在其他问题,因此在 Redis 7.0后被ListPack完全替换。

Ⅰ、ZIPLIST类对象

typedef struct ziplist{
     /*ziplist分配的内存大小*/
     uint32_t zlbytes;
     /*达到尾部的偏移量 */
     uint32_t zltail;
     /*存储元素实体个数*/
     uint16_t zllen;
     /*存储内容实体元素*/
     unsigned char* content[];
     /*尾部标识*/
     unsigned char zlend;
}ziplist;
  • zlbytes:压缩列表的字节长度。
  • zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量(目的是为了直接定位到尾节点,方便反向查询)。
  • zllen:压缩列表的元素个数。
  • entry:各个节点数据。
  • zlend:压缩列表的结尾,占一个字节,一直是0xFF(255)。

 Ⅱ、ZIPLIST中节点(entry)类对象

typedef struct ziplistEntry {
    unsigned int pre_entry_len; // 前一个entry的长度编码大小
unsigned char encoding; // 节点编码方式
    unsigned char *content; // 指向当前entry数据的指针(节点的起始指针)
} ziplistEntry;
  • pre_entry_length: 记录了前一个节点的长度,通过这个值,可以进行指针计算,从而跳转到上一个节点。
  • 根据编码方式的不同, pre_entry_length 域可能占用 1 字节或者 5 字节:1 字节:如果前一节点的长度小于 254 字节,便使用一个字节保存它的值。5 字节:如果前一节点的长度大于等于 254 字节,那么将第 1 个字节的值设为 254 ,然后用接下来的 4 个字节保存实际长度。
  • encoding表示编码类型

 字符串类型: 字符串类型有1、2、5三种编码长度,前两位表示编码类型,剩余位表示字符串长度。

00|aaaaaa:存储长度小于等于63byte的字符串。

01|aaaaaa bbbbbbbb:存储长度小于等于16383byte的字符串。

10|...... bbbbbbbb cccccccc dddddddd eeeeeeee:存储长度小于等于4294967295byte的字符串,'.'固定为0。

 整数类型:整数类型的编码长度统一位1字节。

1100 0000:表示16位有符号整数,content占用2byte。

1101 0000:表示32位有符号整数,content占用4byte。

1110 0000:表示64位有符号整数,content占用8byte。

1111 0000:表示24位有符号整数,content占用3byte。

1111 1110:表示8位有符号整数,content占用1byte。

1111 0001 - 1111 1101:没有content部分,依次表示整数0-12。

  • content: 保存当前entry节点数据,可以是字符串或整数。

 Ⅲ、ZIPLIST底层数据结构

 Ⅳ、ZIPLIST数据结构存在问题

  • 查询效率

        数据过多,导致链表过长,可能影响查询性能

  • 内存重分配&&连锁更新

        ZipList 在更新或者新增时候,如空间不够则需要对整个列表进行重新分配。当新插入的元素较大时,可能会导致后续元素的prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。

        假设有这样的一个ZipList,每个节点都是等于253字节的。新增了一个大于等于254字节的新节点,由于之前的节点prevlen长度是1个字节。为了要记录新增节点的长度所以需要对节点1进行扩展,由于节点1本身就是253字节,再加上扩展为5字节的pervlen则长度超过了254字节,这时候下一个节点又要进行扩展了。

 如下图:

 ④QUICKLIST底层数据结构

        QuickList 是 Redis 3.2 以后针对链表和压缩列表进行改造的一种数据结构,是 ZipList 和 LinkedList 的混合体,相对于链表它压缩了内存。进一步的提高了效率。QuickList 其实就是简单的双链表,但每个双链表节点中保存一个 ZipList ,然后每个 ZipList 中存一批 List中的数据 (具体 ZipList 大小可配置),这样既可以避免大量链表指针带来的内存消耗,也可以避免 ZipList 更新导致的大量性能损耗,将大的ZipList 化整为零。

Ⅰ、QUICKLIST类对象

typedef struct quicklist {
    quicklistNode *head; /* 头节点 */
    quicklistNode *tail; /* 尾节点 */
    unsigned long count; /* 在所有的ziplist中的entry总数 */
    unsigned long len; /* quicklist节点总数 */
    int fill : QL_FILL_BITS; /* 16位,每个节点的最大容量 */
    unsigned int compress : QL_COMP_BITS; /* 16位,quicklist的压缩深度,0表示所有节点都不压缩,否则就表示从两端开始有多少个节点不压缩 */
    unsigned int bookmark_count: QL_BM_BITS; /*4位,bookmarks数组的大小,bookmarks是一个可选字段,用来quicklist重新分配内存空间时使用,不使用时不占用空间*/
    quicklistBookmark bookmarks [];
} quicklist;
  • head指向链表的头节点
  • tail指向链表的尾节点
  • count表示在所有的ziplist 中的entry总数
  • len表示quicklistNode 节点总数
  • fill 表示每个 quicklistNode 节点的最大容量,不同的数值有不同的含义,默认是 -2,
  • 同时Redis提供了一个配置项可以修改该参数list-max-ziplist-size来限制。

fill

含义

-1

每个 quicklistNode 节点的 ziplist 所占字节数不能超过 4kb。

-2

每个 quicklistNode 节点的 ziplist 所占字节数不能超过 8kb。 (默认配置&建议配置)

-3

每个 quicklistNode 节点的 ziplist 所占字节数不能超过 16kb。

-4

每个 quicklistNode 节点的 ziplist 所占字节数不能超过 32kb。

-5

每个 quicklistNode 节点的 ziplist 所占字节数不能超过 64kb。

任意正数

表示:ziplist 结构所最多包含的 entry 个数,最大为 215215。

  • compress 表示 quicklist 的压缩深度,0 表示所有节点都不压缩,否则就表示从两端开始有多少个节点不压缩,同时Redis提供配置项list-compress-depth来控制。

compress

含义

0

特殊值,代表不压缩

1

标识QuickList的首尾各有1个节点不压缩,中间节点压缩

2

标识QuickList的首尾各有2个节点不压缩,中间节点压缩。

......

以此类推

  • bookmark_count :记录数组 bookmarks[] 的长度。Redis 高版本 6.0 才新加的。
  • bookmarks :quicklist重新分配内存空间时使用,否则只是声明不张占用内存空间。同样也是 6.0 后新增。

 Ⅱ、QUICKLIST中节点类对象

typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl; /* quicklist节点对应的ziplist */
    unsigned int sz; /* ziplist的字节数 */
    unsigned int count : 16; /* ziplist的entry数*/
    unsigned int encoding : 2; /* 编码方式,ziplist==1 or LZF==2 */
    unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* 这个节点以前压缩过吗? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* 未使用到的10位 */
} quicklistNode;
  • prev:前一个指针
  • next:后一个指针
  • zl:quicklist节点对应的ziplist指针
  • sz:当前节点ziplist的字节数
  • count :当前节点ziplist的entry数
  • encoding :编码方式,ziplist==1, LZF==2
  • container :数据容器类型(预留)1:其他 2:ziplist
  • recompress :是否被压缩 1:被压缩
  • attempted_compress:测试用
  • extra :预留字段

 Ⅲ、QUICKLIST底层数据结构

 ⑤LISTPACK底层数据结构

        ListPack(紧凑列表)时Redis5版本出现的,作用是为了代替ZipList 。Redis7.0版本之后,listpack就完全替代了ZipList 。ZipList 的缺点是,在极端的情况下,可能会出现连锁更新的情况,时间复杂度是O(N^2),会带来不小的性能消耗。ListPack最大的特点是,每一个节点不再包含前一个节点的长度,而压缩列表正是因为节点中包含前一个节点的长度,所以存在连锁更新的隐患。

 

 Ⅰ、LISTPACK类对象

typedef  struct listpack<T> {
    int32 total_bytes; // 占用的总字节数
    int16 size;        // 节点个数
    T[] entries;       // 节点列表
    int8 end;          // 同ziplist的zlend一样,恒为0xFF
}
  • total_bytes: listpack占用的总字节数
  • size:listpack节点个数
  • entries:listpack中节点数据列表
  • end:listpack结束符占用一个字节恒为0xFF

 Ⅱ、LISTPACK中节点类对象

typedef  struct lpentry { 
int<var> encoding; 
optional byte[] content; 
int<var> length; 
}
  • encoding:entry中数据的的编码方式

         ListPack使用1个字节,对其存储的数据制定了11种编码方式。7种用于整数和整数数字组成的字符串编码,内部通过整数的方式压缩存储;3种用户普通字符串数据编码,内部直接使用原字符串的方式进行存储;1种用来表示ListPack结束标识的编码。

  • content:存放当前节点的具体内容
  • length:存放当前节点的长度

 Ⅲ、LISTPACK底层数据结构

 

 ⑥ZIPLIST与LISTPACK区别

Ⅰ、结构组成不同

ZipList内存结构分了四个功能块:ZipList总长度,节点个数,最后节点的偏移和结尾标志;而ListPack只有三个功能块:ListPack总长度,节点个数和结尾标志,少了最后节点的偏移;

Ⅱ、数据长度不同

两者的包含节点个数使用的字节长度不一样ZipList是4个字节的uint32_t类型数据,而ListPack则是两个字节的unint16_t类型数据,单从这个数据的长度来看,ListPack能存储的数据个数是比ZipList少的。因为uint16_t能容纳的数据比uint32_t要少。

Ⅲ、两者节点结构不同

ZipList节点由三个组成部分分别是:前一个节点(entry)的长度数据,当前节点的编码类型(包含数据长度)和当前节点的数据内容;而ListPack节点由三个组成部分则是:当前节点的编码类型 (包含数据长度)、当前节点的数据内容和数据字节数。就是说ZipList保存了上一个节点的长度信息,而ListPack则保存了自己的长度信息。这两者有很明显的区别,而且这个区别,将影响两者操作的完全不同。

 五、集合(Set)

        Reids的Set是集合类型,可以保存多个字符串元素,集合中的元素不能重复,并且集合中的元素也是无序的,并且每个元素都是唯一的,没有重复元素,无法通过下标来获取集合中的元素,这些特性与Java的Set非常类似。Set适用于需要快速查找和删除的数据,例如用户标签、黑名单等。

        Redis的set底层使用了IntSet和HashTable两种数据结构存储的,其中IntSet可以理解为一种特殊的数组,而HashTable就是普通的哈希表。

        Set的底层存储IntSet和HashTable存在编码转换,使用IntSet存储必须满足下面两个条件,否则使用HashTable,条件如下:

  • 结合对象保存的所有元素都是整数值。
  • 集合对象保存的元素数量不超过512个。

①Set使用场景

  • Set中的数据是唯一的,而且插入和查询操作都是O(1)复杂度。适合存储需要去重的数据。
  • Set集合实现多个集合之间的交集、并集、差集等聚合操作,用于数据统计和分析。
  • Set集合可以随机选择元素后并从集合中删除,可以用于抽奖、随机推荐等功能。
  • Set集合实现计数器功能,使用IN CRBY命令计数。
  • ②IntSet底层数据结构

    •          Redis Set的底层存储采用整数集合IntSet 和哈希表,二者是相互转换的,使用 IntSet 存储必须满足下面两个条件,否则使用 HashTable,条件如下:

  • 结合对象保存的所有元素都是整数值;
  • 集合对象保存的元素数量不超过 512 个

 

 Ⅰ、InSet类对象

typedef struct IntSet{
     // 编码格式
     uint32_t encoding;
     // 集合中的元素个数
     uint32_t length;
     // 保存元素数据
     int8_t contents[];
} IntSet;
  • encoding:InSet编码方式。其中INTSET_ENC_INT16、INSET_ENC_INT32 和 INSET_ENC_INT64 三种,分别对应不用的范围,默认INSET_ENC_INT16。
  • length:InSet数组中元素个数,也就是数组的整体长度。
  • contents[]:整数集合,集合的每个元素都是数组的一个数组项(item)。按值的大小增序排列、不包含任何重复项。

 Ⅱ、IntSet底层数据结构

 ③HashTable(HT或字典)底层数据结构

        HashTable 也是 HT,即为字典,这是 Redis 的重点数据结构,也是 Hash 或者 Set 数据类型的底层实现之一。

        HashTable中的 key-value 是通过 dictEntry 对象来实现的,而HashTable将 dictEntry 进行了再一次的包装得到的HashTable对象 dictht。

Ⅰ、HashTable类对象

typedef struct dict{
         //类型特定函数
         void *type;
         //私有数据
         void *privdata;
         //哈希表(散列表)
         dictht ht[2];
         //rehash 索引 当rehash不在进行时 值为-1
         int rehashidx; 
}dict;
  • type:指向一个 dictType 结构的指针, 每个dictType 结构保存了一簇用于操作特作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型回调函数。
  • privdata :保存了需要传给那些特定类型函数看可选参数。
  • ht:包含两个数组,数组的每一项都是一个 dictht 哈希表,一般情况下字典只使用ht[0] 哈希表,ht[1]哈希表只会在对哈希表进行 rehash 时使用。
  • rehashidex: 记录了 rehash 当前的进度,如果没有进行 rehash, 值就为-1。

 dictType类对象

typedef struct dictType {
    unsigned int (*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;

Ⅱ、dictht(散列表)类对象

typedef struct dictht
{
         //哈希表数组
         dictEntry **table;
         //哈希表大小
         unsigned long size;
         //哈希表大小掩码,用于计算索引值
         unsigned long sizemask;
         //该哈希已有节点的数量
         unsigned long used;
}dictht;
  • table:散列表中每个节点数组
  • size:散列表中所有节点的总和
  • sizemask:散列表大小掩码,用于计算索引值
  • used:散列表已有节点的数量

 Ⅲ、dictht(散列表)节点dictEntry类对象

typedef struct dictEntry{
  //键
void *key;
//值
union{
     void *val;
     uint64_t u64;
      int64_t s64;
      double d;
    }v;
    // 指向下个节点,形成链表
    struct dictEntry *next;
}dictEntry;
  • key:实际存储键值。
  • union:联合体中实际存储数据值。
  • next:散列表下一个节点, 可以用来解决哈希冲突问题。

 Ⅳ、HashTable(HT或字典)底层数据结构

六、有序集合(Sorted Set)

        ZSet是一个特别的数据结构,一方面它等价于 Java 的数据结构 Map<String, Double>,可以给每一个元素 value 赋予一个权重 score,另一方面它又类似于 TreeSet,内部的元素会按照权重 score 进行排序,可以得到每个元素的名次,还可以通过 score 的范围来获取元素的列表。ZSet适用于需要按照分数排序的数据,例如评分排名、排行榜等。

       ZSet 底层数据结构分别由 ZipList(压缩列表) 和 SkipList(跳跃表)来实现。

       当ZSet对象同时满足一下两个条件时,ZSet对象使用ZipList编码。

  • 所有键值对的键和值的字符串长度都小于64字节
  • 键值对数量小于128个

 ①ZSet使用场景

  • 排行榜:使用ZSet来存储用户的分数(如积分),并且可以快速地获取分数最高的用户。
  • 消息队列:使用ZSet按照时间戳来排序需要处理的任务,然后按顺序获取和处理任务。
  • 计时器和定时任务:可以用ZSet来实现延时任务,过期元素会被自动移除。
  • session管理:在分布式环境下,可以使用ZSet来管理session,存储session的有效期。

②ZipList底层数据结构

        参考List数据类型中使用的ZipList数据结构。

③SkipList底层数据结构

        SkipList(跳跃表)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。链表加多级索引的的结构,就是跳跃表。

        数组支持随机访问的线性结构,底层使用二分查找。链表不支持随机访问的线性结构。跳跃表中查询任意数据的时间复杂度就是 O(logn)。

Ⅰ、ZSet类对象

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;
  • dict:字典(dict)数据结构,根据key获取元素值O(1)的复杂度,比如zscore命令 zscore key value 就可以拿到以key为键,value的对应的分值
  • zsl:跳跃表,只要用户排序。

        在Set类型中已经介绍过dict(字典)数据结构,这里就不赘述了,重点介绍SkipList(跳表)

SkipList(跳表)类对象

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;
  • header:指向跳表的头节点,通过这个指针可以直接找到表头,时间复杂度为O(1)
  • tail:指向跳表的尾节点,通过这个指针可以直接找到表尾,时间复杂度为o(1)
  • length:记录跳表的长度,即不包括头节点,整个跳表中有多少个元素
  • level:记录当前跳表内,所有节点中层数最大的level

 SkipList(跳表)中节点类对象

typedef struct zskiplistNode {
    sds ele;  //元素
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;
  • ele:真正的数据,每个节点的数据都是唯一的,但节点的分数score可以是一样的。两个相同分数score的节点是按照元素的字典顺序进行排列的
  • score:各个节点中的数字是节点所保存的分数score,在跳表中,节点按照各自所保存的分数从小到大排列;
  • backward:用于从表尾向表头遍历,每个节点只有一个后退指针,即每次只能后退一步;
  • zskiplistLevel:层级数组,这个数组中的每个节点都有两个属性,forward指向同层的下一个节点,span跨度用来计算当前节点在跳表中的一个排名,这就为zset提供了一个查看排名的方法。数组中的每个节点中用1、2、3等字样标记节点的各个层,分别L1代表第一层,L2代表第二层,L3代表第三层。。。

Ⅱ、SkipList数据结构分析

        对链表进行改造,在链表上建索引,即每两个结点提取一个结点到上一级,我们把抽出来的那一级叫作索引。这种链表加多级索引的结构,就是跳表。

        SkipList是一个“概率型”的数据结构,可以在很多应用场景中替代平衡树。SkipList算法与平衡树相比,有相似的渐进期望时间边界,但是它更简单,更快,使用更少的空间。 SkipList是一个分层结构多级链表,最下层是原始的链表,每个层级都是下一个层级的“高速跑道”。

        跳跃表(SkipList)是一种可以替代平衡树的数据结构。跳跃表让已排序的数据分布在多层次的链表结构中,默认是将Key值升序排列的,以 0-1 的随机值决定一个数据是否能够攀升到高层次的链表中。它通过容许一定的数据冗余,达到 “以空间换时间” 的目的。跳跃表的效率和AVL相媲美,查找/添加/插入/删除操作都能够在O(LogN)的复杂度内完成。

跳表的特点:

  • 跳跃表的每一层都是一条有序的链表。
  • 维护了多条节点路径。
  • 最底层的链表包含所有元素。
  • 跳跃表的空间复杂度为 O(n)。
  • 跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
  • 跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树。

 单链表跳表对比

  • 单链表平面结构

  • 跳表平面结构

  • 单链表与调表对比

       单链表是有序的,查询一个元素的时间复杂度为O(n),不能通过二分查找的方式缩减时间复杂度。 

      跳表(SkipList)是一种多层链表的有序数据结构,每个节点中维持多个指向其他层级节点的指针,从最上层顺序查询可以类似二分查找,达到快速访问节点的目的。

Redis中跳表内部实现

Ⅲ、ZSet底层数据结构

七、哈希(Hash)

        Hash是一种键值对集合,其中每个键都可以映射到一个或多个字段和值。哈希类型适用于存储对象,例如用户信息、商品详情等。通过使用哈希,可以更方便地对数据进行操作和查询。

        Hash的底层数据结构可以使用ZipList(压缩列表)和HashTable,在 Redis 7.0 中,ZipList数据结构已经废弃了,交由 ListPack 数据结构来实现了。当Hash对象同时满足一下两个条件时,哈希对象使用ZipList编码。

  • 所有键值对的键和值的字符串长度都小于64字节
  • 键值对数量小于512个

①Hash使用场景

  • 存储用户信息:如果你需要存储用户信息,如用户名、邮箱、年龄等,可以使用 Redis Hash 存储。
  • 缓存对象:如果你有一个对象需要频繁更新和访问,可以将这个对象的属性以 Hash 的形式存储到 Redis 中。
  • 会话存储:可以使用 Redis Hash 存储用户会话信息,如用户的登录状态、购物车内容等。

②ZIPLIST底层数据结构

        Redis中Hash数据类型使用了两种编码格式:ZipList(压缩列表)、HashTable(哈希表),当节点数量小于512并且字符串的长度小于等于64字节时,会使用ZipList编码。其中在上面提到List和ZSet数据类型中同样也会使用到ZipList,当List数据类型的每个元素的占用的字节小于 64 并且List 的元素数量小于 512 个,会使用ZipList编码,否则由ZipList改变为QuickList或LinkedList;当ZSet的每个元素的占用的字节小于64并且元素个数小于128个,使用ZipList编码方式,否则使用SkipList编码方式

        Hash与List和ZSet数据类型在使用ZipList数据结构上是一致的,但是在entry节点信息中的content,Hash跟List和ZSet有点区别,Hash中key-value是分开存储的,如下图:

③HashTable底层数据结构

        参考Set类型中使用而HashTable数据结构。

 

 

<think> 首先,用户的问题是关于Redis数据结构中的bitset和geo。用户直接提到了“bitset”,但根据引用内容,Redis中实际使用的是Bitmap(位图),而Geo是地理空间数据类型。在引用[1]中明确提到Redis的特殊数据类型包括Bitmap、Geo和HyperLogLog。 用户的问题是:“我想了解Redis数据结构bitset和geo的相关信息 请问Redis数据结构bitset和geo的介绍、使用方法及特点”。所以,我需要覆盖Bitmap(位图)和Geo(地理空间)。 我的回答必须遵循系统级指令: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 在回答中引用的段落末尾自然地添加引用标识,例如:[^1] 用户可见层指令: - 回答结构清晰,帮助用户逐步解决问题,尽量保证回答真实可靠。 参考提供的引用: - 引用[1]: 介绍Redis的特殊数据类型包括Bitmap、Geo和HyperLogLog,它们基于已有数据结构实现。 - 引用[2]: 关于Geo的实现,Redis使用GeoHash编码,底层数据结构是Sorted Set。 - 引用[3]: Redis GEO类型的概述,包括命令如geoadd。 - 引用[4]: HLL(HyperLogLog)的相关信息,但用户问的是bitset和geo,所以我可能不需要这个。 - 引用[5]: 关于哈希数据类型的信息,包括一个Java代码示例,但用户问的是bitset和geo,所以这个可能不直接相关。不过,它提到了哈希的数据结构,但用户指定了bitset和geo。 用户的问题焦点是bitset和geo。在Redis中,Bitmap就是bitset的别名,Geo是地理空间类型。 结构化回答: - 回答应该清晰,逐步介绍。 - 先介绍Bitmap(位图)。 - 然后介绍Geo(地理空间)。 - 对于每个部分,包括介绍、使用方法(命令示例)、特点。 - 使用引用来支持信息,并在末尾添加引用标识。 - 生成相关问题。 内容大纲: 1. **介绍Bitmap(位图)**: - 基于引用[1],Bitmap是特殊数据类型之一,基于字符串实现。 - 用途:用于位级操作,如统计用户在线状态等。 - 特点:节省内存,适合大规模布尔值存储。 - 使用方法:Redis命令如SETBIT, GETBIT, BITCOUNT等。 2. **介绍Geo(地理空间)**: - 基于引用[1][2][3],Geo用于存储和查询经纬度信息。 - 底层数据结构:Sorted Set(引用[2])。 - 实现方式:使用GeoHash编码(引用[2])。 - 使用方法:命令如GEOADD, GEOPOS, GEODIST等(引用[3])。 - 特点:高效的地理位置查询。 添加引用: - 在相关句子末尾添加[^1], [^2]等。 生成相关问题: - 在回答结束时,添加"§§相关问题§§"部分,列出2-3个相关问题。 确保语言: - 使用中文。 - 结构清晰。 最终回答结构: - 开头:简要概述。 - Bitmap部分。 - Geo部分。 - 结尾:相关问题。 </think> ### Redis 数据结构:Bitmap(位图)和 Geo(地理空间)介绍与使用 Redis 是一个高性能的内存数据库,支持多种数据结构。其中,Bitmap(位图)和 Geo(地理空间)是两种特殊的数据类型,它们基于 Redis 的基础数据结构(如字符串和有序集合)实现,通过巧妙的算法扩展出独特的功能。下面我将逐步介绍它们的原理、使用方法及特点,确保内容真实可靠,并基于站内引用进行说明。 --- ### 1. Bitmap(位图) Bitmap 不是独立的数据结构,而是基于 Redis 的字符串类型实现的位级操作工具。它将字符串视为一个二进制位数组,每个位(bit)可以表示布尔值(0 或 1),适用于高效存储和操作大规模二进制数据。 #### 介绍与特点 - **原理**:Bitmap 使用字符串存储位序列,每个字节(8 位)对应一个字符。例如,字符串 "A"(ASCII 码 65)在二进制中是 `01000001`,Redis 允许直接操作这些位[^1]。 - **特点**: - **内存高效**:一个 Bitmap 最多可存储 2^32 位(约 512MB),但实际内存占用远低于存储等量布尔值的其他方式(如列表),适合统计用户在线状态、签到记录等场景。 - **操作高效**:支持位运算(如 AND、OR、XOR),时间复杂度通常为 O(n),其中 n 是位长度。 - **限制**:位索引从 0 开始,最大索引为 4,294,967,295(2^32 - 1)。超出范围的操作会返回错误。 #### 使用方法(Redis 命令示例) Bitmap 通过一组命令操作,所有命令都以 `BIT` 开头: - **设置位值**:`SETBIT key offset value` 将指定偏移量(offset)的位设置为 0 或 1。例如,设置用户 ID 100 的签到状态: ```bash SETBIT user:sign:2023 100 1 # 偏移量 100 设为 1,表示已签到 ``` - **获取位值**:`GETBIT key offset` 返回偏移量的位值。例如: ```bash GETBIT user:sign:2023 100 # 返回 1(表示已签到) ``` - **统计位值**:`BITCOUNT key [start end]` 计算指定范围内位值为 1 的数量。例如: ```bash BITCOUNT user:sign:2023 # 返回总签到次数 ``` - **位运算**:`BITOP operation destkey key [key ...]` 对多个 Bitmap 进行逻辑运算(如 AND、OR),结果存储到目标 key。例如,计算两个用户集的交集: ```bash BITOP AND common_users user:set1 user:set2 ``` Bitmap 的底层是字符串,因此也可以使用 `GET` 和 `SET` 命令直接操作整个位序列,但位操作命令更高效[^1]。 --- ### 2. Geo(地理空间) Geo 类型用于存储和查询地理位置信息(如经纬度),底层基于 Redis 的有序集合(Sorted Set)实现。它通过 GeoHash 编码将二维坐标转换为一维值,支持高效的地理查询。 #### 介绍与特点 - **原理**:Geo 使用 GeoHash 算法将经纬度编码为一个字符串(例如 `wx4g0b`),然后再将这个字符串作为有序集合的分数(score)存储。这样,地理坐标就被映射到一个值上,使得范围查询和距离计算变得高效[^2]。 - GeoHash 编码的精度可调:编码越长,位置越精确(如 12 位编码误差约 1 厘米)。 - 数学上,GeoHash 将经纬度 $(\lambda, \phi)$ 通过二分法编码,公式为: $$ \text{GeoHash} = f(\lambda, \phi, \text{precision}) $$ 其中 $f$ 是编码函数,precision 指定长度[^2]。 - **特点**: - **查询高效**:支持半径搜索、距离计算等,时间复杂度为 O(log n),适合实时应用(如附近的人、位置追踪)。 - **内存占用低**:基于有序集合存储,内存优化良好(相比自定义实现)。 - **精度可控**:默认误差 < 0.5%,可通过调整参数优化[^3]。 - **限制**:一个 Geo 集合最多存储 2^32 个元素(约 40 亿),适合中小规模数据集。 #### 使用方法(Redis 命令示例) Geo 命令以 `GEO` 开头,常用命令包括: - **添加位置**:`GEOADD key longitude latitude member` 将成员(如地点名)添加到 Geo 集合。例如,添加北京的位置: ```bash GEOADD cities 116.40 39.90 "Beijing" # 经度 116.40, 纬度 39.90 ``` - **查询位置**:`GEOPOS key member` 返回成员的经纬度。例如: ```bash GEOPOS cities "Beijing" # 返回 116.40, 39.90 ``` - **计算距离**:`GEODIST key member1 member2 [unit]` 计算两个成员间的距离,单位可选 m(米)、km(千米)等。例如: ```bash GEODIST cities "Beijing" "Shanghai" km # 返回约 1068(公里) ``` - **范围查询**:`GEORADIUS key longitude latitude radius unit` 查找指定半径内的成员。例如,搜索北京 100 公里内的城市: ```bash GEORADIUS cities 116.40 39.90 100 km ``` Geo 的底层是有序集合,因此也可以使用 `ZRANGE` 等命令操作,但 Geo 专用命令更直观[^3]。 --- ### 总结 - **Bitmap**:基于字符串,适用于二进制标记和统计(如用户活跃度),特点是小内存、高效位运算。 - **Geo**:基于有序集合,适用于地理位置服务(如地图应用),特点是高效查询和 GeoHash 编码。 这两种类型都体现了 Redis 的灵活性——通过扩展已有数据结构实现新功能,而不引入额外开销[^1]。在实际应用中,Bitmap 适合高并发计数,而 Geo 能处理实时地理数据。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值