官网: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的使用场景:
- 存储:Redis的String类型可以用于存储数据。你可以将经常访问的数据存储在Redis中,并设置过期时间,这样可以减少数据存储的负载并提高系统性能。例如,你可以经常查询的数据数据库结果、API调用结果或计算结果存储在Redis中,接下来需要时直接从Redis中读取,避免重复计算或查询。
- 计数器: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还提供了一系列的命令和操作,可以对链表进行元素的增删改查、范围查询、阻止式弹出等操作。这些操作都是基于实体链表的特点来实现的。
使用场景
- 消息队列:Redis的List可以初始化简单的消息队列。生产者可以将消息添加到List的尾部,而消费者可以从List的头部弹出消息。这种方式可以实现简单的消息发布和订阅功能。
- 实时聊天:列表可以用于实现实时聊天应用程序中的消息存储和传输。每个用户的聊天记录可以存储在一个列表中,新的消息可以添加到列表的尾部,而用户可以从列表的头部开始获取最新的消息。
- 任务队列: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能够实现动态扩展和收缩哈希表的功能,同时保持对字典的稳定访问。这是为了在字典的运行过程中,能够高效地处理大量的键值对,同时保持较低的内存占用。
使用场景
- 唯一标识符和索引:哈希表可以用于唯一生成标识符,例如在全局系统中生成全局唯一的ID。它还可以用于索引数据,例如构建索引结构以优化搜索和排序操作。
- 字典和关联数据库:哈希表常用于实现字典和关联数据库的功能,其中每个元素都有一个唯一的键关联。这使得可以通过键快速查找、插入和删除元素。
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的查找、添加和删除操作都非常快速。
encoding
:一个uint32_t
类型的变量,用来存储编码方式。length
:一个uint32_t
类型的变量,用来存储集合包含的元素数量。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)是为了实现更高效的数据访问和操作。以下是使用字典的一些原因:
- 快速查找:在ZSet中,字典用于存储成员到分值的映射关系。通过使用字典,我们可以以O(1)的复杂度直接获取给定成员的分值。这在需要频繁查找成员分值的场景下非常高效。
- 成员的唯一性:在ZSet中,每个成员只能有一个分值。因此,使用字典可以确保成员的唯一性,避免重复插入相同成员的情况。
- 排序与范围查找:虽然跳跃表(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;
应用场景
- 排行榜: 社群集合可以用来实现排行榜功能。每个成员可以表示一个用户,而分值可以表示用户的积分、得分等。通过社群集合的分值排序功能,可以方便地获取排行榜的前几名或者某个用户的排名。
- 去重集合:群体集合的成员是唯一的,可以用来实现去重集合。通过将成员设置为相同,但分值不同,可以实现去重集合的效果。
- 分页: 通过区间实现分页