Redis基础篇
mhr18 2024-11-20 18:53 25 浏览 0 评论
一、从常用数据结构说起
上文说到Redis提供了丰富的数据结构,包括STRING(字符串)、LIST(列表)、SET(集合)、HASH(散列)和ZSET(有序集合)基本数据类型。
先来一波操作感受一下:
基本的数据类型的大概使用就到这里,接下来就分析一下它的内部结构是怎么实现的。
二、底层实现
Redis是KV类型的数据库,Key-Value我们一般会用什么数据结构存储?
哈希表!没错Redis的最外层确实也是通过hashtable实现的。在Redis里面每个键值对都是一个dictEntry,通过指针指向key的存储结构和value的存储结构,此外还有一个next存储里指向下一个键值对的指针。
typedef struct dictEntry {
void *key; //key void*表示任意类型指针
union {
void *val;//value定义
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; //next指针
} dictEntry;
看到这里大家会有疑惑,那过期时间放在哪里?
对,实际上在dicEntry的外面还有一层redisDB。
/* Redis数据库结构体 */
typedef struct redisDb {
// 数据库键空间,存放着所有的键值对(键为key,值为相应的类型对象)
dict *dict;
// 键的过期时间
dict *expires;
// 处于阻塞状态的键和相应的client(主要用于List类型的阻塞操作)
dict *blocking_keys;
// 准备好数据可以解除阻塞状态的键和相应的client
dict *ready_keys;
// 被watch命令监控的key和相应client
dict *watched_keys;
// 数据库ID标识
int id;
// 数据库内所有键的平均TTL(生存时间)
long long avg_ttl;
} redisDb;
外层说完了,接下来就以set hello world为例,逐步分析一下。
首先key是字符串,Redis自己实现了一个字符串类型叫SDS,后面我们会细说,所以hello指向一个SDS的存储结构。value是world,也是一个字符串,是不是也用SDS存储呢?
这里就要重点介绍以下了,由前文我们知道Redis的key都是字符串类型,value的类型有多种。那Redis如何是适配这多种类型呢?于是就封装了一层redisObject。而我们所说的Redis数据类型的任何一种,都是通过redisObject存储的。
我们来看一下redisObject的结构:
typedef struct redisObject {
//对象的数据类型,占4bits,共5种类型
unsigned type:4;
//对象的编码类型,占4bits,共10种类型
unsigned encoding:4;
//least recently used
//实用LRU算法计算相对server.lruclock的LRU时间
unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
//引用计数
int refcount;
//指向底层数据实现的指针
void *ptr;
} robj;
到这里就能看出一些奇妙的地方了,type是数据类型,encoding是编码类型,且数量不等。有意思了,那我们接下来看看这个encoding究竟是些什么。
127.0.0.1:6379> set number 123
OK
127.0.0.1:6379> object encoding number
"int"
127.0.0.1:6379> set story "long long ago,and long long ago,other long long ago"
OK
127.0.0.1:6379> object encoding story
"raw"
127.0.0.1:6379> set msg "hello world"
OK
127.0.0.1:6379> object encoding msg
"embstr"
到此我们大概看出来一些端倪,Redis的数据类型背后根据存储的数据不同使用的不同的编码存储。为什么要这样做呢?
节约存储空间!
其实远远不止这些,我们后面会详细说到。接下来就看每种数据结构有哪些编码类型。
三、数据结构详解
1、String
上文我们已经分析出了,String底层有三种。
int:存储8个字节的长整形
embstr:代表embsds格式的SDS,存储小于44字节的字符串
raw:存储大于44字节的SDS
接下来问题来了,什么是SDS呢?SDS全称是Simple Dynamic String,即简单动态字符串。
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 长度 */
uint8_t alloc; /* 分配的内存大小 */
unsigned char flags; /* 属性,标志不同种类的sds */
char buf[];/* 内容 */
};
本质上就是一个char数组!
那么既然是一个char数组,为什么要通过SDS实现呢?
SDS
很简单,因为C语言中没有字符串类型,只有char数组。但是使用char数组会有一些问题,什么问题呢,大家可以想一下。
1、必须分配足够的空间,否则可能会溢出
2、如果要获取长度,需要遍历数组,时间复杂度是O(n)
3、如果长度变更,需要重新内存分配
4、字符串规则是遇到第一个'/0'即为结束,因此不能存放二进制的内容
好了,既然有了这些问题,SDS是如何解决的呢?
1、SDS实现了动态扩容,无需担心溢出的问题
2、定义了len属性,获取长度时间复杂度是O(1)
3、通过空间预分配和惰性空间释放,放置了重新分配内存
4、判断结束标志示len属性,避免了二进制不安全
哇,到这里是不是感觉到Redis的编码格式设计的很巧妙。别着急,慢慢来,还有。
embstr和raw,为什么要设计两个编码格式呢?就是为了长度不同吗?SDS也已经满足了呀?
实际原因是embstr的使用,只分配了一次内存,redisObject和SDS是一起分配的,二raw是分配了两次内存
那这三种类型之间是怎么转换的呢?
1、int 不在时整形,转成raw
2、int大小超过long的范围,转成embstr
3、embstr超过44字节,转成raw
注意:不可回转!!!
到此,String的数据结构就介绍完毕了。最后大家都思考一下String的使用场景有哪些?
1、缓存:热点数据,提升检索效率;
2、数据共享:session共享多个服务器共用;
3、分布式锁:setNx方法,判断是否添加成功;
4、全局唯一ID:INT类型的incrby;
5、计数器:INT类型
6、限流:INT类型
哈哈,是不是感觉String好强大,不需要其他数据类型了?那么问题来了,如果我要存一个对象怎么办?举个例子,存一个学生信息,包括姓名、年龄、学号等信息。大家可能会说,String存储一个json就行了,没错可以实现。但是我如果只想获取学生的年龄呢?
接下来介绍的Hash类型,就是解决这个问题的。
2、Hash
Hash类型是指Redis键值对中的值本身又是一个键值对结构,形如value=[{field1,value1},...{fieldN,valueN}],hash的value只能是字符串,不能嵌套其他类型。同样是存储字符串,Hash和String有什么区别呢?如下图所示:
从上图可以明显看出:
1、把所有相关的值聚集到一个key中,节省内存空间
2、只使用一个key,可以有效减少key冲突
3、当需要批量获取值的时候,只需要使用一个命令,减少内存/IO/CPU
那么它的底层编码是如何实现的呢?是不是使用dicEntry实现的呢?我们先来操作一波:
127.0.0.1:6379> hset user1 name aaaaaaaaaaaaaaaaaaa
(integer) 1
127.0.0.1:6379> hset user2 name aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 1
127.0.0.1:6379> object encoding user1
"ziplist"
127.0.0.1:6379> object encoding user2
"hashtable"
可以看到,哈希类型的内部编码有两种:ziplist(压缩列表),hashtable(哈希表)。又有两个陌生的结构,不要慌张,我们下文会逐个分析。
ziplist
ziplist是一个经过特殊编码的,由连续的内存块组成的双向链表。它不存储指向上一个节点和下一个节点的指针,而是存储上一个节点的长度和当前节点的长度。这让数据在内存中更为紧凑,所以叫做zip,同时可以轻易地得到前驱后驱数据项的位置。
<zlbytes><zltail><zllen><entry>...<entry><zlend>
接下来我们具体看下实际元素里,究竟是怎么存储的。
typedef struct zlentry {
//prevrawlen 前驱节点的长度
//prevrawlensize 编码前驱节点的长度prevrawlen所需要的字节大小
unsigned int prevrawlensize, prevrawlen;
//len 当前节点值长度
//lensize 编码当前节点长度len所需的字节数
unsigned int lensize, len;
//当前节点header的大小 = lensize + prevrawlensize
unsigned int headersize;
//当前节点的编码格式
unsigned char encoding;
//指向当前节点的指针,以char *类型保存
unsigned char *p;
} zlentry;
那么什么时候使用ziplist呢?
hash对象保存的键值对数量<512所有键值对字符串长度小于54字节
如果任何一个条件不满足,存储结构就会转成hashtable。接下来介绍hashtable
hashtable
前面我们知道了,Redis的KV结构是通过dictEntry来实现的。在hashtable中,又对dictEntry进行了多层封装。
typedef struct dictht {
// 两个哈希表
dictEntry **table;
// 哈希表的大小
unsigned long size;
// 哈希表大小掩码
unsigned long sizemask;
// 哈希表中数据项数量
unsigned long used;
} dictht;
dictEntry放在了dictht(hashtable)里面了
typedef struct dict {
// 哈希表的类型,包括哈希函数,比较函数,键值的内存释放函数
dictType *type;
// 存储一些额外的数据
void *privdata;
// 两个哈希表
dictht ht[2];
// 哈希表重置下标,指定的是哈希数组的数组下标
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 绑定到哈希表的迭代器个数
int iterators; /* number of iterators currently running */
} dict;
dictht放在了dict里面了。
从源码可以看出,它是一个数组+链表的结构。如图:
我们发现dictht后面是个null,说明第二个hashtable没有数据。那么为什么要定义两个hashtable,其中一个不用呢?
答案是为了扩展哈希表!ht默认使用ht[0],ht[1]不初始化。hash表有一个普遍存在的问题就是hash冲突,如果冲突过多都会放置在上图的dicEntry数组里。这样hash的读性能就退化称数组的遍历,效率降低。解决hash冲突的有效手段就是对hash进行扩容,让数据足够分散。这个过程会进行一次rehash,这个过程中就会使用到ht[1]。首先是初始化ht[1]的大小为ht[0]的最小n次幂。然后将ht[0]的数据写入ht[1],然后再将ht[0]释放。怎么样这个设计过程是不是很巧妙?
使用场景:
1、和String一样!毕竟hash存储的也是String。
2、存储对象类型的数据:比string节省key。
3、购物车:key:用户id,filed:商品id,value:数量。增加、减少、删除等都可以操作。
3. List
List主要用来存储有序数据,数据可重复。
先来操作一波:
127.0.0.1:6379> lpush queue a (integer) 1 127.0.0.1:6379> lpush queue b c (integer) 3 127.0.0.1:6379> object encoding queue "quicklist"
可以看到list的底层结构是quicklist来实现的。接下来就介绍一下quicklist。
quicklist
quicklist 实际上是 ziplist 和 linkedlist 的混合体,它将 linkedList 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。
typedef struct quicklist {
quicklistNode *head;//指向第一个quicklistNode
quicklistNode *tail;//指向最后一个quicklistNode
unsigned long count; //在所有ziplist中entry的个数总和
unsigned int len;//quicklistNode的个数
int fill : 16; //ziplist大小限定,由server.list_max_ziplist_size给定
unsigned int compress : 16; //节点压缩深度设置,由server.list-compress-depth给定,0表示关闭压缩
} quicklist;
看到里面用到了一个quicklistNode存储数据。
typedef struct quicklistNode {
struct quicklistNode *prev; //上一个node节点
struct quicklistNode *next; //下一个node
unsigned char *zl; //保存的数据 压缩前ziplist 压缩后压缩的数据
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
ziplist上文已经介绍了,就不多说了。总的来说,quicklist就是ziplist 和 linkedlist 的混合体。
应用场景:
1、列表:文章列表、热门排行榜之类的
2、队列/栈:list有两个阻塞操作:BLPOP/BRPOP
4、set
set存储String类型的无序集合,最大存储2^32-1。
先操作一波:
127.0.0.1:6379> sadd myset a b c
(integer) 3
127.0.0.1:6379> object encoding myset
"hashtable"
127.0.0.1:6379> sadd newset 1 2 3
(integer) 3
127.0.0.1:6379> object encoding newset
"intset"
可以看到set的编码格式有两种intset和hashtable。如果第一个原始是一个整数,就会初始化为intset,如果intset保存的值的数量大于512(set_max_intset_entries默认值)个,会转化称hashtable。
typedef struct intset {
uint32_t encoding;//数组中的值的编码方式
uint32_t length;//数组长度
int8_t contents[];//数组,记录每个值
} intset;
使用场景:
1、抽奖:spop
2、点赞、打卡:数据集无序,sadd添加,srem取消,sismember是否操作,smembers所有,scard总数
3、标签:sadd添加 sinter筛选
5、zset
sorted set存储的是有序的集合元素。它是为每个元素添加了一个score,按照score的大小排序。
操作一波:
127.0.0.1:6379> zadd myzset 1 java 2 redis 3 mysql
(integer) 3
127.0.0.1:6379> object encoding myzset
"ziplist"
127.0.0.1:6379> zadd myzsetnew 4 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 1
127.0.0.1:6379> object encoding myzsetnew
"skiplist"
可以看到,有序集合是由 ziplist (压缩列表) 或 skiplist (跳跃表)组成的。
当数据比较少时(<128),且所有元素长度都小于64字节,有序集合使用的是 ziplist 存储的,否则使用skiplist结构存储。
ziplist我们已经很熟悉了,那么问题来了,什么是skiplist?
skiplist
skiplist是一个特殊的链表,相比一般的链表,有更高的查找效率,其效率可比拟于二叉查找树。
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 头尾指针
unsigned long length; // skiplist的长度
int level; // 最高多少级链表
} zskiplist;
zskiplist的定义,没啥内容,就头尾指针、长度和级数,重点还是在zskiplistNode中。
typedef struct zskiplistNode {
sds ele; // 节点存储的具体值
double score; // 节点对应的分值
struct zskiplistNode *backward; // 前向指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 每一层的后向指针
unsigned long span; // 到下一个节点的跨度
} level[];
} zskiplistNode;
通过上面的源码,我们可以简单画一下大概的结构图。
很明显查询我们需要遍历整个链表,效率低。我们想要提高其查找效率,可以考虑在链表上建索引的方式。每两个结点提取一个结点到上一级,我们把抽出来的那一级叫作索引。这就是skiplist的实现方式。
在图中,需要寻找 68,在给出的查找过程中,利用跳表数据结构优势,只比较了 3次。由此可见,跳表预先间隔地保存了有序链表中的节点,从而在查找过程中能达到类似于二分搜索的效果,而二分搜索思想就是通过比较中点数据放弃另一半的查找,从而节省一半的查找时间。
应用场景:顺序会动态变化的场景。
1、热榜、热搜:incrby进行加1,zrevrange获取排序
到这里常用的数据类型就介绍完毕了,简单总结一下。
其他数据结构
bitmap
通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身。bitmap本身会极大的节省储存空间。
Redis从2.2.0版本开始新增了setbit,getbit,bitcount等几个bitmap相关命令。虽然是新命令,但是并没有新增新的数据类型,因为setbit等命令只不过是在set上的扩展。
使用场景:
1、在线用户统计
2、每日用户访问统计
hyperloglogs
HyperLogLog是用来做基数统计的算法,它的优点是在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。什么是基数呢?举个例子:比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
使用场景:统计网站的UV,日活。
geo
顾名思义,是用来地理位置计算的。
使用场景:附近的人
Stream
5.0之后新出的数据类型,支持广播的可持久化消息队列,类似于kafka。
使用场景:消息队列
- 上一篇:Redis最全概念(新手必看)
- 下一篇:Redis 管道技术——Pipeline
相关推荐
- 【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库
-
如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...
- Pure Storage推出统一数据管理云平台及新闪存阵列
-
PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...
- 对Java学习的10条建议(对java课程的建议)
-
不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...
- SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!
-
官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...
- JDK21有没有什么稳定、简单又强势的特性?
-
佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...
- 「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了
-
在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...
- Java面试题及答案最全总结(2025版)
-
大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...
- 数据库日常运维工作内容(数据库日常运维 工作内容)
-
#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...
- 分布式之系统底层原理(上)(底层分布式技术)
-
作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...
- oracle 死锁了怎么办?kill 进程 直接上干货
-
1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...
- SpringBoot 各种分页查询方式详解(全网最全)
-
一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...
- 《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略
-
《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...
- LoadRunner(loadrunner录制不到脚本)
-
一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...
- Redis数据类型介绍(redis 数据类型)
-
介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...
- RMAN备份监控及优化总结(rman备份原理)
-
今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)