redis常用数据结构及底层
- string字符串、list链表、set无序集合、zset有序集合、hash哈希
1.string
- 底层结构是
SDS简单动态字符串
- 可以存字符串,整型,浮点型,二进制数据
- 作为redis的key实现
struct sdshdr {
int len; // 已用长度(字符串实际长度)
int free; // 剩余可用空间
char buf[]; // 数组,存储实际字符数据
};
- 相较于c字符串,它通过len和free字段实现O(1)长度查询,自动扩容,支持二进制存储。
2.list
- 是一个有序可重复链表结构,会根据数据量和长度,自动选择不同数据结构,提升性能
- 底层实现主要三种:
- ziplist压缩列表:redis3.2之前,元素数量<512,元素大小<64用压缩列表
- linkedlist双链表:redis3.2之前,大数据量使用。
- quicklist快速列表:redis3.2之后的默认存储,所有情况都用quicklist,他是由多个ziplist组成的双向链表
3.set
- 无序,元素唯一的集合类型,会根据数据量和内容,自动选择不同数据结构,提升性能
- 底层两种实现:
- intset整数集合:适用于,都是整数,数量<=512。内存连续,升序排列,插入删除慢
- hashtable哈希表:适用于,多数据类型,数量多。标准hash,内存开销较大(存储指针、哈希表结构)操作效率高
redis会根据数据自动切换,添加非整数/数量超512自动转hashtable,但是不会重新转回来
4.zset
- 元素唯一,通过每个元素关联的score自动排序的结构,支持范围查找,范围排名等操作
- 底层两种实现:
- skiplist跳表:跳表提供有序遍历和范围操作
- hashtable哈希表:哈希表提供快速查找
为什么用跳表而不是红黑树?
实现简单,插入删除性能好(时间复杂度平均 O(log n)),更容易实现范围操作
- 跳表原理:通过分多个索引层来加速查询,底层是完整的有序链表,上层是索引层,每上一层减少一半,所以log₂n
第3层:1 --------------------------> 9
第2层:1 ------> 5 ------> 7 ------> 9
第1层:1 -> 3 -> 5 -> 6 -> 7 -> 8 -> 9
5.hash
- 一个键值对集合,常用于存对象(用户配置)等,会根据字段数量和内容自动选择不同的实现方式
- 底层两种实现:
- ziplist压缩列表:小对象用压缩列表,(字段数<512,长度<64)连续内存,占用少,节省空间,查询速度稍慢,字段超过阈值时,会自动转换为hashtable
- hashtable哈希表:大对象用hash表,读写效率更高
为什么小数据用 ziplist?
内存利用率高(无指针开销)
Redis 的哈希表如何扩容?
采用 渐进式扩容:扩容时同时保留新旧两个哈希表,分批次迁移数据
关于hash渐进式扩容,可以看我的这篇https://blog.csdn.net/m0_74282926/article/details/147522876
总结
数据类型 | 底层结构 | 说明 |
---|---|---|
String | int / embstr / raw(SDS) | 根据长度和内容选择优化方式:int 表示整数,embstr 表示小字符串,raw 是通用 SDS |
List | ziplist(旧) / quicklist | Redis 3.2+ 默认用 quicklist(多个 ziplist + 双向链表) |
Set | intset / hashtable | 小量整数用 intset,其他情况用 hashtable |
ZSet | skiplist + hashtable | 哈希表查找,跳表排序;两个结构同时维护 |
Hash | ziplist / hashtable | 字段少且短用 ziplist,否则用 hashtable |
为什么redis单线程反而更快?
- 一个误区:并不是多线程一定比单线程快:
- 在mysql中我们对于耗时较长的磁盘io操作往往采用多线程并发执行,因为多线程产生的线程切换开销往往小于磁盘读写开销,所以并发等待可以提高效率;
- 而redis是内存存储,本身单线程速度就很快,如果用多线程,
涉及cpu上下文切换,锁竞争,这些耗时可能会超过从内存中取数据的时间,从而降低性能。
-
redis的性能瓶颈往往是网络或者内存,而非cpu,内存访问的速度高于切换线程的开销
-
Redis 6.0 起引入了I/O 多线程模型,用于并行处理高并发下,客户端的网络读写数据(网络 I/O),但命令执行仍是单线程,以保证数据一致性
网络用多线程并发提速,核心逻辑用单线程保证安全。
- redis的数据协议设计很高效,比如HashTable,理想情况下只需要O(1)的时间复杂度就可以找到数据。
例如 Hash 的 ziplist + hashtable 组合,在数据量小时用 ziplist(节省内存),大时转 HashTable(O(1) 查询)。
优化版数据结构底层分析,面经二次优化
redis常用数据结构,及其底层(string、list、hash、set、zset)
- string:
- SDS
简单动态字符串
,可以存整数,浮点数,字符串,二进制数据, - 存有一个
长度
字段,可以在O(1)获取字符串长度
,一个free字段,显示剩余空间 - 支持
动态扩容
- 用来做
key的存储结构
,利用incr自增
做计数器,
- SDS
- lsit:
- redis3.2之前使用
ziplist和双向链表
,ziplist就是一个压缩链表
,主要是让内存排列更紧凑
,压缩空间,双链表就是支持两端插入删除
,操作效率高。 - redis3.2之后
统一用quicklist
,他是由多个ziplist组成的双向链表,他就整合了两者的优点,ziplist节省空间,双链表提升操作效率 - 可以用来做消息队列,利用lpush和rpush还可以做队列,栈等。
- redis3.2之前使用
- hash:
- 使用
ziplist
+hashtable
- hashtable,通过哈希函数进行hash计算,快速定位,具有O(1),他支持动态扩容,当然,会有hash碰撞,常见hash碰撞处理方法,
开放寻址法
和拉链法
开放寻址
指得就是当前位置冲突,就顺位下移
一个位置,但是这种处理后续会带来更多的hash冲突
,所以一般不建议使用
拉链法
:将用来存数据的数组的元素
,链接一个链表
,发生冲突就把元素放到链表尾部
- 由于他的查找具有O1复杂度,它也可以用来存排行榜上需要实时更新的一些数据,因为榜的话我们刷新频率较高,所以对这个数据更新速度要求比较高,可以用hash来做
- 使用
- set:无序集合
hashtable
+intset
- intset
整数集合
,存各种类型的int(int8,int16,int32,int64),会根据存入int的大小,自动紧凑排列
,(常规的整型数组想要存下上述不同大小int,需要用int64,对于8 16 32来说会有空间浪费问题),节省空间,并且是有序的,所以查找的时间复杂度可以到logn,支持多种整数类型,自带动态扩容
- 因为他本身无序,可以用来做
抽奖活动
,还可以利用集合的并差交操作
,做那个共同关注
,可能认识的人
- zset:
- 相
比
于set多了一个score
分数,通过这个score分数排序
,实现有序
- 跳表和hashtable
- 跳表,大致结构是底层是完整的数据链表,向上会有索引层,每向上一层,索引数就减半,跳表是有序的,并且由于这种索引层机制,使得范围查询很高效, 插入删除操作也有logn
- 可以做热榜排行,每当检测到词条被点击,就可以让分数+1
- 相