type
status
date
slug
summary
tags
category
icon
password
Redis 很快,主要原因有两点,一是基于内存存储,二是归功于键值存储的数据结构!几乎是以 微秒级别索引到数据!那么快速的 Redis 有没有什么慢操作了?想知道这个问题,得从他的数据结构入手进行分析!
一、Redis 中的数据结构有哪些?
简单来说,底层数据结构一共有 6 种,分别是简单动态字符串、双向链表、压缩列表、哈
希表、跳表和整数数组。它们和数据类型的对应关系如下图所示:

- String 类型的底层实现只有一种数据结构
- 其他类型都有两种底层实现结构,即一个键对应了一个集合的数据。
二、键和值用什么结构组织
其实说的就是全局的数据结构,即键和值的索引方式,很简单就是一个全局的大的哈希表!
直接看下图即可:

可以发现:
- 每个哈希槽都有一个 entry
- 每个 entry 保存了 key 和 value 的指针
- 两个指针分别保存了内存中真实的 key 和 value 的地址
效率?
很明显,哈希表索引数据只需要 的计算即可,因此效率非常高!
三、为什么哈希表操作变慢了
前面说了,全局使用哈希表存储时 的复杂度,但是哈希表数据量大了,会有一个问题,就是 哈希冲突!
Redis 解决哈希冲突的方式是 拉链法,但是链太长了怎么办?
很简单,重新哈希 即可!
其实 Redis 全局哈希表有两个,就是为了 Rehash 准备的,整个 Rehash 分为以下几个步骤:
- 给哈希表 2 分配两倍哈希表 1 的空间;
- 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
- 释放哈希表 1 的空间。
Rehash 这样就没问题了吗?
No,还有问题,整个重新哈希和拷贝数据的过程是会造成线程阻塞的,无法处理其他请求!
于是 渐进式哈希 就诞生了!
类似于分而治之的思想,将一次漫长的拷贝分解为若干次小的拷贝过程,就可以很好的化解这个问题了,具体如下:
- 每次处理请求时,将当前请求 value 所在的哈希槽上的所有 entry 都进行一次拷贝
- 到某一次请求结束,当前哈希表是可以被全部拷贝完成的
- 当然,对于没有全部拷贝完的中间过程,进行键值匹配时是需要在两个哈希表进行分别查找的(这也没关系,也就是两次 罢了)
知道了键值的映射匹配关系,也仅仅是找到了 value 的位置,对于非 简单动态字符串的 String类型的其他集合类型,还需要根据集合的存储数据结构进行进一步查找!
详见下一小节!
四、集合底层数据结构
集合中的五种数据结构:整数数组、双向链表、哈希表、压缩列表和跳表!
哈希表上面已经介绍过了!
整数数据就是数组,双向链表表就是普通链表,基本都是 的复杂度!
关于更为详细的数据结构介绍,推荐下边的 小林Coding 的文章:
这里说一下,压缩列表和跳表!
压缩列表

- zlbytes,记录整个压缩列表占用对内存字节数;
- zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
- zllen,记录压缩列表包含的节点数量;
- zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)
很明显,头和尾可以直接通过前三个字段计算得到,但其他位置就只能遍历了,复杂度 !
- 紧凑存储,无内存碎片问题
- 会有 连锁更新 问题,具体详见上面推荐的 小林Coding 的文章
跳表

就是有序链表的升级,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位!
当数据量很大时,跳表的查找复杂度就是 .
五、不同操作的复杂度
- 单元素(即 value 只有一个)的增删改查为
- 范围操作为 ,scan 操作为渐进式遍历,复杂度会低一点
- 集合长度统计为 ,由于有字段专门记录长度
- 压缩列表和双向链表头尾操作都为 ,有偏移量字段
六、小结
- 可用 scan 命令代替范围操作
- 双向链表和压缩列表头尾快,那么就用这两种数据结构作FIFO的场景使用而非随机访问
- 作者:ITNXD
- 链接:https://blog.itnxd.eu.org/article/what-are-the-slow-operations-of-fast-redis
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。


