Redis的底层数据结构分析(redis底层数据类型)
mhr18 2024-11-15 22:06 18 浏览 0 评论
首先问一下有关redis可能知其然不知其所以然的问题
1、为什么redis默认是16个数据库?
2、为什么redis有这么多数据结构,它的数据结构都存储在哪儿呢?
3、为什么redis还可以存储二进制字符串?
接下来我们带着疑问,去github上面把源码拉下来,看看其中到底有何神秘之处,是否真如我们所想,一看便知。
1、数据结构分析
源码拉取
我们直接拉去最新的代码一看便知
# 使用git工具克隆项目到本地
git clone https://github.com/redis/redis.git
1.1、找到redisDb的数据结构
1.2、redisDb结构
typedef struct redisDb {
dict *dict; /* 存储所有的key-value */
dict *expires; /* 存储key的过期时间 */
dict *blocking_keys; /* blpop存储阻塞key和客户端对象*/
dict *ready_keys; /* 阻塞后push,响应阻塞的那些客户端和key */
dict *watched_keys; /* 存储watch监控的key和客户端对象 WATCHED keys for MULTI/EXEC CAS */
int id; /* 数据库的ID为0-15,默认redis有16个数据库 */
long long avg_ttl; /* 存储对象的额平均ttl(time in live)时间用于统计 */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
clusterSlotToKeyMapping *slots_to_keys; /* Array of slots to keys. Only used in cluster mode (db 0). */
} redisDb;
重要字段
字段 | 类型 | 描述 |
id | int整型 | 数据库的ID为0-15,默认redis有16个数据库 |
dict | dict字典类型 | 存储所有的key-value |
expires | dict字典类型 | 存储key的过期时间 |
1.3、RedisObject对象
我们在server.h中还发现了一个RedisObject, 它就是存储Value的值的,包括string, list, set, sortedset, hash等
typedef struct redisObject {
unsigned type:4; /*类型 5种对象类型*/
unsigned encoding:4; /*编码*/
unsigned lru:LRU_BITS; /* LRU_BITS为24bit记录最后一次被命令程序访问的时间 */
int refcount; /*引用计数*/
void *ptr; /*指向底层实现数据结构的指针*/
} robj;
1.3.1、type字段
type表示对象的类型,占4位,当我们执行type命令时,便是通过RedisObject的type字段获得对象的类型。
命令查看type类型
# 设置整数 类型为string
127.0.0.1:6379> set test01 1
OK
127.0.0.1:6379> type test01
string
# 设置短字符串 长度小于44个字节 类型为string
127.0.0.1:6379> set test02 aaa
OK
127.0.0.1:6379> type test02
string
# 设置列表 类型为list
127.0.0.1:6379> lpush test03 aaa bbb ccc
(integer) 3
127.0.0.1:6379> type test03
list
# 设置集合 类型为set
127.0.0.1:6379> sadd test04 aaa bbb ccc
(integer) 3
127.0.0.1:6379> type test04
set
# 设置散列表 类型为hash
127.0.0.1:6379> hset test05 name zhangsan age 20
(integer) 2
127.0.0.1:6379> type test05
hash
# 设置有序集合 类型为zset
127.0.0.1:6379> zadd test06 100 zhangsan 80 lisi 90 wangwu
(integer) 3
127.0.0.1:6379> type test06
zset
1.3.2、encoding字段
也是4位,表示对象的内部编码,每个对象有不同的实现编码。
Redis可以通过不同的使用场景来设置对象不同的编码,大大的提高了redis的灵活性。
通过object encoding可以查看对象采用什么样的编码
# key为test01表示整数,编码是int
127.0.0.1:6379> object encoding test01
"int"
# key为test02表示字符串,编码是embstr字符串
127.0.0.1:6379> object encoding test02
"embstr"
# 设置长字符串(超过44个字节) 类型为string
127.0.0.1:6379> set test02 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
OK
127.0.0.1:6379> object encoding test02
"raw"
# key为test03表示字符串,编码是列表 存储数据的是quicklist(快速列表)
127.0.0.1:6379> object encoding test03
"quicklist"
# key为test04表示set集合,编码是哈希表 存储数据的是hashtable
127.0.0.1:6379> object encoding test04
"hashtable"
# key为test05表示hash散列表,编码是ziplist 也就是压缩列表
127.0.0.1:6379> object encoding test05
"ziplist"
# key为test05表示hashtable散列表,编码是hashtable 也就散列表 存储大字段
127.0.0.1:6379> mget test05 name
1) (nil)
2) "zhangsan"
127.0.0.1:6379> hset test05 name zhangsan1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
(integer) 0
127.0.0.1:6379> object encoding test05
"hashtable"
# sadd数字和上面存储的字符串结构不一样
127.0.0.1:6379> sadd test10 a b c
(integer) 3
127.0.0.1:6379> sadd test11 1 2 3
(integer) 3
127.0.0.1:6379> object encoding test10
"hashtable"
127.0.0.1:6379> object encoding test11
"intset"
# 跳跃表例子
127.0.0.1:6379> zadd test12 100 zhangsan11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 45 lisi 90 wangwu
(integer) 3
127.0.0.1:6379> object encoding test12
"skiplist"
从这里可以看出,redis的编码很多,也对应这不同的数据结构,为什么这么多呢,当然是效率咯。后面会详细说明这些编码。
1.3.3、lru字段
lru 记录的是对象最后一次被命令程序访问的时间,高16位存储一个分钟数级别的时间戳,低8位存储访问计数。
1.3.4、refcount字段
refcount 记录的是该对象被引用的次数,类型为整型, 主要在于对象的引用计数和内存回收。
当对象的refcount>1时,称为共享对象.
Redis 为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象
1.3.5、ptr字段
ptr 指针指向具体的数据,比如:set name zhangsan,ptr指向包含字符串world 的指针
1.4、redis的字符串类型结构
redis的字符串跟一般的字符串不一样,它叫做SDS(Simple Dynamic String), 用于存储字符串和整型数据。结构如下:
struct sdshdr{
//记录buf数组中已使用字节的数量
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
字符串的buf数组长度=free+len+1
使用SDS有什么好处呢,为什么要专门设计一个呢?
- SDS在字符串基础上加入了len和free字段,获取字段长度时间复杂度从O(N)变为O(1).
- SDS记录了长度,在可能造成缓冲区溢出时,会自动重新分配内存,杜绝溢出。
- 可以存储二进制数据,以字符串长度len来作为结束标识符
1.5、Redis中的跳跃表
1.5.1、跳跃表简介
跳跃表时有序集合的底层实现,效率高,实现简单。
就是将有序链表中的部分节点分层,每一层都是一个有序节点。只是越上层的节点数量越少,直接快速到达目标节点,从而提升效率。
可以看到,分层之后我们可以更快速的查找到目标元素。
也是在server.h中可以看到跳跃表的结构
typedef struct zskiplistNode {
sds ele; /*存储字符串类型数据*/
double score; /*分数*/
struct zskiplistNode *backward; /*后退指针,指向当前节点最底层的前一个节点*/
struct zskiplistLevel { /*层,是一个数组,随机生成1-64的值*/
struct zskiplistNode *forward; /*指向本层下一个节点*/
unsigned long span; /*本层下个节点到本节点的元素个数*/
} level[];
} zskiplistNode;
1.5.2、跳跃表的优势
- 快速查找到目标节点
- 可以在O(1)复杂度获取头节点,尾节点,长度和高度。
1.6、压缩列表
压缩列表(ziplist)是由一系列特殊编码的连续内存块组成的顺序型数据结构, 就是为了节省内存。
sorted-set和hash元素个数少且是小整数或短字符串, 就是使用压缩列表存储的。
list用快速链表(quicklist)数据结构存储,而快速链表是双向列表与压缩列表的组合。
1.7、快速列表
1.7.1、跳跃表简介
快速列表(quicklist)是Redis底层重要的数据结构。是列表的底层实现,也是一个双向列表,既可以充当队列,也可以充当栈的使用。
typedef struct quicklist {
quicklistNode *head; /*头节点指针*/
quicklistNode *tail; /*尾结点指针*/
unsigned long count; /* 列表中所有数据项的个数总和 */
unsigned long len; /* quicklist节点的个数,即ziplist的个数 */
signed int fill : QL_FILL_BITS; /* ziplist大小限定,由list-max-ziplist-size给定 */
unsigned int compress : QL_COMP_BITS; /* 节点压缩深度设置,由list-compress-depth给定 */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
快速列表节点定义
typedef struct quicklistNode {
struct quicklistNode *prev; /*上一个节点的指针*/
struct quicklistNode *next; /*下一个节点的指针*/
unsigned char *entry; /*数据指针,如果没有被压缩,就指向ziplist结构,反之指quicklistLZF结构*/
size_t sz; /* 表示指向ziplist结构的总长度(内存占用长度) */
unsigned int count : 16; /* 表示ziplist中的数据项个数 */
unsigned int encoding : 2; /* RAW==1 or LZF==2 编码方式,1--ziplist,2--quicklistLZF*/
unsigned int container : 2; /* PLAIN==1 or PACKED==2 预留字段,存放数据的方式,1--NONE,2--ziplist*/
unsigned int recompress : 1; /* 解压标记,当查看一个被压缩的数据时,需要暂时解压,标记此参数为1,之后再重新进行压缩 */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
quicklist每个节点的实际数据存储结构为ziplist,这种结构的优势在于节省存储空间。为了进一步降低ziplist的存储空间,还可以对ziplist进行压缩。
Redis采用的压缩算法是LZF。
其基本思想是:数据与前面重复的记录重复位置及长度,不重复的记录原始数据。
压缩过后的数据可以分成多个片段,每个片段有两个部分:解释字段和数据字段。quicklistLZF的结构
typedef struct quicklistLZF {
size_t sz; /* LZF压缩后占用的字节数*/
char compressed[]; /*数组,指向数据部分*/
} quicklistLZF;
1.7.2、应用场景
列表(list)的底层实现、发布与订阅、慢查询、监视器等功能
1.8、字典dict
1.8.1、字典简介
什么是字典?
字典dict又称之为散列表(hash),是用来存储键值对的一种数据结构。
Redis整个数据库都是采用字典来存储的。所以也叫做KV结构数据库。
Hash表的实现是使用数组+链表。
什么是数组?
用来存储数据的容器,就是在计算机内开辟一块连续的空间用于存储数据。因为空间是连续的,就可以使用头节点内存地址+偏移量就可以定位到每一个元素的内存地址。时间复杂度O(1)
什么是Hash函数?
因为数组的长度是有限的,我们通过hash函数将redis的key进行hash函数转换为固定长度的散列值,然后再对数组容量进行取模。
数组下标=hash(key) % 数组容量(hash值除以数组容量得到的余数)
什么是hash冲突?
我数组长度只有16,你来了30个key, 并且其中存在两个key通过hash函数取模之后计算的数组下标是一样的。这就是hash冲突。
hash冲突如何解决?
既然存储的值会冲突,冲突的值可以存储为一个链表,如果出现hash冲突了,就在链表的末尾添加一个节点。
什么是链表?
链表就是再计算机内开辟一块不连续的空间用于存储数据,但是每个节点出了存储数据本身,还可以存储下一个节点的内存地址的一个指向。这就是单链表。如果存储了前后指针,那就是双向链表。如果双向链表首尾相连,那就是循环链表。
搞清楚了这些下后,来看一下redis中字典的结构。
1.8.2、Redis的字典结构
字典数据结构
typedef struct dict {
dictEntry **table; /*hash表数组*/
dictType *type; /*字典类型*/
unsigned long size; /*哈希表数组的大小*/
unsigned long sizemask; /*用于映射位置的掩码,值永远等于(size-1)*/
unsigned long used; /*哈希表已有节点的数量,包含next单链表数据*/
void *privdata;
} dict;
hash表的数组初始容量为4,随着k-v存储量的增加需要对hash表数组进行扩容,新扩容量为当前量的一倍,即4,8,16,32。
索引值=Hash值&掩码值(Hash值与Hash表容量取余)
hash表的节点对象
typedef struct dictEntry {
void *key; /*键*/
void *val; /*值*/
struct dictEntry *next; /*下一个节点的指针*/
} dictEntry;
字典扩容
字典达到存储上限,需要rehash(扩容)
- 初次申请默认容量为4个dictEntry,非初次申请为当前hash表容量的一倍。
- rehashidx=0表示要进行rehash操作
- 新增加的数据在新的hash表h[1]
- 修改、删除、查询在老hash表h[0]、新hash表h[1]中(rehash)
- 将老的hash表h[0]的数据重新计算索引值后, 全部迁移到新的hash表h[1]中,这个过程称为rehash
1.9、流对象
stream主要由:消息、生产者、消费者和消费组构成。
Redis Stream的底层主要使用了listpack(紧凑列表)和Rax树(基数树)。
listpack表示一个字符串列表的序列化,listpack可用于存储字符串或整数。用于存储stream的消息内容。
Rax 是一个有序字典树 (基数树 Radix Tree),按照 key 的字典序排列,支持快速地定位、插入和删除操作。
Rax 被用在 Redis Stream 结构里面用于存储消息队列,在 Stream 里面消息 ID 的前缀是时间戳 + 序号,这样的消息可以理解为时间序列消息。
使用 Rax 结构进行存储就可以快速地根据消息 ID 定位到具体的消息,然后继续遍历指定消息之后的所有消息。
2、小结
Redis里面采用了大量的数据结构优化来提升性能,所以才能更加强大,更好的了解redis的底层数据结构,有利于我们直到redis为什么性能这么高的一个重要原因之一。
3、相关文章
本人还写了Redis的其他相关文章,有兴趣的可以点击查看!
相关推荐
- C++开发必知的内存问题及常用的解决方法-经典文章
-
1.内存管理功能问题由于C++语言对内存有主动控制权,内存使用灵活和效率高,但代价是不小心使用就会导致以下内存错误:omemoryoverrun:写内存越界odoublefree:同一块内...
- 缓存用不好,系统崩得早!10条军规让你成为缓存高手
-
凌晨三点,我被电话惊醒:“苏工!首页崩了!”监控显示:缓存命中率0%,数据库QPS10万+,线程阻塞2000+。根本原因竟是同事没加缓存!不会用缓存的程序员,就像不会刹车的赛车手——...
- 彻底搞清楚内存泄漏的原因,如何避免内存泄漏,如何定位内存泄漏
-
作为C/C++开发人员,内存泄漏是最容易遇到的问题之一,这是由C/C++语言的特性引起的。C/C++语言与其他语言不同,需要开发者去申请和释放内存,即需要开发者去管理内存,如果内存使用不当,就容易造成...
- Java中间件-Memcached(Java中间件大全)
-
一、知识结构及面试题目分析缓存技术的大规模使用是互联网架构区别于传统IT技术最大的地方,是整体高并发高性能架构设计中是重中之重的关键一笔,也是互联网公司比较偏好的面试题目。按照在软件系统中所处位置...
- linux内存碎片防治技术(linux内存碎片整理)
-
推荐视频:90分钟了解Linux内存架构,numa的优势,slab的实现,vmalloc原理剖析Linux内核内存分配与回收Linuxkernel组织管理物理内存的方式是buddysystem(伙...
- Redis主从架构详解(redis主从配置详细过程)
-
Redis主从架构搭建Redis主节点配置创建主节点目录(/opt/redis-master),复制redis.conf到该目录下,redis.conf配置项修改#后台启动daemonizeyes...
- 揭开CXL内存的神秘面纱(内存c1)
-
摘要:现代数据中心对内存容量的高需求促进了内存扩展和分解方面的多条创新线,其中一项获得极大关注的工作是基于ComputeeXpressLink(CXL)的内存扩展。为了更好地利用CXL,研究人员建...
- 一文彻底弄懂 TPS RPS QPS(tps cps)
-
以下是关于RPS、QPS、TPS的核心区别与关联的总结,结合实际场景和优化建议:一、核心定义与区别RPS:RequestsPerSecond每秒请求数客户端到服务器的完整请求数量Web服务...
- 用Redis的“集合”找出你和朋友的“共同关注”
-
你是不是在刷抖音、微博、小红书的时候,常常会看到这样的提示:“你和XXX有共同关注的博主/朋友”?或者当你关注了一个新的明星,系统会推荐“你的朋友YYY也关注了这位明星”?这个看似简单的功能背后,其实...
- WOT2016彭哲夫:科班出身开发者对运维人员的期许
-
“运维与开发”是老生常谈的话题,前几天和一个运维人聊天,TA说一些公司运维岗位都不公开招聘了,这让众多运维人员情何以堪?是运维的岗位真的饱和了?是找到合适的运维人才难?还是有这样那样的因素?带着这些疑...
- Java程序员最常用的20%技术总结(java程序员要掌握什么)
-
我听说编程语言,经常使用的是其中20%的技术。在Java这门语言中,这20%包括哪些内容?找到一份Java初级程序员的工作,有哪些是必须掌握的,有哪些是可以现学现卖的?一个完整的Javaweb项目,有...
- 秒杀系统实战(四)| 缓存与数据库双写一致性实战
-
前言微笑挖坑,努力填坑。————已经拥有黑眼圈,但还没学会小猪老师时间管理学的蛮三刀同学本文是秒杀系统的第四篇,我们来讨论秒杀系统中「缓存热点数据」的问题,进一步延伸到数据库和缓存的...
- 头条评论精灵翻牌子(头条评论精灵翻牌子怎么弄)
-
关于“头条评论精灵翻牌子”功能,这通常是指平台通过算法或运营手段,将用户的优质评论随机或定向推送到更显眼的位置(如信息流顶部、独立曝光位等),以提升互动率和用户参与感。以下是详细解析和建议:一、功能理...
- 15个程序员们都应该知道的大模型高级提示词指令模板和示例
-
作为程序员你如何写大模型指令?你写的指令是不是更专业呢?下面是15个程序员使用的专业的大模型指令,如果早知道可以能节省你很多时间。这些指令可以用在chatgpt,deepseek等大模型。1.一键...
- MyBatis-Plus内置的主键生成策略有大坑,要注意!
-
昨天小伙伴使用Mybaits-Plus开发的项目线上(集群、K8S)出现了主键重复问题,其报错如下:Mybatis-Plus启动时会通过com.baomidou.mybatisplus.core.to...
你 发表评论:
欢迎- 一周热门
-
-
Redis客户端 Jedis 与 Lettuce
-
高并发架构系列:Redis并发竞争key的解决方案详解
-
redis如何防止并发(redis如何防止高并发)
-
开源推荐:如何实现的一个高性能 Redis 服务器
-
redis安装与调优部署文档(WinServer)
-
Redis 入门 - 安装最全讲解(Windows、Linux、Docker)
-
一文带你了解 Redis 的发布与订阅的底层原理
-
Redis如何应对并发访问(redis控制并发量)
-
oracle数据库查询Sql语句是否使用索引及常见的索引失效的情况
-
Java SE Development Kit 8u441下载地址【windows版本】
-
- 最近发表
- 标签列表
-
- 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)