PS: redis7.0.9版本的
1.Redis 源代码分类
1.1Redis 基本的数据结构
- 基础
- Redis对象object.c
- 字符串t_string.c
- 列表t_list.c
- 字典t_hash.c
- 集合及有序集合t_set.c和z_set.c
- 数据流 t_stream
- 底层实现结构 listpack.c 和 rax.c
- 简单动态字符串 sds.c
- 整数集合 intset.c
- 压缩列表 ziplist.c
- 快速链表 quicklist.c
- listpack
- 字典 dict.c
1.2 Redis 数据库的实现
- 数据库的底层实现 db.c
- 持久化 rdb.c 和 aof.c
1.3 Redis服务端和客户端实现
- 事件驱动 ae.c 和 ae_epoll.c
- 网络连接 anet.c 和 networking.c
- 服务端服务 server.c
- 客户端程序 redis-cli.c
1.4 其它
- 主从复制 replication.c
- 哨兵 sentinel.c
- 集群 cluster.c
- 其他数据结构,如 hyperloglog.c , geo.c等
- 其他功能, 如 pub/sub 、Lua 脚本
2. Redis 字典数据库KV键值对
2.1 怎么实现键值对(k-v)数据库
- key 一般都是String类型的字符串对象
- value 类型则为redis对象(redisObject)
- value可以是字符串对象
- 也可以是集合数据类型的对象,List对象、Hash对象、Set对象和Zset对象
2.2 十大类型说明
- 传统的五大类型
- String
- List
- Hash
- Set
- Zset
- 另外五大类型
- bitmap
- 实质String
- hyperLogLog
- 实质String
- GEO
- 实质 Zset
- Stream
- 实质Stream
- BITFIELD
- 看具体key
- bitmap
2.3 Redis定义了redisObject结构体来表示传统的五大类型
Redis中每个对象都是一个 redisObject结构
1. 字典、k-v是什么
-
每个键值对都会有一个dictEntry
-
dict.h源码文件
- dictEntry 表示哈希表节点的结构。存放了void *key和void *value的指针
- *key 指向String对象
- *value 既能指向String对象也能指向集合类型的对象(比如List + Hash + Set + Zset 对象)
- 备注
- void *key 和 void *value 指针指向的是内部抽象类的Redis对象,Redis中的每个对象都由 redisObject 构成
-
重点:从dictEntry 到 RedisObject
2. 键值对如何保存进Redis并进行读取操作,O(1)复杂度
3. redisObject + Redis数据类型+Redis所有编码(底层)方式三者之间的关系
3.五大结构底层C语言源码分析
3.1Redis数据类型与数据结构总纲图
1.总体数据结构大纲
- SDS 动态字符串
- 双向链表
- 压缩列表 ziplist
- 哈希表 hashtable
- 跳表 skiplist
- 整数集合 intset
- 快速列表 quicklist
- 紧凑列表 listpack
2.Redis6 与 Redis7
3.2 简单的命令分析
-
每个键值对都会有一个 dictEntry
-
set hello world
- 查看类型 type hello
- 查看编码 object encoding hello
-
因为Redis是KV键值对的数据库,每个键值对都会有一个dictEntry(源码位置:dict.h),里面指向了key和value的指针,next 指向下一个 dictEntry。
-
key 是字符串,但是 Redis 没有直接使用 C 的字符数组,而是存储在redis自定义的 SDS中。value 既不是直接作为字符串存储,也不是直接存储在 SDS 中,而是存储在redisObject 中。实际上五种常用的数据类型的任何一种,都是通过 redisObject 来存储的。
3.3 redisObject结构的作用
-
4位的type表示具体的数据类型
-
4位的encoding表示该类型的物理编码方式见下表,同一种数据类型可能有不同的编码方式。(比如String就提供了3种:int embstr raw)
-
lru字段表示当内存超限时采用LRU算法清除内存中的对象
-
refcount表示对象的引用计数
-
ptr指针指向真正的底层数据结构的指针
例如 set age 17
type | 类型 |
---|---|
encoding | 编码,此处是数字类型 |
lru | 最近被访问的时间 |
refcount | 等于1,表示当前对象被引用的次数 |
ptr | value的值是多少,当前就是17 |
3.4 经典五大数据结构解析
3.4.1. 各个类型的数据结构的编码映射和定义
3.4.2. String 数据结构介绍
1.三大物理编码方式
RedisObject 内部对应3大物理编码
int
-
保存long型(长整型)的64位(8个字节)有符号整数
-
最多19位
- 只有整数才会使用int,如果是浮点数,Redis内部其实先将浮点数转化为字符串值,然后再保存
embstr
代表embstr 格式的 SDS(Simple Dynamic String 简单动态字符串),保存长度小于44字节的字符串
raw
保存长度大于44字节的字符串
2. 三大物理编码案例
测试
C语言中字符串的展现
SDS简单动态字符串
- sds.h 源码分析
- 说明
-
Redis中字符串的实现,SDS有多种结构(sds.h)
-
define SDS_TYPE_5 (2^5=32byte)
define SDS_TYPE_8 (2 ^ 8=256byte)
define SDS_TYPE_16 (2 ^ 16=65536byte=64KB)
define SDS_TYPE_32 (2 ^ 32byte=4GB)
define SDS_TYPE_64 2的64次方byte=17179869184G用于存储不同的长度的字符串。
-
len 表示 SDS 的长度,使我们在获取字符串长度的时候可以在 O(1)情况下拿到,而不是像 C 那样需要遍历一遍字符串。
alloc 可以用来计算 free 就是字符串已经分配的未使用的空间,有了这个值就可以引入预分配空间的算法了,而不用去考虑内存分配的问题。
buf 表示字符串数组,真存数据的。
-
Redis 为什么重新设计了一个SDS数据结构?
因为C里面没有Java的String类型,只能靠char[]来实现,字符串在C中的存储方式,想要获取【Redis】的长度,需要从头开始遍历,直到遇到 ‘\0’ 为止,所以Redis自己构建了一种名为简单动态字符串SDS(simple dynamic string)的抽象类型,并将SDS作为Redis的默认字符串
C | SDS | |
---|---|---|
字符串长度处理 | 需要从头开始遍历,直到遇到 ‘\0’ 为止,时间复杂度O(N) | 记录当前字符串的长度,直接读取即可,时间复杂度 O(1) |
内存重新分配 | 分配内存空间超过后,会导致数组下标越级或者内存分配溢出 | 空间预分配:SDS 修改后,len 长度小于 1M,那么将会额外分配与 len 相同长度的未使用空间。如果修改后长度大于 1M,那么将分配1M的使用空间。 惰性空间释放:有空间分配对应的就有空间释放。SDS 缩短时并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配。 |
二进制安全 | 二进制数据并不是规则的字符串格式,可能会包含一些特殊的字符,比如 ‘\0’ 等。前面提到过,C中字符串遇到 ‘\0’ 会结束,那 ‘\0’ 之后的数据就读取不上了 | 根据 len 长度来判断字符串结束的,二进制安全的问题就解决了 |
set k1 v1 底层的调用关系(对应三大编码)
INT编码格式
当字符串键值的内容可以用一个64位有符号整形来表示时,Redis会将键值转化为long型来进行存储,此时即对应 OBJ_ENCODING_INT 编码类型。内部的内存结构表示如下:
Redis 启动时会预先建立 10000 个分别存储 0~9999 的 redisObject 变量作为共享对象,这就意味着如果 set字符串的键值在 0~10000 之间的话,则可以 **直接指向共享对象 而不需要再建立新对象,此时键值不占空间!**set k1 123set k2 123
EMBSTR编码格式
对于长度小于 44的字符串,Redis 对键值采用OBJ_ENCODING_EMBSTR 方式,EMBSTR 顾名思义即:embedded string,表示嵌入式的String。从内存结构上来讲 即字符串 sds结构体与其对应的 redisObject 对象分配在同一块连续的内存空间,字符串sds嵌入在redisObject对象之中一样。
RAW编码格式
当字符串的键值为长度大于44的超长字符串时,Redis 则会将键值的内部编码方式改为OBJ_ENCODING_RAW格式,这与OBJ_ENCODING_EMBSTR编码方式的不同之处在于,此时动态字符串sds的内存与其依赖的redisObject的内存不再连续了
补充:
对于embstr,由于其实现是只读的,因此在对embstr 对象进行修改时,都会先转化为raw 再进行修改,因此,只要是修改 embstr 对象,修改后的对象一定是 raw, 无论是否达到了44个字节
流程图:
结论:
-
只有整数才会使用 int,如果是浮点数, Redis 内部其实先将浮点数转化为字符串值,然后再保存。
-
embstr 与 raw 类型底层的数据结构其实都是 SDS (简单动态字符串,Redis 内部定义 sdshdr 一种结构)。
那这两者的区别见下图:
1 int | Long类型整数时,RedisObject中的ptr指针直接赋值为整数数据,不再额外的指针再指向整数了,节省了指针的空间开销。 |
---|---|
2 embstr | 当保存的是字符串数据且字符串小于等于44字节时,embstr类型将会调用内存分配函数,只分配一块连续的内存空间,空间中依次包含 redisObject 与 sdshdr 两个数据结构,让元数据、指针和SDS是一块连续的内存区域,这样就可以避免内存碎片 |
3 raw | 当字符串大于44字节时,SDS的数据量变多变大了,SDS和RedisObject布局分家各自过,会给SDS分配多的空间并用指针指向SDS结构,raw 类型将会调用两次内存分配函数,分配两块内存空间,一块用于包含 redisObject结构,而另一块用于包含 sdshdr 结构 |