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

被折叠的 条评论
为什么被折叠?



