本文共 8360 字,大约阅读时间需要 27 分钟。
Redis入门 | 五大数据结构及其应用场景
转载声明: 文内部分文图转自微信公众号 : @Java3y
1. Redis简述
Redis是一个开源的,基于内存的数据结构存储,可用作于数据库、缓存、消息中间件。
1.1 为什么要用Redis?
Q: redis中存储数据是以key-value形式, 那么为什么不用java的map 容器代替呢?
答:
- java仅实现的是本地缓存, 如果有多台机器的话, 都需要各自保存一份数据, 不具有一致性
- Redis是分布式缓存, 每个机器都共享一份数据, 具有一致性
- java的map不是专业做缓存的, JVM的内存太大的时候容易挂掉, 且随着JVM结束而销毁, 一般用于临时存储数据, 且如果需要过期策略等机制都需要额外手写!
- redis是专业缓存的, 可以有几十个G做缓存, 且能存入硬盘, 一旦重启也可将数据恢复, 自带丰富的数据结构, 和过期策略等机制
为什么要用redis而不用map做缓存?
- Redis 可以用几十 G 内存来做缓存,Map 不行,一般 JVM 也就分几个 G 数据就够大了
- Redis 的缓存可以持久化,Map 是内存对象,程序一重启数据就没了
- Redis 可以实现分布式的缓存,Map 只能存在创建它的程序里
- Redis 可以处理每秒百万级的并发,是专业的缓存服务,Map 只是一个普通的对象
- Redis 缓存有过期机制,Map 本身无此功能
- Redis 有丰富的 API,Map 就简单太多了
1.2 为什么要用缓存?
缓 解 数 据 库 压 力 !
1.3 Redis 为什么这么快?
1s读 10000次 写5000次
具体原因:
-
操作内存 :直接操作内存速度会相对较快。
-
5种数据结构的存在
-
SDS : 它的定义是不单单是一个char 数组构成,每个sds都会比它真实占用的字符长度都长,通过一个空闲标识符表示sds当前空闲字符有多少,如此设计,在一定长度范围的内的字符串都可以使用此sds,而且不会频繁的进行内存分配,直到此sds不能容纳分配的字符串,如果遇到这种情况情况,才需要进行扩扩容;这是redis的最基础的,所有的redis k-v 中的字符串都是依托于sds;
-
dict : 类似java中的hashmap( 数组, 负载因子, hash算法 )
不过dict 有两个数组 其中一个用作扩容 ( dict的扩容是渐进式的,不会影响当前使用, 一点点迁移, 直到迁移完成 不像hashmap是new一个更大的然后全部移走 )
-
-
跳跃表 :
跳跃表使用了历史上最屌的算法:抛硬币
在跳跃表是由N层链表组成,最底层是最完整的的数据,每次数据插入,率先进入到这个链表(有序的),插入完成后,通过抛硬币的算法,判断是否将数据向上层跑,如果是1的话,就抛到上层,然后继续抛硬盘,判断是否继续向上层抛,直到抛出了0结束整个操作,每抛到一层的时候,如果当前层没有数据,就构造一个链表,将数据放进去,然后使用指针指向来源地址,就这样依次类推,形成了跳跃表,每次查询,从最上层遍历查询,如果找到就返回结果,否则就在此层找到最接近查询的值,将查询操作移到另外一层,就是刚才说到来源地址,所在层,重复查询 -
使用单线程+多路 I/O 复用模型
- 单线程足够简单,无论在redis的实现还是作为调用方,都不需要为数据并发提心吊胆,不需要加锁。
- 不会出现不必要的线程调度,多线程频繁切换上下文,也会带来很多性能消耗
- 多路 I/O 复用模型,说白了就是当一个请求来访问redis后,redis去组织数据要返回给请求,这个时间段,redis的请求入口不是阻塞的,其他请求可以继续向redis发送请求,等到redis io流完成后,再向调用者返回数据,这样一来,单线程也不怕会影响速度了
2. Redis数据结构
2.1 Redis Object
但要值得注意的是:Redis并没有直接使用上图的数据结构来实现key-value
数据库,而是基于这些数据结构创建了一个对象系统。
简单来说:Redis使用对象来表示数据库中的键和值。每次我们在Redis数据库中新创建一个键值对时,至少会创建出两个对象。一个是键对象,一个是值对象。
Redis中的每个对象都由一个RedisObject
结构来表示:
typedef struct redisObject{ // 对象的类型 unsigned type 4:; // 对象的编码格式 unsigned encoding:4; // 指向底层实现数据结构的指针 void * ptr; //.....}robj;
简单来说就是Redis对key-value
封装成对象,key是一个对象,value也是一个对象。每个对象都有type(类型)、encoding(编码)、ptr(指向底层数据结构的指针)等 来表示。
2.2 Redis数据结构
2.2.1 SDS
简单动态字符串(Simple dynamic string,SDS)
Redis中的字符串跟C语言中的字符串,是有点差距的。
Redis使用sdshdr结构来表示一个SDS值:
struct sdshdr{ // 字节数组,用于保存字符串 char buf[]; // 记录buf数组中已使用的字节数量,也是字符串的长度 int len; // 记录buf数组未使用的字节数量 int free;}
例子:
2.1.1使用SDS的好处
SDS与C的字符串表示比较
- sdshdr数据结构中用len属性记录了字符串的长度。那么获取字符串的长度时,时间复杂度只需要O(1)。
- SDS不会发生溢出的问题,如果修改SDS时,空间不足。先会扩展空间,再进行修改!(内部实现了动态扩展机制)。
- SDS可以减少内存分配的次数(空间预分配机制)。在扩展空间时,除了分配修改时所必要的空间,还会分配额外的空闲空间(free 属性)。
- SDS是二进制安全的,所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据。
2.2.2 链表
Redis中的链表是怎么实现的:
使用listNode结构来表示每个节点:
typedef strcut listNode{ //前置节点 strcut listNode *pre; //后置节点 strcut listNode *pre; //节点的值 void *value;}listNode
使用listNode是可以组成链表了,Redis中使用list结构来持有链表:
typedef struct list{ //表头结点 listNode *head; //表尾节点 listNode *tail; //链表长度 unsigned long len; //节点值复制函数 void *(*dup) (viod *ptr); //节点值释放函数 void (*free) (viod *ptr); //节点值对比函数 int (*match) (void *ptr,void *key);}list
具体的结构如图:
Redis链表的特性
Redis的链表有以下特性:
- 无环双向链表
- 获取表头指针,表尾指针,链表节点长度的时间复杂度均为O(1)
- 链表使用
void *
指针来保存节点值,可以保存各种不同类型的值
2.2.3 哈希表
声明:《Redis设计与实现》里边有“字典”这么一个概念,我个人认为还是直接叫哈希表比较通俗易懂。从代码上看:“字典”也是在哈希表基础上再抽象了一层而已。
在Redis中,key-value
的数据结构底层就是哈希表来实现的。对于哈希表来说,我们也并不陌生。在Java中,哈希表实际上就是数组+链表的形式来构建的。下面我们来看看Redis的哈希表是怎么构建的吧。
在Redis里边,哈希表使用dictht结构来定义:
typedef struct dictht{ //哈希表数组 dictEntry **table; //哈希表大小 unsigned long size; //哈希表大小掩码,用于计算索引值 //总是等于size-1 unsigned long sizemark; //哈希表已有节点数量 unsigned long used; }dictht
从结构上看,我们可以发现:Redis实现的哈希表和Java中实现的是类似的。只不过Redis多了几个属性来记录常用的值:sizemark(掩码)、used(已有的节点数量)、size(大小)。
同样地,Redis为了更好的操作,对哈希表往上再封装了一层(参考上面的Redis实现链表),使用dict结构来表示:
typedef struct dict { //类型特定函数 dictType *type; //私有数据 void *privdata; //哈希表 dictht ht[2]; //rehash索引 //当rehash不进行时,值为-1 int rehashidx; }dict;//-----------------------------------typedef struct dictType{ //计算哈希值的函数 unsigned int (*hashFunction)(const void * key); //复制键的函数 void *(*keyDup)(void *private, const void *key); //复制值得函数 void *(*valDup)(void *private, const void *obj); //对比键的函数 int (*keyCompare)(void *privdata , const void *key1, const void *key2) //销毁键的函数 void (*keyDestructor)(void *private, void *key); //销毁值的函数 void (*valDestructor)(void *private, void *obj); }dictType
所以,最后我们可以发现,Redis所实现的哈希表最后的数据结构是这样子的:
从代码实现和示例图上我们可以发现,Redis中有两个哈希表:- ht[0]:用于存放真实的
key-vlaue
数据 - ht[1]:用于扩容(rehash)
Redis中哈希算法和哈希冲突跟Java实现的差不多,它俩差异就是:
- Redis哈希冲突时:是将新节点添加在链表的表头。
- JDK1.8后,Java在哈希冲突时:是将新的节点添加到链表的表尾。
2.2.3.1 rehash的过程
下面来具体讲讲Redis是怎么rehash的,因为我们从上面可以明显地看到,Redis是专门使用一个哈希表来做rehash的。这跟Java一次性直接rehash是有区别的。
在对哈希表进行扩展或者收缩操作时,reash过程并不是一次性地完成的,而是渐进式地完成的。
Redis在rehash时采取渐进式的原因:数据量如果过大的话,一次性rehash会有庞大的计算量,这很可能导致服务器一段时间内停止服务。
Redis具体是rehash时这么干的:
- (1:在字典中维持一个索引计数器变量rehashidx,并将设置为0,表示rehash开始。
- (2:在rehash期间每次对字典进行增加、查询、删除和更新操作时,除了执行指定命令外;还会将ht[0]中rehashidx索引上的值rehash到ht[1],操作完成后rehashidx+1。
- (3:字典操作不断执行,最终在某个时间点,所有的键值对完成rehash,这时将rehashidx设置为-1,表示rehash完成
- (4:在渐进式rehash过程中,字典会同时使用两个哈希表ht[0]和ht[1],所有的更新、删除、查找操作也会在两个哈希表进行。例如要查找一个键的话,服务器会优先查找ht[0],如果不存在,再查找ht[1],诸如此类。此外当执行新增操作时,新的键值对一律保存到ht[1],不再对ht[0]进行任何操作,以保证ht[0]的键值对数量只减不增,直至变为空表。
2.2.4 跳跃表
跳跃表(shiplist)是实现sortset(有序集合)的底层数据结构之一!
跳跃表可能对于大部分人来说不太常见,之前我在学习的时候发现了一篇不错的文章讲跳跃表的,建议大家先去看完下文再继续回来阅读:
跳跃表:
Redis的跳跃表实现由zskiplist
和 zskiplistNode
两个结构组成。其中zskiplist保存跳跃表的信息(表头,表尾节点,长度),zskiplistNode则表示跳跃表的节点。
按照惯例,我们来看看zskiplistNode跳跃表节点的结构是怎么样的:
typeof struct zskiplistNode { // 后退指针 struct zskiplistNode *backward; // 分值 double score; // 成员对象 robj *obj; // 层 struct zskiplistLevel { // 前进指针 struct zskiplistNode *forward; // 跨度 unsigned int span; } level[];} zskiplistNode;
zskiplistNode的对象示例图(带有不同层高的节点):
示例图如下:
zskiplist的结构如下:
typeof struct zskiplist { // 表头节点,表尾节点 struct skiplistNode *header,*tail; // 表中节点数量 unsigned long length; // 表中最大层数 int level;} zskiplist;
最后我们整个跳跃表的示例图如下:
2.2.5 整数集合
整数集合(intset)是set(集合)的底层数据结构之一。当一个set(集合)只包含整数值元素,并且元素的数量不多时,Redis就会采用整数集合(intset)作为set(集合)的底层实现。
整数集合(intset)保证了元素是不会出现重复的,并且是有序的(从小到大排序),intset的结构是这样子的:
typeof struct intset { // 编码方式 unit32_t encoding; // 集合包含的元素数量 unit32_t lenght; // 保存元素的数组 int8_t contents[];} intset;
intset示例图:
2.2.6 压缩列表
压缩列表(ziplist)是 List 和 Hash 的底层实现之一。
如果list的每个都是小整数值,或者是比较短的字符串,压缩列表(ziplist)作为list的底层实现。
压缩列表(ziplist)是Redis为了节约内存而开发的,是由一系列的特殊编码的连续内存块组成的顺序性数据结构。
压缩列表结构图例如下:
下面我们看看节点的结构图:压缩列表从表尾节点倒序遍历,首先指针通过
zltail
偏移量指向表尾节点,然后通过指向节点记录的前一个节点的长度依次向前遍历访问整个压缩列表。
2.3 Redis中数据结构的对象
看这张图,觉不觉得就很好理解了?
2.4 Redis Object中指针的具体实现
2.4.1 字符串(stirng)对象 3个
在上面的图我们知道string类型有三种编码格式:
-
int:整数值,这个整数值可以使用long类型来表示
- 如果是浮点数,那就用embstr或者raw编码。具体用哪个就看这个数的长度了
-
embstr:字符串值,这个字符串值的长度小于32字节
-
raw:字符串值,这个字符串值的长度大于32字节
embstr和raw的区别:
- raw分配内存和释放内存的次数是两次,embstr是一次
- embstr编码的数据保存在一块连续的内存里面
编码之间的转换:
- int类型如果存的不再是一个整数值,则会从int转成raw
- embstr是只读的,在修改的时候回从embstr转成raw
2.4.2 列表(list)对象 2个
在上面的图我们知道list类型有两种编码格式:
- ziplist:字符串元素的长度都小于64个字节
&&
总数量少于512个 - linkedlist:字符串元素的长度大于64个字节
||
总数量大于512个
ZipList 编码的列表结构:
redis > RPUSH numbers 1 "three" 5 (integer) 3
LinkedList编码的列表结构:
LinkedList编码的列表结构
编码之间的转换:
- 原本是
ziplist
编码的,如果保存的数据长度太大或者元素数量过多,会转换成 linkedlist 编码的。
2.4.3 哈希(Hash)对象 2个
在上面的图我们知道Hash类型有两种编码格式:
- ZipList:key和value的字符串长度都小于64字节
&&
键值对总数量小于512 - HashTable:key和value的字符串长度大于64字节
||
键值对总数量大于512
ziplist编码的哈希结构:
压缩列表:
HashTable编码的哈希结构:
编码之间的转换:
- 原本是ziplist编码的,如果保存的数据长度太大或者元素数量过多,会转换成 hashtable 编码的。
2.4.4 集合(set)对象 2个
在上面的图我们知道set类型有两种编码格式:
- IntSet:保存的元素全都是整数
&&
总数量小于512 - HashTable:保存的元素不是整数
||
总数量大于512
intset编码的集合结构:
hashtable编码的集合结构:编码之间的转换:
- 原本是intset编码的,如果保存的数据不是整数值或者元素数量大于512,会转换成hashtable编码的。
2.4.5 有序集合(sortset)对象 2个
在上面的图我们知道set类型有两种编码格式:
- ZipList:元素长度小于64
&&
总数量小于128 - SkipList:元素长度大于64
||
总数量大于128
ziplist编码的有序集合结构:
压缩列表:
skiplist编码的有序集合结构:
有序集合(sortset)对象同时采用SkipList
和哈希表
来实现:
- skiplist能够达到插入的时间复杂度为O(logn),根据成员查分值的时间复杂度为O(1)
编码之间的转换:
- 原本是ziplist编码的,如果保存的数据长度大于64或者元素数量大于128,会转换成skiplist编码的。
2.4.6 Redis对象一些细节
- (1:服务器在执行某些命令的时候,会先检查给定的键的类型能否执行指定的命令。
- 比如我们的数据结构是sortset,但你使用了list的命令。这是不对的,服务器会检查一下我们的数据结构是什么才会进一步执行命令
- (2:Redis的对象系统带有引用计数实现的内存回收机制。
- 对象不再被使用的时候,对象所占用的内存会释放掉
- (3:Redis会共享值为0到9999的字符串对象
- (4:对象会记录自己的最后一次被访问时间,这个时间可以用于计算对象的空转时间。
3. 应用场景
转载地址:https://blog.csdn.net/weixin_40597409/article/details/115484138 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!