1 字符串(String)
Redis中的字符串对象可以存储数值和字符串,并且redis中会使用使用C语言传统字符串表示字面量,使用 SDS (Simple Dynamic String,简单动态字符串)表示可修改的字符串。
1.1 SDS(Simple Dynamic String,简单动态字符串)
(1)定义
struct sdshdr {
// 记录 buf 数组中已使用字节的数量(SDS字符串的长度)
int len;
// 记录 buf 数组中未使用的长度(字节数量)
int free;
// 记录字符串内容
char buf[];
};
(2)优点
- 快速获取字符串的长度;(C传统字符串不保存字符串长度)
- 杜绝缓冲区溢出;(对SDS进行修改时,会先对空间进行判断)
- 减少内存重分配次数;(可以通过len和free的变化来确认保存的字符串)
- 二进制安全;(SDS使用二进制处理buf[]中的内容)
- 兼容部分C字符串函数。
1.2 使用哪一种数据结构
(1)long
- 字符串对象保存的是整数值, 并且这个整数值可以用 long 类型来表示。
(2)SDS
- 字符串对象保存的是一个字符串值。
1.3 字符串使用场景
(1)缓存功能
提高读写性能,降低数据库压力。
(2)计数器功能
快速实现计数和查询的功能。(例如点击数和浏览量等)
(3)共享Session功能
提高Seesion更新和获取的效率。(分布式系统中)
2 列表(List)
List为有序列表,Redis底层使用压缩列表(ziplist)或者链表(linkedlist)来实现列表。
2.1 压缩列表(ziplist)
压缩列表是 Redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构;一个压缩列表可以包含任意多个节点(entry), 每个节点可以保存一个字节数组或者一个整数值。
(1)定义
压缩列表
- zlbytes(uint32_t,4字节):记录整个压缩列表占用的内存字节数;(用于计算zlend和内存重分配)
- zltail(uint32_t,4字节):记录压缩列表表尾节点距离压缩列表的起始地址有多少字节;(可以获取尾节点地址)
- zllen(uint16_t,2字节):记录了压缩列表包含的节点数量;(当这个属性的值小于 UINT16_MAX (65535)时, 这个属性的值就是压缩列表包含节点的数量;当这个值等于 UINT16_MAX 时, 节点的真实数量需要遍历整个压缩列表才能计算得出)
- entryX(列表节点):压缩列表包含的各个节点,节点的长度由节点保存的内容决定;
- zlend(uint8_t,1字节):特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。
压缩列表节点
- previous_entry_length :以字节为单位(1字节过5字节), 记录了压缩列表中前一个节点的长,可用于从后往前遍历;(前一节点的长度小于 254 字节,previous_entry_length 的长度为 1 字节;前一节点的长度大于等于 254 字节, previous_entry_length 的长度为 5 字节)
- encoding:记录了节点的 content 属性所保存数据的类型以及长度;(字节数组:一字节、两字节或者五字节长, 值的最高位为 00 、 01 或者 10 ,后面为数组的长度;整数:一字节长, 值的最高位以 11 开头,后面为整数的长度)
- content:保存节点的值,类型和长度由 encoding 决定。
(2)连锁更新问题 - 添加新节点:当插入一个长度较大的节点时,可能导致后一节点的previous_entry_length无法满足条件,而实现连锁更新;
- 删除节点:当删除一个中间值时,可能导致后一节点的previous_entry_length无法满足条件,而实现连锁更新;
- 这些情况并不多,不必担心连锁更新会影响压缩列表的性能。
2.2 链表(linkedlist)
链表提供了高效的节点重排能力, 以及顺序性的节点访问方式, 并且可以通过增删节点来灵活地调整链表的长度。
(1)定义
链表
struct linkedlist{
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 链表所包含的节点数量
unsigned long len;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
};
链表节点
struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
};
(2)特点
- 双向链表(双端):链表节点带有prev和next指针;
- 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,链表以NULL结尾;
- 带表头节点和表尾节点:head、tail;
- 掉链表长度计数器:len;
- 多态:链表节点使用void*指针来保存节点值,并且可以通过linkedlist结构的dup、free和match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
2.3 使用哪一种数据结构
(1)ziplist
- 列表对象保存的所有字符串元素的长度都小于 64 字节;(可通过配置修改)
- 列表对象保存的元素数量小于 512 个。(可通过配置修改)
(2)linkedlist
- 不满足ziplist中的任一条件。
2.4 列表使用场景
(1)列表展示功能
如展示用户列表、粉丝列表和文章列表等。
(2)消息队列功能
使用列表左进右出的命令组合来完成队列的设计。
3 集合(Set)
Set 是不可重复的无序集合,Redis底层使用整数集合(intset)或者字典(hashtable)来实现集合。
3.1 整数集合(intset)
当一个集合只包含整数值元素, 并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现。
(1)定义
struct intset {
// 编码方式(数组的类型)
uint32_t encoding;
// 集合包含的元素数量(contents[]的长度)
uint32_t length;
// 保存元素的数组(各个项在数组中从小到大有序地排列, 并且数组中不包含任何重复项)
int8_t contents[];
};
(2)升级
每当我们要将一个新元素添加到整数集合里面, 并且新元素的类型比整数集合现有所有元素的类型都要长时, 整数集合需要先进行升级(upgrade), 然后才能将新元素添加到整数集合里面。
步骤
- 根据新元素的类型, 扩展整数集合底层数组的空间大小;
- 将底层数组现有的所有元素都转换成与新元素相同的类型, 并将类型转换后的元素放置到正确的位上, 而且在放置元素的过程中, 需要继续维持底层数组的有序性质不变;
- 将新元素添加到底层数组里面。(在新元素小于所有现有元素的情况下, 新元素会被放置在底层数组的最开头(索引 0 );在新元素大于所有现有元素的情况下, 新元素会被放置在底层数组的最末尾(索引 length-1 ))
好处
- 提升灵活性:可以存储任一类型;
- 节约内存:需要时才转换类型。
注意
- 整数集合不支持降级操作, 一旦对数组进行了升级, 编码就会一直保持升级后的状态。
3.2 字典(hashtable)
Redis 的字典使用哈希表作为底层实现, 一个哈希表里面可以有多个哈希表节点, 而每个哈希表节点就保存了字典中的一个键值对。
(1)定义
字典
struct dict {
// 类型特定函数(每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数)
dictType *type;
// 私有数据(保存了需要传给那些特定函数的可选参数)
void *privdata;
// 哈希表(一般情况下,字典只使用ht[0]哈希表,ht[1]只会在对ht[0]哈希表进行rehash时使用)
dictht ht[2];
// rehash 索引(记录了rehash目前的进度,当不rehash时为-1)
int rehashidx;
};
哈希表
struct dictht {
// 哈希表数组(存放的键值对)
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值(总是等于 size - 1)
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
};
哈希表节点
struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
};
(2)哈希算法
- 使用字典设置的哈希函数,计算键 key 的哈希值 :hash = dict->type->hashFunction(key);
- 然后使用哈希表的 sizemask 属性和哈希值,计算出索引值 :index = hash & dict->ht[x].sizemask;(根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1] )
- 最后将键值对放到对应的位置上。
(3)哈希冲突
- Redis 的哈希表使用链地址法(separate chaining)来解决键冲突。(添加数据时使用头插法)
(4)rehash
条件
- 扩展操作
1)服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
2)服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;
3)ht[1] 的大小为第一个大于等于 ht[0].used * 2 的2^n(2 的 n 次方幂)。 - 收缩操作
1)当哈希表的负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作;
2)ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n。
步骤
- 为字典的 ht[1] 哈希表分配空间;(取决于ht[0].used和操作)
- 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面;
- 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。
(5)渐进式rehash
rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。
步骤
- 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表;
- 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始;
- 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一;
- 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
注意点
- 因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行;( 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类)
- 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作。( 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表)
3.3 使用哪一种数据结构
(1)intset
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过 512 个。(可以通过配置修改)
(2)hashtable(ht)
- 不满足intset的任一条件;(字典的键为元素的值,字典的值都为NULL。)
3.4 集合使用场景
(1)去重功能
利用set集合的不可重复性,可以快速实时统计访问网站的独立IP。
(2)集合操作功能
Set 集合可以进行交集、并集、差集的操作,所以可以通过交集计算共同好友和共同爱好等。
4 有序集合(Zset)
Zset是排序的 Set集合,去重且可以排序,写进去的时候给定一个分数,redis自动根据分数排序。Redis底层通过压缩列表(ziplist)或者跳跃表(skiplist)来实现有序集合。
4.1 压缩列表(ziplist)
每个集合元素使用两个紧挨在一起的压缩列表节点来保存, 第一个节点保存元素的成员(member), 而第二个元素则保存元素的分值(score)。
注:压缩列表数据结构详见2.1。
4.2 跳跃表(skiplist)
跳跃表包含一个字典和一个跳跃表:
- 跳跃表(zsl ):按分值从小到大保存了所有集合元素, 每个跳跃表节点都保存了一个集合元素: 跳跃表节点的 object 属性保存了元素的成员, 而跳跃表节点的 score 属性则保存了元素的分值;(可以进行范围型操作)
- 字典(dict):为有序集合创建了一个从成员到分值的映射, 字典中的每个键值对都保存了一个集合元素: 字典的键保存了元素的成员, 而字典的值则保存了元素的分值;(O(1)复杂度查找到分值)
- zset 结构的跳跃表和字典会通过指针来共享相同元素的成员和分值, 所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值, 也不会因此而浪费额外的内存。
(1)定义
跳跃表
struct zskiplist {
// 表头节点和表尾节点(可进行双向遍历)
struct zskiplistNode *header, *tail;
// 表中节点的数量(跳跃表的长度,不包含表头节点)
unsigned long length;
// 表中层数最大的节点的层数(表头节点的层数不计算在内)
int level;
};
跳跃表节点
struct zskiplistNode {
// 后退指针(指向位于当前节点的前一个节点,每个节点只有一个后退指针)
struct zskiplistNode *backward;
// 分值(从小到大排列)
double score;
// 成员对象(保存的值)
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针(遍历用,用于访问位于表尾方向的其他节点)
struct zskiplistNode *forward;
// 跨度(记录了前进指针所指向节点和当前节点的距离,跨度实际上是用来计算排位(rank)的)
unsigned int span;
} level[];
};
4.3 使用哪一种数据结构
(1)ziplist
- 有序集合保存的所有元素成员的长度都小于 64 字节;(可通过配置文件修改)
- 有序集合保存的元素数量小于 128 个。(可通过配置文件修改)
(2)skiplist - 不满足ziplist的任一条件。
4.4 有序集合使用场景
(1)排行榜功能
利用set集合的不可重复性和有序性,可以实现播放榜和排名榜等
(2)加权队列
比如普通任务的score为1,重要任务的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
5 哈希(Hash)
类似 Map 的一种结构,一般存放结构化的数据(比如对象),Redis底层使用压缩列表(ziplist)或者字典(hashtable)来实现哈希。
5.1 压缩列表(ziplist)
redis会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾。(同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后)
注:压缩列表数据结构详见2.1。
5.2 字典(hashtable)
哈希对象中的每个键值对都使用一个字典键值对来保存。
注:字典数据结构详见3.2。
5.3 使用哪一种数据结构
(1)ziplist
- 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节;(可通过配置修改)
- 哈希对象保存的键值对数量小于 512 个。(可通过配置修改)
(2)hashtable(ht) - 不满足ziplist任一条件。
5.4 哈希使用场景
存储对象
例如存放购物车信息。(用户id为key,商品id为field,商品数量为value)
注:本人博客上写的所有东西,部分是来源于书籍,网络,其他的是个人的理解,感悟。本人博客均只用于个人学习、复习,不作为商业用途,如有侵权,请联系我修改或删除。