目录
前言
1.String
1.1RAW编码
1.2EMBSTR编码
1.3 INT编码
2.List
3.Set
3.1 InSet编码转化成Dict编码
4.ZSet
4.1结合SkipList和HT实现
4.2使用ZipList实现
4.3编码转换
4.4 ZipList排序功能
5.Hash
5.1Hash底层存储结构
6.Redis数据结构和数据类型关系图
前言
Redis 中的每种数据类型都由特定的数据结构来实现,数据结构为数据类型提供了底层的存储和操作机制。
不同的数据类型具有不同的特性和应用场景,数据结构的选择和设计是为了更好地满足这些特性。
数据结构是实现数据类型的基础,它们共同构成了 Redis 强大的功能体系,使得 Redis 能够在不同的应用场景中,根据数据的特点和操作需求,选择合适的数据类型和对应的数据结构来高效地处理和管理数据。
1.String
String是Redis中最常见的数据存储类型:
其基本编码方式是RAW,基于简单动态字符串(SDS)实现,存储上限为512mb。
查看编码:object encoding key
1.1RAW编码
ptr指针指向SDS,RedisObject和SDS是两个独立空间所以会有寻址过程通过ptr指针找到SDS。所以会有两次申请内存空间,释放内存也会有两次内存释放,效率比较低。
1.2EMBSTR编码
如果存储的SDS长度小于44字节,EMBSTR编码,此时object head与SDS是一段连续空间。申请内存时只需要调用一次内存分配函数,效率更高。
好处:减少内存碎片
Redis 通常按页分配内存,使用 jemalloc 等内存分配器,它会分配 8、16、32、64 等字节的内存块。对于小于 44 字节的字符串,采用 embstr 编码可以将其存储在一个连续的 64 字节内存块中,减少了内存碎片的产生。
如果字符串较长,超过 44 字节,为其单独分配内存空间(转为 raw 编码),采用常规的 robj 和SDS分离方案性能下降,产生内存碎片。
1.3 INT编码
如果存储的字符审是整数值,并且大小在LONG_MAX范围内,INT编码,直接将数据保存在RedisObject的ptr指针位置(刚好8字节),不再需要SDS了。
2.List
Redis的List结构类似双端链表,可以从首尾操作列表中的元素
选择何种数据结构呢?
- LinkList:普通链表,可以从双端访问,链表每个节点都是独立的内存,节点与节点之间的访问通过指针,指针比较占用内存,内存碎片较多。
- ZipList:压缩列表,可以从双端访问,内存占用较低,存储上限低。如果一次性存储大量内存需要申请大量内存空间比较困难。
- QuickList:LinkedList+ZipList,可以双端访问,内存占用较低,包含多个ZipList,存储上限高。
在3.2版本之前,Redis采用ZipList来实现List,当元素数量小于512并且元素大小小于64字节时采用ZipList编码,超过则采用LinkedList编码。
在3.2版本之后,redis统一采用QuickList来实现List。Redis 可以通过list-max-ziplist-size参数来控制每个 ZipList 节点的大小,通过list-compress-depth参数来控制 QuickList 的压缩深度,即链表两端不压缩的节点数量,以此来平衡内存使用和操作性能。插入元素底层源码
整体内存结构
3.Set
Set是Redis中的单列集合,满足下列特点:
- 不保证有序性
- 保证元素唯一(可以判断元素是否存在)
- 求交集,并集,差集
不管是插入元素还是求交集等等这些操作都需要判断元素是否存在,这样就需要选择对元素查询效率较高的数据结构
- 为了查询效率和唯一性,set采用HT编码(Dict)Dict中的key用来存储元素,value统一为null
- 当存储的所有数据都是整数,并且元素数量不超过set-max-intset-entset会采用IntSet编码,以节省内存。
判断用哪种数据结构底层源码
创建IntSet数据结构底层源码
创建Dict数据结构底层源码
整体执行源码
3.1 InSet编码转化成Dict编码
当元素数量超过set-max-intset-entries或者存储元素不是数字了时会自动转换成Dict编码
执行结构图:
当插入一个元素为m1字符串时会升级为Dict编码,创建Dict数据结构
RedisObject指针ptr指向Dict,encoding编码改为Dict
4.ZSet
ZSet也就是SortedSet,其中每个元素都需要指定一个score值和member值:
可以根据score值排序
member必须唯一
可以根据member查询分数
因此,ZSet底层数据结构必须满足键值存储,键必须唯一,可排序这几个需求。
4.1结合SkipList和HT实现
- SkipList:可以排序,并且可以同时存储score和ele值(member) | 满足键值存储和可排序
- HT(Dict):可以键值存储,并且可以根据key找value | 满足高效查询键值唯一
所以ZSet底层是使用这两种数据结构来实现的dict用于快速查找,SkipList用于键值存储和排序。
底层源码:
内存图:
4.2使用ZipList实现
使用上述那种方式这样带来的坏处就是ZSet非常占用内存空间,使用两种数据结构,存储两份。
当元素数量不多时,HT和SkipList的优势不明显,而且更耗内存。因此Zset还会采用ZipList不过需要同时满足两个条件:
- 元素数量小于zset_max_ziplist_entries,默认值128
- 每个元素都小于zset_max_ziplist_value字节,默认值64
底层判断使用哪种数据结构如下:
4.3编码转换
既然ZSet底层都是用来两种编码方式根据不同情况,那么肯定会出现编码转换,添加元素不满足一下两个条件就会触发编码转换。
- 元素数量小于zset_max_ziplist_entries,默认值128
- 每个元素都小于zset_max_ziplist_value字节,默认值64
底层执行源码:
4.4 ZipList排序功能
ZipList本身没有排序功能,而且没有键值对的概念,因此需要有zset通过编码实现:
- ZipList是连续内存,因此score和element是紧换在一起的两个entry,element在前,score在后
- score越小越接近队首,score越大越接近队尾,按照score值升序排列
底层结构图:
因为元素比较少ZipList内存中是连续存储的,所以查找的时候就去遍历,插入元素的时候按照大小顺序插入,这样就保证了排序。性能也不错。
5.Hash
Hash结构与Redis中的Zset非常类似:
- 都是键值存储
- 都需要根据键获取值
- 键必须唯一
区别如下:
- zset的键是member,值是score;hash的键和值都是任意值
- zset要根据score排序;hash则无需排序
5.1Hash底层存储结构
- Hash结构默认采用ZipList编码,用以节省内存。ZipList中相邻的两个entry分别保存field和value
- 当数据量较大时,hash结HT编码,也就是Dict,触发条件有两个:
ziplist中的元素数量超过了hash-max-ziplist-entries(默认512)
ziplist中的任意entry大小超过了hash-max-ziplist-value(默认64字节)转换流程:
触发条件达成转换编码