Redis实战之常用数据结构分析

一、常用数据结构列表
1.String:
简单的key-value类型,value可以为String,也可以为数字。使用场景有缓存系统下拉框值,保存分布式session,接口限流(利用过期机制expire,key自增机制incr),用户积分等。
2.List:
简单的字符串列表,数据结构类型为队列(FIFO),可以在队头或者对尾插入删除数据。常用使用场景有消息队列(lpop,rpush),排行榜,实时排行榜(Zsort也能实现)。
3.Hash:String类型field和value的映射表。使用场景有用户登录状态信息保存,购物车保存(属性频繁变化的对象)。
4.set:
String类型集合,元素无序且不重复。使用场景有系统白名单黑名单设置
5.Sort set:
String类型集合,元素有序且不重复。常用于实时排行榜等设计等
二、简单动态字符串SDS(simple dynamic string)

struct sdshdr {
// 记录buf数组中已使用字节的数量
// 等于SDS所保存字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
内存中展示如下:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190724134444162.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L05hbkd1b0h1YW5nRG91,size_16,color_FFFFFF,t_70)
free:0表示SDS没有分配任何未使用空间。
len:5 表示当前保存的字符串长度为5。
buf:char类型字符数组。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190724135159624.png)
上图不同的是这里free等于5表示字符数组中有5个单元长度未分配。
通过这种未使用空间,redis实现了空间预分配和惰性释放两种策略。
空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。
惰性释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用。(当SDS也提供了相应的API,当我们有需要时,也可以手动释放这些未使用的空间。)

三、List
链表节点定义:

typedef struct listNode {

// 前置节点
struct listNode * prev;

// 后置节点
struct listNode * next;

// 节点的值
void * value;

}listNode;

双向链表
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190724143745229.png)
使用list操作列表

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,而dup、free和match成员则是用于实现多态链表所需的类型特定函数:
● dup 函数用于复制链表节点所保存的值;
● free 函数用于释放链表节点所保存的值;
● match函数则用于对比链表节点所保存的值和另一个输入值是否相等
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190724144054122.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L05hbkd1b0h1YW5nRG91,size_16,color_FFFFFF,t_70)
四、字典
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。类似于java中HashMap

typedef struct dictht {

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

// 哈希表大小
unsigned long size;

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

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

} dictht;

节点定义

typedef struct dictEntry {

// 键
void *key;

// 值
union{
void *val;
uint64_tu64;
int64_ts64;
} v;

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

两者关系结构如下:
![在这里插入图片描述](https://img-blog.csdnimg.cn/2019072414502986.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L05hbkd1b0h1YW5nRG91,size_16,color_FFFFFF,t_70)![在这里插入图片描述](https://img-blog.csdnimg.cn/20190724145131933.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L05hbkd1b0h1YW5nRG91,size_16,color_FFFFFF,t_70)

当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面.

**哈希算法**
> 使用字典设置的哈希函数,计算键key的哈希值
> hash = dict->type->hashFunction(key);
> 使用哈希表的sizemask属性和哈希值,计算出索引值
> 根据情况不同,ht[x]可以是ht[0]或者ht[1] ndex = hash & dict->ht[x].sizemask;


**解决键冲突**

> 当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时,我们称这些键发生了冲突(collision)。
> Redis的哈希表使用链地址法(separate
> chaining)来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题。

![在这里插入图片描述](https://img-blog.csdnimg.cn/20190724162244200.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L05hbkd1b0h1YW5nRG91,size_16,color_FFFFFF,t_70)
**扩容和收缩**

> 当哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩。具体步骤:
>
>       1、如果执行扩展操作,会基于原哈希表创建一个大小等于 ht[0].used*2n 的哈希表(也就是每次扩展都是根据原哈希表已使用的空间扩大一倍创建另一个哈希表)。相反如果执行的是收缩操作,每次收缩是根据已使用空间缩小一倍创建一个新的哈希表。
>
>       2、重新利用上面的哈希算法,计算索引值,然后将键值对放到新的哈希表位置上。
>
>       3、所有键值对都迁徙完毕后,释放原哈希表的内存空间。

**触发扩容的条件**

>       1、服务器目前没有执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于1。
>
>       2、服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于5。
>
>     ps:负载因子 = 哈希表已保存节点数量 / 哈希表大小。 渐近式 rehash
>
>   扩容和收缩操作不是一次性、集中式完成的,而是分多次、渐进式完成的。如果保存在Redis中的键值对只有几个几十个,那么 rehash
> 操作可以瞬间完成,但是如果键值对有几百万,几千万甚至几亿,那么要一次性的进行
> rehash,势必会造成Redis一段时间内不能进行别的操作。所以Redis采用渐进式
> rehash,这样在进行渐进式rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行
> 增加操作,一定是在新的哈希表上进行的。

猜你喜欢

转载自www.cnblogs.com/jarvisblogs/p/11257828.html
今日推荐