30redis基础
redis
Redis为什么这么快
(1)纯内存操作这是最最主要的原因Redis数据读写操作发生在内存中,访问速度是纳秒级别,而数据库频繁读写磁盘的速度是毫秒级别,两者相差多个数量级.
(2)高效的IO模型Redis使用单线程事件循环配合IO多路复用技术,让单线程可以同时处理多个网络连接上的IO事件,避免了多线程模型中的上下文切换和锁竞争问题.
事件循环:无限循环,不断轮询并处理就绪的事件,直到Redis服务停止,整个循环会处理两类事件:文件事件(建立TCP,客户端读写)和时间事件(定时任务),先IO后定时,循环往复.
(3)优化的内部数据结构Redis提供多种数据类型,内部采用高度优化的编码方式Redis会根据数据大小和类型动态选择最合适的内部编码,以在性能和空间效率之间取得最佳平衡.
(4)简洁高效的通信协议Redis使用自己设计的RESP协议该协议实现简单,解析性能好,且是二进制安全的客户端和服务端之间的序列化/反序列开销很小,有助于提升整体的交互速度.
多线程
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因
Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会额外创建 6 个线程(这里的线程数不包括主线程):
- Redis-server : Redis的主线程,主要负责执行命令;
- bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务;
- 通过
bio_close_file后台线程来释放 AOF / RDB 等过程中产生的临时文件资源。 - 通过
bio_aof_fsync后台线程调用fsync函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘(AOF 文件)。 - 通过
bio_lazy_free后台线程释放大对象(已删除)占用的内存空间. - io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。
Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要设置 IO 线程数 > 1,需要修改 redis 配置文件 redis.conf:
既然是单线程,那怎么监听大量的客户端连接呢?
Redis 通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。
IO多路复用
为什么用Redis而不用本地缓存呢
本地缓存是指将数据存储在本地应用程序或服务器上,通常用于加速数据访问和提高响应速度。
(1)数据一致性
Redis -> 数据一致
本地缓存 -> 多服务器同时部署时存在数据不一致问题
(2)内存限制
Redis -> 独立部署,内存空间更大
本地缓存 -> 受限于单台服务器内存
(3)数据丢失风险
Redis -> 可持久化,数据不易丢失
本地缓存 -> 服务器宕机数据丢失
(4)管理维护
Redis -> 集中管理, 提供丰富的管理工具
本地缓存 -> 分散,管理不便
(5)拓展功能丰富
Redis -> 功能丰富,支持多种数据结构以功能
本地缓存 -> 功能有限,通常只支持简单的键值对存储.
redis底层数据
简单讲一下Redis的数据类型及应用场景
(1)数据类型String可存储字符串,整数,浮点数,支持直接读写,自增自减等
Hash存储键值对的无序散列表,可单独对某个键值对进行增删改查
List基于双向链表实现的有序数据结构,链表每个节点存储一个字符串,支持双端插入/删除数据
Set存储不重复元素的无序集合,元素均为字符串,支持交集,并集,差集等
Zset在Set基础上为每个元素关联一个分数(score),通过分数对元素进行有序排序.
(2)应用场景基础类型
String -> 缓存对象,常规计数,分布式锁,共享session信息等
List -> 消息队列(生产者需要自行实现全局唯一ID)
Hash -> 缓存对象,购物车等
Set -> 聚合计算场景,比如点赞,共同关注,抽奖活动等
ZSet -> 排序场景,如排行榜拓展类型
BitMap -> 二进制状态统计场景,如用户登录状态,连续见到的用户总数
HyperLogLog->UV统计,用概率换空间
GEO-> 地理位置存储
stream -> 消息队列,自动生成全局唯一消息ID,支持以消费组形式消费.(难以解决堆积和丢失问题)
String 是使用什么存储的?为什么不用 c 语言中的字符串?
Redis 的 String 字符串是用 SDS 数据结构存储的。
下图就是 Redis 5.0 的 SDS 的数据结构:

结构中的每个成员变量分别介绍下:
- len,记录了字符串长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1)。
- alloc,分配给字符数组的空间长度。这样在修改字符串的时候,可以通过
alloc - len计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出的问题。 - flags,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后面在说明区别之处。
| 类型 | 字节 | 位 |
|---|---|---|
| sdshdr5 | < 1 | <8 |
| sdshdr8 | 1 | 8 |
| sdshdr16 | 2 | 16 |
| sdshdr32 | 4 | 32 |
| sdshdr64 | 8 | 64 |
- buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。
比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。
SDS 相比于 C 语言中的字符串有如下提升:
- 可以避免缓冲区溢出:C 语言中的字符串被修改(比如拼接)时,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。SDS 被修改时,会先根据 len 属性检查空间大小是否满足要求,如果不满足,则先扩展至所需大小再进行修改操作。
- 获取字符串长度的复杂度较低:C 语言中的字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。SDS 的长度获取直接读取 len 属性即可,时间复杂度为 O(1)。
- 减少内存分配次数:为了避免修改(增加/减少)字符串时,每次都需要重新分配内存(C 语言的字符串是这样的),SDS 实现了空间预分配和惰性空间释放两种优化策略。当 SDS 需要增加字符串时,Redis 会为 SDS 分配好内存,并且根据特定的算法分配多余的内存,这样可以减少连续执行字符串增长操作所需的内存重分配次数。当 SDS 需要减少字符串时,这部分内存不会立即被回收,会被记录下来,等待后续使用(支持手动释放,有对应的 API)。
- 二进制安全:C 语言中的字符串以空字符
\0作为字符串结束的标识,这存在一些问题,像一些二进制文件(比如图片、视频、音频)就可能包括空字符,C 字符串无法正确保存。SDS 使用 len 属性判断字符串是否结束,不存在这个问题。
List 类型的底层数据结构
是由双向链表或压缩列表实现的:
- 如果列表的元素个数小于
512个(默认值,可由list-max-ziplist-entries配置),列表每个元素的值都小于64字节(默认值,可由list-max-ziplist-value配置),Redis 会使用压缩列表作为 List 类型的底层数据结构; - 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;
但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表(7.0Listpack)
Hash 类型的底层数据
结构是由压缩列表或哈希表实现的:
- 如果哈希类型元素个数小于
512个(默认值,可由hash-max-ziplist-entries配置),所有值小于64字节(默认值,可由hash-max-ziplist-value配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构; - 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
信息流展示
- 举例:最新文章、最新动态。
- 相关命令:
LPUSH、LRANGE。
Set底层
当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择,并且 Set 提供了判断某个元素是否在一个 Set 集合内的重要接口,这个也是 List 所不能提供的。

Zset底层
Sorted Set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。
*Zset 的底层是 *跳表(skiplist)+ 字典(dict)*,但小 Zset 用压缩列表(ziplist)(listpack)节省内存*
“跳表负责有序遍历,字典负责快速查找” —— 两者互补,避免了纯跳表的 O(log N) 查找缺陷。
Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+ 树?
| 维度 | 红黑树 | 跳表 | 为什么 Redis 选跳表 |
|---|---|---|---|
| 实现复杂度 | ❌ 高(需处理旋转、颜色平衡) | ✅ 低(仅需随机层数+指针) | Redis 代码库追求简洁,红黑树代码量是跳表的 3倍(实测:Redis ZSet 代码 150行 vs 红黑树 450+行) |
| 内存占用 | ❌ 高(每个节点需额外存储颜色、父指针) | ✅ 低(仅需 value + forward 指针) |
跳表节点结构更紧凑,内存节省 20%+(Redis 10 万 ZSet 元素实测) |
| 性能 | ⚠️ 最坏 O(log n),但常数因子高 | ✅ 平均 O(log n),常数因子低 | Redis 是单线程,跳表的随机性避免了红黑树的平衡开销,实际性能几乎一致 |
Zset 的两种实现方式(精准条件)
| 条件 | 说明 | 默认值 |
|---|---|---|
| 元素数量 | ≤ zset-max-ziplist-entries |
128 |
| 成员长度 | ≤ zset-max-ziplist-value |
64 字节 |
| 分数长度 | ≤ zset-max-ziplist-value |
64 字节 |
为什么用 ziplist?
- 内存占用:压缩列表是连续内存块,无指针开销(比跳表节省 30%+ 内存)
- 适用场景:小规模 Zset(如用户标签、小范围排序)
为什么 Zset 的 ziplist 不用普通压缩列表? 答:Zset 的 ziplist 专门设计,将成员和分数交替存储(member, score, member, score),避免了普通 ziplist 的遍历问题。
✅ 跳表(skiplist)+ 字典实现(大 Zset 主流)
为什么需要字典?
“跳表只能按分数排序,但无法快速查成员!”
例如:ZRANGEBYSCORE 用跳表,ZSCORE 用字典 → O(1) 查找。
Hash底层
Hash的存储机制:两种实现方式
1. 压缩列表(ziplist)实现(内存优化)
当Hash满足以下条件时,Redis会使用**压缩列表(ziplist)**存储:
1 | |
为什么用ziplist?
- 内存占用低:压缩列表是连续内存块,没有指针开销
- 适用场景:小Hash(元素少+键值小)
ziplist结构(简要)
entry包含:prevlen(前节点长度)、encoding(类型+长度)、data(实际数据)
2. 哈希表(hashtable)实现(性能优先)
当Hash超过上述阈值时,Redis会自动转换为哈希表实现。
哈希表核心机制
- 哈希函数:使用MurmurHash2算法(高效、低冲突)
- 冲突解决:链地址法(每个桶维护一个链表)
- 动态扩容:
- 当
used / size > 0.7时扩容(负载因子) - 扩容为当前大小的2倍(如4→8→16→32…)
- 增量式rehash:将数据逐步从ht[0]迁移到ht[1]
- 当

dict
1 | |
**ht[2]:**表示在一个Dict结构中,包含有两个dictht的结构,也就是我们说的两张哈希表。
**rehashidx:**是dict在rehash时的偏移索引,具体如何工作在后边的rehash过程中会详细讲。
dictht
1 | |
****table:指向实际hash存储。存储可以看做是一个数组,所以是*table表示。(源码中的table是一个二级指针,也就是指向dictEntry*的指针)。
**size:**哈希表的大小。实际就是dictEntry有多少元素空间。
**sizemask:**哈希表大小的掩码表示,总是等于size-1.这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面,索引计算规则是index=hash&sizemask,前提是size的大小是二次方幂,这一点与JAVA哈希表底层计算索引是一样的原理。
**used:**表示已经使用的节点数量。通过这个字段可以很方便地查询到目前dict元素总量。
Q1:为什么Redis的Hash在底层用字典(dict)而不是直接用哈希表?
答:字典(dict)是Redis对哈希表的封装,提供了以下关键能力:
- 两个哈希表实现增量式rehash
- 类型特性函数(dictType)支持多种数据类型
- 迭代器支持
- 通过
privdata传递私有数据
💡 加分点:可以补充”Redis的字典是高度可定制的,可以用于实现String、Hash、Set等不同数据结构。”
Q2:Redis的Hash在什么情况下会从ziplist转换为hashtable?
答:当满足以下任一条件时:
hash-max-ziplist-entries:Hash中元素数量超过阈值(默认512)hash-max-ziplist-value:某个字段名或值的长度超过阈值(默认64字节)
💡 面试官会追问:为什么设置这两个阈值? 答:为了在内存效率和操作性能之间取得平衡:
- 小Hash:用ziplist节省内存
- 大Hash:用hashtable保证操作速度
Q3:Redis的哈希表扩容时,为什么是2倍扩容?
答:2倍扩容是经验法则:
- 避免频繁扩容(如果扩容因子是1.5,会频繁触发扩容)
- 保证负载因子在合理范围(0.5-0.7)
- 简单且高效(2的幂次方,计算索引快)
💡 面试官会追问:为什么负载因子选0.7? 答:根据哈希表理论,负载因子在0.7左右时,链表长度平均为1.5-2,平衡了内存利用率和查询性能。
“Redis的Hash底层用字典(dict)实现,字典基于哈希表+链地址法;小Hash用ziplist节省内存,大Hash用hashtable保证性能——通过两个哈希表实现增量式rehash,避免扩容卡顿,这是Redis性能与内存平衡的完美体现!”
哈希表扩容
随着数据逐步增多,触发了 rehash 操作,这个过程分为三步:
- 给「哈希表 2」 分配空间,一般会比「哈希表 1」 大 2 倍;
- 将「哈希表 1 」的数据迁移到「哈希表 2」 中;
- 迁移完成后,「哈希表 1 」的空间会被释放,并把「哈希表 2」 设置为「哈希表 1」,然后在「哈希表 2」 新创建一个空白的哈希表,为下次 rehash 做准备。
渐进式rehash
Redis 采用了渐进式 rehash,也就是将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。
举个例子,如果ht[0]中已经使用的节点数量为500,那么扩容时ht[1]被分配的空间是1024而不是1000。这么做是为了维护扩容后表的大小始终是2次方幂。
实现步骤
- 初始化新表与状态变量 Redis 为 ht[1] 分配内存空间,并将 rehashidx 设置为 0,表示迁移开始。
- 分批迁移数据 每次对字典执行增删改查操作时,除了完成用户请求,还会迁移 ht[0] 中 rehashidx 指向的哈希桶到 ht[1]。迁移完成后,rehashidx 加 1。
- 主动迁移 为避免迁移因操作频率低而停滞,Redis 的定时任务会批量迁移多个哈希桶(如一次迁移 100 个)。
- 读写操作适配双表 查询、删除、更新操作会先查找 ht[1],再查找 ht[0];新增操作只写入 ht[1],避免旧表产生新数据。
- 完成迁移 当 rehashidx 等于 ht[0] 的容量时,迁移完成。Redis 释放 ht[0] 的内存,将 ht[1] 赋值为 ht[0],并重置 ht[1] 和 rehashidx。
如果rehashidx刚好在一个已删除的空位置上,那么是直接返回还是尝试往下找?我们来看一下dictRehash函数的源码:
可以看到,答案是会继续往下去找,但是有个上限是n*10,即最多再找这么多次,n是传进来的参数,调用的时候实际值为1,即最多往后再找10个,这么做是防止因为连续碰到空位置导致主线程操作被阻塞。
哈希表扩容的时候,有读请求怎么查?
查找一个 key 的值的话,先会在「哈希表 1」 里面进行查找,如果没找到,就会继续到哈希表 2 里面进行找到。
介绍一下 Redis 中的 listpack
quicklist 虽然通过控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来减少连锁更新带来的性能影响,但是并没有完全解决连锁更新的问题。
因为 quicklistNode 还是用了压缩列表来保存元素,压缩列表连锁更新的问题,来源于它的结构设计,所以要想彻底解决这个问题,需要设计一个新的数据结构。
于是,Redis 在 5.0 新设计一个数据结构叫 listpack,目的是替代压缩列表,它最大特点是 listpack 中每个节点不再包含前一个节点的长度了,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。
listpack 采用了压缩列表的很多优秀的设计,比如还是用一块连续的内存空间来紧凑地保存数据,并且为了节省内存的开销,listpack 节点会采用不同的编码方式保存不同大小的数据。
我们先看看 listpack 结构:

listpack 头包含两个属性,分别记录了 listpack 总字节数和元素数量,然后 listpack 末尾也有个结尾标识。图中的 listpack entry 就是 listpack 的节点了。
每个 listpack 节点结构如下:

主要包含三个方面内容:
- encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码;
- data,实际存放的数据;
- len,encoding+data的总长度;
可以看到,listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。
Listpack 通过移除 prevlen 字段,让插入/删除操作从 O(N) 变为 O(1)——它保留了 ziplist 的内存紧凑性,却彻底消灭了连锁更新,是 Redis 5.0 的最佳列表实现!
Listpack vs Ziplist:全面对比
| 特性 | Ziplist | Listpack | 优势 |
|---|---|---|---|
prevlen 字段 |
✅ 存在 | ❌ 不存在 | ✅ 彻底消除连锁更新 |
| 节点长度计算 | 依赖 prevlen |
通过 encoding |
✅ 更高效 |
| 插入/删除复杂度 | O(N) | O(1) | ✅ 大幅提升性能 |
| 内存占用 | 极低(小列表) | 略高但可忽略 | ✅ 仍比普通链表节省 30%+ |
| 最大元素数 | 65535(需遍历) | 255(用 zsize 计算) |
✅ 无限制 |
| Redis 版本 | 早期 | 5.0+ 默认 | ✅ 已成为标准 |
ziplist是怎么实现的?
压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。

压缩列表在表头有三个字段:
- *zlbytes*,记录整个压缩列表占用对内存字节数;
- *zltail*,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
- *zllen*,记录压缩列表包含的节点数量;
- *zlend*,标记压缩列表的结束点,固定值 0xFF(十进制255)。
在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段(zllen)的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了,因此压缩列表不适合保存过多的元素。
另外,压缩列表节点(entry)的构成如下:

压缩列表节点包含三部分内容:
- prevlen,记录了「前一个节点」的长度,目的是为了实现从后向前遍历;
- encoding,记录了当前节点实际数据的「类型和长度」,类型主要有两种:字符串和整数。
- data,记录了当前节点的实际数据,类型和长度都由 encoding 决定;
当我们往压缩列表中插入数据时,压缩列表就会根据数据类型是字符串还是整数,以及数据的大小,会使用不同空间大小的 prevlen 和 encoding 这两个元素里保存的信息,这种根据数据大小和类型进行不同的空间大小分配的设计思想,正是 Redis 为了节省内存而采用的。
为什么ziplist能节省内存? 答:通过动态编码(如整数直接存储,字符串用长度编码),避免了字符串的额外长度存储,内存占用比普通哈希表低30%+。
压缩列表的缺点是会发生连锁更新的问题,因此连锁更新一旦发生,就会导致压缩列表占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能。
所以说,虽然压缩列表紧凑型的内存布局能节省内存开销,但是如果保存的元素数量增加了,或是元素变大了,会导致内存重新分配,最糟糕的是会有「连锁更新」的问题。
因此,压缩列表只会用于保存的节点数量不多的场景,只要节点数量足够小,即使发生连锁更新,也是能接受的。
Redis为什么使用跳表而不是用B+树?
Redis 是内存数据库,跳表在实现简单性、写入性能、内存访问模式等方面的综合优势,使其成为更合适的选择。
| 维度 | 跳表优势 | B+ 树劣势 |
|---|---|---|
| 内存访问 | 符合CPU缓存局部性,指针跳转更高效 | 节点结构复杂,缓存不友好 |
| 实现复杂度 | 代码简洁,无复杂平衡操作 | 节点分裂/合并逻辑复杂,代码量大 |
| 写入性能 | 插入/删除仅需调整局部指针 | 插入可能触发递归节点分裂,成本高 |
| 内存占用 | 结构紧凑,无内部碎片 | 节点预分配可能浪费内存 |
- 锁竞争:在并发环境下,B+ 树的锁粒度较粗(如页锁),容易成为性能瓶颈。
- 细粒度锁或无锁:跳表可以通过分段锁或无锁结构(如 CAS)实现高效并发。
Redis 选择使用跳表锁竞争:在并发环境下,B+ 树的锁粒度较粗(如页锁),容易成为性能瓶颈。(Skip List)而不是 B+ 树来实现有序集合(Sorted Set)等数据结构,是经过多方面权衡后的结果。
跳表是怎么实现的?
链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N),于是就出现了跳表。跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,这样的好处是能快读定位数据。
那跳表长什么样呢?我这里举个例子,下图展示了一个层级为 3 的跳表。

图中头节点有 L0~L2 三个头指针,分别指向了不同层级的节点,然后每个层级的节点都通过指针连接起来:
- L0 层级共有 5 个节点,分别是节点1、2、3、4、5;
- L1 层级共有 3 个节点,分别是节点 2、3、5;
- L2 层级只有 1 个节点,也就是节点 3 。
如果我们要在链表中查找节点 4 这个元素,只能从头开始遍历链表,需要查找 4 次,而使用了跳表后,只需要查找 2 次就能定位到节点 4,因为可以在头节点直接从 L2 层级跳到节点 3,然后再往前遍历找到节点 4。
可以看到,这个查找过程就是在多个层级上跳来跳去,最后定位到元素。当数据量很大时,跳表的查找复杂度就是 O(logN)。
那跳表节点是怎么实现多层级的呢?这就需要看「跳表节点」的数据结构了,如下:
1 | |
Zset 对象要同时保存「元素」和「元素的权重」,对应到跳表节点结构里就是 sds 类型的 ele 变量和 double 类型的 score 变量。每个跳表节点都有一个后向指针(struct zskiplistNode *backward),指向前一个节点,目的是为了方便从跳表的尾节点开始访问节点,这样倒序查找时很方便。
跳表是一个带有层级关系的链表,而且每一层级可以包含多个节点,每一个节点通过指针连接起来,实现这一特性就是靠跳表节点结构体中的zskiplistLevel 结构体类型的 level 数组。
level 数组中的每一个元素代表跳表的一层,也就是由 zskiplistLevel 结构体表示,比如 leve[0] 就表示第一层,leve[1] 就表示第二层。zskiplistLevel 结构体里定义了「指向下一个跳表节点的指针」和「跨度」,跨度时用来记录两个节点之间的距离。
比如,下面这张图,展示了各个节点的跨度。

第一眼看到跨度的时候,以为是遍历操作有关,实际上并没有任何关系,遍历操作只需要用前向指针(struct zskiplistNode *forward)就可以完成了。
Redis 跳表在创建节点的时候,随机生成每个节点的层数,并没有严格维持相邻两层的节点数量比例为 2 : 1 的情况。
具体的做法是,跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。
这样的做法,相当于每增加一层的概率不超过 25%,层数越高,概率越低,层高最大限制是 64。
虽然我前面讲解跳表的时候,图中的跳表的「头节点」都是 3 层高,但是其实如果层高最大限制是 64,那么在创建跳表「头节点」的时候,就会直接创建 64 层高的头节点。
数据结构场景
- 在绝大多数情况下,String 更适合存储对象数据,尤其是当对象结构简单且整体读写是主要操作时。
- 如果你需要频繁操作对象的部分字段或节省内存,Hash 可能是更好的选择。
由于购物车中的商品频繁修改和变动,购物车信息建议使用 Hash 存储:
- 用户 id 为 key
- 商品 id 为 field,商品数量为 value
Set 的常见应用场景如下:
- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是
HyperLogLog更适合一些)、文章点赞、动态点赞等等。 - 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集)等等。
- 需要随机获取数据源中的元素的场景:抽奖系统、随机点名等等。
[使用 Set 实现抽奖系统怎么做?]
如果想要使用 Set 实现一个简单的抽奖系统的话,直接使用下面这几个命令就可以了:
SADD key member1 member2 ...:向指定集合添加一个或多个元素。SPOP key count:随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。SRANDMEMBER key count:随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
end