本文的 原始地址 ,传送门
下面的正文,如果 出现图片 缺少, 请去原文查找:
说在前面
在45岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、小米、 去哪儿的面试资格,遇到很多很重要的面试题:
- redis 为什么那么快?
- redis 10并发?怎么实现的? 底层原理是什么?
最近有小伙伴在面试阿里,又遇到了相关的面试题。 小伙伴把 尼恩给他辅导的 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构 说出来,阿里offer就到手了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
Redis 如何实现 100W并发?
Redis,全称 Remote Dictionary Server,是一款开源的内存数据库。
Redis 就好比是电脑里的 “高速缓存区”,数据都存储在内存中,比起传统硬盘存储的数据库,数据读写速度快得惊人。
Redis 凭借超高的读写速度,Redis 在众多数据库中脱颖而出。
官方测试 , Redis速度惊人。
单体 Redis 每秒能完成 10 万次的读写操作,比 MongoDB 快约 10 倍, 比 MySQL 快约 100 倍。
Redis 为什么这么快?
接下来,咱们 一步一步,层层深入, 让面试官 口水直流。
正餐开始之前,尼恩给大家来一道甜点。
首先回顾:redis的 核心模块架构
Client 客户端,官方提供了 C 语言开发的客户端,可以发送命令,性能分析和测试等。
网络层: 基于 I/O 多路复用 事件驱动架构,封装了一个短小精悍的高性能 ae 库,全称是 a simple event-driven programming library (一个简单的 事件驱动 编程库)。 在 ae 这个库里面, 可以通过 aeApiState 结构体对 epoll、select、kqueue、evport 四种底层 操作系统 I/O 多路复用的实现进行适配,让上层调用方感知不到在不同操作系统实现 I/O 多路复用的差异。
命令执行层:命令解析和执行层,负责执行客户端的各种命令,比如 SET、DEL、GET等。Redis 中的事件可以分两大类:一类是网络连接、读、写事件;另一类是时间事件,比如定时执行 rehash 、RDB 内存快照生成,过期键值对清理操作。
内存分配和回收,为数据分配内存,提供不同的数据结构保存数据。
- 高可用模块,提供了副本、哨兵、集群实现高可用。
- 持久化层,提供了 RDB 内存快照文件 和 AOF 两种持久化策略,实现数据可靠性。
- 监控与统计,提供了一些监控工具和性能分析工具,比如监控内存使用、基准测试、内存碎片、bigkey 统计、慢指令查询等。
接下来, 45岁的 老架构 尼恩,带大家 一起 来领略 redis 是如何进行 速度 革命的。
速度很快, 很干活,很硬核。
大家 抓好 扶手 , 坐稳啦。
一、速度 革命 的起点 : 纯内存操作
Redis是一种基于内存的数据库,与传统的基于磁盘的数据库(例如MySQL)不同,它将所有的数据都存储在内存中。
Redis将所有数据存储在内存中,直接跳过了传统数据库依赖的磁盘I/O,这带来了数量级的性能提升。
内存读写速度可达数十纳秒级别(纳秒级),固态硬盘也需微秒级(微秒级),而机械硬盘的随机读写延迟高达数 毫秒(毫秒级)。
这种存储方式使Redis在高频读写场景中,如每秒十万次以上的操作,依然能保持微秒级响应时间,相比磁盘数据库性能提升可达1000倍以上。
Redis将所有数据存储在内存中,避免了传统数据库的磁盘I/O瓶颈。
内存的读写速度远高于磁盘,这使得Redis能够实现超高的响应速度。
机械硬盘的读写速度,大致如下
固态硬盘的读写速度,大致如下
内存的读写速度,和磁盘读写速度的对比:
最快情况下, 固态 硬盘 速度,大致是 内存速度的 百分之一,
最慢情况下, 机械 硬盘 速度,大致是 内存速度的 万分之一,
由此可见, 内存的速度远远快于磁盘。
内存读写速度可以达到每秒数百GB,而磁盘(特别机械硬盘) 读写速度通常只有数十MB,是数 万倍的差距。
内存可以支持更多的数据结构和操作。
常见的数据结构如数组、链表、树、哈希、集合等,常见的操作如排序、查找、过滤、聚合等。内存是一个灵活介质,满足各种复杂和高效的功能,不是磁盘操作可比的。
内存可以支持更高的并发和扩展性。
内存是一种分布式和并行的存储介质,它可以支持多个CPU核心同时访问同一块内存区域,也可以支持多个服务器之间共享同一块内存区域。
磁盘是一种集中式和串行的存储介质,它只能支持一个CPU核心或一个服务器访问同一块磁盘区域,也不能支持多个服务器之间共享同一块磁盘区域。
二、速度革命的 利器神兵:高性能数据结构
Redis的下层 6种数据结构经过精细优化:
- 哈希表采用渐进式rehash,平均时间复杂度保持O(1)
- 压缩列表(ziplist)和整数集合(intset)将小数据存储密度提升3-5倍
- 跳跃表(skiplist)在有序集合操作中,提供平均O(log n)的查询性能,空间开销比平衡树低40%
这些结构使Redis在相同内存下存储能力提升2-3倍,操作性能提升10-20倍。
Redis的上层的 8 种对象类型 , 下层都是基于6种 高效数据结构实现的。
Redis 上层 8 种对象类型 ,包括:
- 字符串(
String
) - Hash
- 列表(
List
) - 集合(
Set
) - 有序集合(
Sorted Set
) - 哈希(
Hash
) - Geo
- Bitmaps
- 等。
Redis 下层 有6种 核心数据结构
- sds( 动态字符串)
- ziplist(压缩列表)
- linkedlist(双端链表)
- hashtable(字典)
- skiplist(跳表)。
Redis 下层 6种 核心数据结构 , 能够在内存中高效地存储和操作数据,为Redis的快速性能提供了坚实的基础。
Redis 下层 6种 核心数据结构 , 可以减少内存占用和计算复杂度,提高数据操作的效率。
Redis 上层 8种对象类型 String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),和三种特殊类型 Geo(地理位置)、HyperLogLog(基数统计)、Bitmaps(位图)。
Redis 上层 8种对象类型 满足各种应用场景的需求。
- String可以用来做缓存、计数器、限流、分布式锁、分布式Session等。
- Hash可以用来存储复杂对象。
- List可以用来做消息队列、排行榜、计数器、最近访问记录等。
- Set可以用来做标签系统、好友关系、共同好友、排名系统、订阅关系等。
- Zset可以用来做排行榜、最近访问记录、计数器、好友关系等。
- Geo可以用来做位置服务、物流配送、电商推荐、游戏地图等。
- HyperLogLog可以用来做用户去重、网站UV统计、广告点击统计、分布式计算等。
- Bitmaps可以用来做在线用户数统计、黑白名单统计、布隆过滤器等。
Redis 上层 8种对象类型 中,其中 List、Hash、Set 和 SortedSet 四种对象类型, 被称为集合类型,特点是一个 key 对应一个集合数据。
Redis 上层 每一种 集合类型,都有两种底层实现结构,
Redis 上层 8种对象类型 的 Redis Object 结构体
Redis中的8种对象类型 ,在存储的时候,都有一个叫redisObject的结构体对象,对应的C语言代码如下:
typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:LRU_BITS; int refcount; void *ptr; } robj;
AI 代码解读
对于不同的数据结构,只需要分配不同的存储空间大小,然后调整不同的type类型和encoding编码格式,就可以有效的实现这些常用存储结构
同时通过ptr这个指针,可以根据type的不同指向不同的存储结构中
即保证了上层结构的统一性,又保证了底层实现的多样性。
有了对这个结构体的简单的了解,我们再来梳理下面的内容
Redis中 下层6种 核心数据结构 计算复杂度
Redis 下层 有6种 核心数据结构
- sds( 动态字符串)
- ziplist(压缩列表)
- linkedlist(双端链表)
- hashtable(字典)
- skiplist(跳表)。
Redis不仅依赖内存,还采用多种高效的数据结构来优化性能:
动态字符串(SDS)
O(1)复杂度获取字符串长度,支持动态扩展。
压缩列表(ziplist)
紧凑存储小型数据,节省内存。
跳跃表(skiplist)
O(log N)复杂度实现有序集合的快速查询。
字典(hash table)
O(1)复杂度实现键值对的快速查找。
高性能 神兵一: SDS(简单动态字符串)
Redis 的底层数据结构有六种,简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组,String 的底层实现是简单动态字符串,List、Hash、Set 和 SortedSet 都有两种底层实现结构,这四种类型被称为集合类型,特点是一个 key 对应一个集合数据
Redis 中的 SDS(Simple Dynamic String)是一种动态扩展长度的字符串实现方式,它是 Redis 内部数据结构的基础,也是字符串数据结构的底层实现。
与许多编程语言和数据结构中的动态数组类似,SDS 能够根据需要动态调整大小,存储和操作字符串数据。
在C
语言中, 传统字符串或者 静态字符串, 用字符数组来表示。
上面是一个 静态字符串 例子, 使用长度为N+1的字符数组来表示长度为 的字符串,并且字符串数组的最后一个元素总是空字符'\0'
。
如果我们想要获取上述 静态字符串 长度, 需要从头开始遍历,直到遇到 '\0' 为止。
SDS是Redis中的一种简单动态字符串结构,它是一种动态大小的字节数组,用于存储和操作字符串数据。
Redis 动态字符串是一种能够动态扩展长度的字符串实现方式。使用一个len
字段记录当前字符串的长度,使用free
表示空闲的长度。
SDS是Redis内部数据结构的基础,也是字符串数据结构的底层实现。
SDS 的结构主要包括以下几个部分:
- len:记录当前字符串的长度。
- free:记录 buf 数组中没有使用的字节的数目,即空闲长度。
- buf:字节数组,用于存储字符串。buf 的大小等于 len + free + 1,其中多余的 1 个字节是用来存储 '\0' 的,以标识字符串的结束。
可以看出C
语言获取字符串长度的时间复杂度为O(N)
,而SDS获取字符串长度的时间复杂度为O(1)
。
想要获取SDS 长度, 只需要获取len
字段即可。
SDS 的源码如下:
/*
* redis中保存字符串对象的结构
*/
struct sdshdr {
//用于记录buf数组中使用的字节的数目,和SDS存储的字符串的长度相等
int len;
//用于记录buf数组中没有使用的字节的数目
int free;
//字节数组,用于储存字符串
char buf[]; //buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的
};
AI 代码解读
这种结构使得 SDS 在获取字符串长度时,只需直接读取 len 字段,时间复杂度为 O(1),而传统 C 语言字符串则需要从头开始遍历,直到遇到 '\0' 为止,时间复杂度为 O(N)。
SDS 与 C 语言字符串的区别
特征 | C 语言字符串(静态字符串) | SDS |
---|---|---|
类型 | 静态字符数组 | 动态字符串结构 |
内存管理 | 需手动分配和释放内存 | 自动扩展和释放内存 |
存储空间 | 需要提前预留足够的空间 | 根据需要动态调整大小 |
长度计算 | 需要遍历整个字符串计算长度 | O(1)复杂度直接获取字符串长度 |
二进制安全 | 不二进制安全 | 二进制安全 |
缓冲区溢出保护 | 不提供缓冲区溢出保护 | 提供缓冲区溢出保护 |
操作复杂度 | 操作复杂度随字符串长度增加而增加 | 操作复杂度不受字符串长度影响 |
可拓展性 | 不易扩展,需要手动处理内存扩展 | 自动扩展,支持动态调整大小 |
SDS 的优点
1 二进制安全:
- SDS 可以存储任意二进制数据,包括图片、视频、音频等,而不会受到特殊字符或空字符的限制。
- 这使得 SDS 具有更广泛的适用性,能够处理各种类型的数据。
2 动态扩展:
- SDS 的大小可以根据存储的字符串长度动态调整。
- 这种动态扩展的能力使得 SDS 能够处理任意长度的字符串数据,而不需要提前预留足够的空间。
3 O(1)复杂度的操作:
- SDS 支持常数时间复杂度的操作,如添加字符、删除字符、修改字符等。
- 无论字符串的长度是多少,这些操作的时间开销都是固定的,从而保证了高效的性能。
4 缓冲区溢出保护:
- SDS 在存储字符串时,会自动添加一个空字符 '\0' 作为字符串的结束标志。
- 这种缓冲区溢出保护能够防止缓冲区溢出的问题,提高系统的稳定性和安全性。
5 惰性空间释放:
- 当 SDS 缩短字符串时,并不会立即释放多余的空间,而是将多余的空间保留下来以备后续的再利用。
- 这种惰性空间释放的策略可以减少内存分配和释放的开销,提高内存利用率。
这些优点使得SDS在Redis中被广泛应用于存储和操作字符串数据,为Redis的高性能和高可靠性提供了坚实的基础。
SDS 作为 Redis 中的字符串数据结构底层实现,具有二进制安全、动态扩展、O(1)复杂度的操作、缓冲区溢出保护和惰性空间释放等优点。
高性能 神兵二: 压缩列表
Redis中的压缩列表(ziplist)是一种特殊的数据结构,用于存储列表和哈希数据类型中的元素。
压缩列表通过将多个小的数据单元压缩在一起,以节省内存空间,并提高访问效率。
什么是压缩列表(Ziplist)?
Redis 中的压缩列表(Ziplist)是一种专门设计用来存储小型数据集合的数据结构。
它可以将多个小的数据单元紧密地压缩在一起,从而节省内存空间并提高访问效率。
一个 压缩列表(Ziplist) 逻辑划分为多个字段,如图所示:
各字段含义如下:
- zlbytes:整体长度,也就是 压缩列表的字节长度,占4个字节,因此压缩列表最长(2^32)-1字节;
- zltail: 尾部位置, 压缩列表尾元素相对于压缩列表起始地址的偏移量,占4个字节;
- zllen:节点数量, 压缩列表的元素数目,占两个字节(2^16) ;那么,当压缩列表的元素数目超过(2^16)-1怎么处理呢?此时通过zllen字段无法获得压缩列表的元素数目,必须遍历整个压缩列表才能获取到元素数目;
- entry1......entryN:压缩列表存储的 元素 内容,可以为字节数组或者整数;
- zlend:压缩列表的结尾,占一个字节,恒为0xFF。
压缩列表以一种紧凑的方式存储数据,将多个元素紧密地排列在一起,节省了存储空间。
在压缩列表中,相邻的元素可以共享同一个内存空间,这种紧凑的存储形式可以大大减少内存的消耗。
压缩列表(Ziplist)与普通列表(如传统双向链表)的 对标分析:
1、 结构设计对比
特性 | 压缩列表(Ziplist) | 普通列表(如双向链表) |
---|---|---|
存储方式 | 连续内存空间(类似字节数组),通过 偏移量 定位节点 | 离散内存块,通过前后指针关联节点 |
节点结构 | 无指针,存储previous_entry_length (前驱节点长度)+ encoding (编码类型)+ 数据内容 |
包含前驱指针、后继指针及数据内容 |
头部元数据 | 包含zlbytes (总字节数)、zltail (尾节点偏移)、zllen (节点数量) |
通常仅记录头尾指针和节点数量 |
2、内存效率对比
维度 | 压缩列表 | 普通列表 |
---|---|---|
内存占用 | 极低:无指针开销,变长编码优化存储 | 较高:每个节点需额外存储两个指针(每个指针占8字节) |
空间利用率 | 通过紧凑存储减少内存碎片,适合小数据场景 | 易产生内存碎片,空间利用率较低 |
3、访问性能对比
操作类型 | 压缩列表 | 普通列表 |
---|---|---|
随机访问 | O(n)复杂度,需遍历节点 | O(n)复杂度,但指针直接定位更快 |
头尾操作 | O(1)复杂度(利用zltail 快速定位尾节点) |
O(1)复杂度(头尾指针直接访问) |
插入/删除 | 可能触发连锁更新(需重新分配内存并调整后续节点) | O(1)复杂度(仅修改指针) |
压缩列表 优点
1 紧凑的存储形式
想象一下,有一堆小玩具,如果把它们散乱地放在箱子里,会占用很多空间。
但如果把它们紧凑地排列在一起,就能节省很多空间。
压缩列表就是这样,它把多个元素紧密地排列在一起,大大节省了存储空间。
2 灵活的编码方式,进一步压榨内存空间
压缩列表中的每个元素都可以根据需要选择不同的编码方式。
比如,对于小的整数,它可以使用更紧凑的整数编码;
对于字符串,它可以使用字符串编码;
对于二进制数据,它可以使用字节数组编码。
这种灵活性使得压缩列表能够根据不同元素的特点来优化存储。
3 高效访问: 快速的随机访问
就像在书架上找书一样,如果根据书的编号,很快找到想要的那本。
压缩列表也是这样,它支持快速的随机访问操作,可以通过下标索引来直接访问列表中的任意元素。
4 自动适应: 动态调整大小
压缩列表能够根据实际需要自动调整大小。当元素数量增加时,它会分配更多的内存空间来容纳这些元素;当元素数量减少时,它会释放多余的内存空间来节省资源。这种动态调整大小的能力使得压缩列表能够高效地管理内存。
5 小而美: 适用于小型数据集
压缩列表特别适合于存储数量较少但访问频繁的数据。
比如,可以用它来存储一个用户的个人信息、一个商品的详细信息等。
由于它采用紧凑的存储形式并支持 快速的随机访问,因此能够在这些场景下发挥出色的性能。
压缩列表使用案例
假设有一个包含10个短字符串的 list 列表,使用普通的列表结构可能会占用较多的内存。
但如果使用压缩列表来存储这个列表,内存占用可能会减少50%甚至更多。
原因是:压缩列表以一种紧凑的方式存储数据,将多个元素紧密地排列在一起,节省了存储空间。
由于压缩列表采用紧凑的存储形式,并且支持快速的随机访问,因此特别适合于存储数量较少但访问频繁的数据。
所以 ,压缩列表特 别适合于: 存储数量较少 但访问频繁的数据, 也就是 小型数据集。
小型数据集,例如长度较短的列表或者哈希表。
高性能 神兵3: 双端链表(/双向链表)
Redis中的双端链表是一种优化后的数据结构,专门用于存储有序的元素集合。
双端链表 具备双向链接的特性,即每个节点都包含指向前一个节点和后一个节点的指针。
Redis中的双端链表是一种经过优化的数据结构,用于存储有序的元素集合。
双端链表 的 链表整体结构
除了节点外,双端链表还有描述链表整体属性的元数据,也就是 链表整体结构
链表整体结构 ,是 描述链表整体属性的元数据 ,主要包括:
- 头节点(head node):链表的第一个节点,通常用于快速定位链表的起始位置。头节点的
prev
指针为NULL
。 - 尾节点(tail node):链表的最后一个节点,通常用于快速定位链表的结束位置。尾节点的
next
指针为NULL
。 - 长度(len):记录链表的当前长度。每当链表中添加或删除节点时,这个值会相应更新,以反映链表的当前长度。
len
字段,用于记录链表的当前长度。这个字段的优势在于:快速获取长度
获取链表长度时,不需要遍历整个链表,直接读取len
字段即可,时间复杂度为O(1)。
头节点与尾节点的作用
- 头节点:作为链表的入口,方便进行头部插入和删除操作。
- 尾节点:作为链表的出口,方便进行尾部插入和删除操作。
双端链表 节点结构
节点是双端链表的基本构建单元,每个节点包含以下关键部分:
- prev指针:指向前一个节点的指针。如果当前节点是链表的头节点,则此指针为
NULL
。 - next指针:指向后一个节点的指针。如果当前节点是链表的尾节点,则此指针为
NULL
。 - value数据域:用于存储链表节点的数据元素。这个数据元素可以是任意类型,因此使用
void *
类型来表示,使得双端链表具有通用性。
它具有双向链接的特性,每个节点都包含指向前一个节点和后一个节点的指针。
typedef struct list {
listNode *head; // 头节点指针
listNode *tail; // 尾节点指针
unsigned long len; // 链表长度
// 其他字段...
} list;
AI 代码解读
从结构中可以看出元数据中还有两个特殊的节点:头节点(head node)和尾节点(tail node),它们分别位于链表的头部和尾部。
通过这些属性,双端链表节点构成了链表的基本组成部分,它们通过prev
和next
指针连接在一起,形成了双向链接的链表结构。
双端链表的 优势
通过头节点和尾节点,可以方便地对双端链表进行头部插入、尾部插入、头部删除、尾部删除等操作,从而实现了对双端链表的高效操作。
- 头部插入:在链表的头部添加新节点。
- 尾部插入:在链表的尾部添加新节点。
- 头部删除:从链表的头部删除节点。
- 尾部删除:从链表的尾部删除节点。
这些操作使得双端链表在处理有序元素集合时非常高效。
高性能 神兵四: 渐进式扩容 双 哈希结构(/字典)
Redis 的底层数据结构有六种,简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组
上层的 Hash、Set 两种 对象类型 ,都 用到了 下层的 哈希表(/字典) 。
在Redis中,字典(dictionary)是一种用于存储键值对数据的数据结构,也称为哈希表(hash table)。
字典是Redis中最常用的数据结构之一,具有快速查找、动态调整大小、哈希冲突处理、迭代器支持等特点,适用于各种数据存储和操作需求,实现键值对存储和快速查找。
在Redis中hashtable就是字典dict
通过源码,可以看到dict是这样定义的:
哈希表的实现
Redis 的哈希表实现了一个“分离链表法”的哈希表结构,具体如下:
- 槽位数组(buckets):一个数组,数组的每个元素是一个链表的头节点,链表用于存储发生哈希冲突的键值对。每个数组元素是一个链表的头节点。
- 链表节点:每个节点包含一个键值对,以及指向下一个节点的指针。
- 哈希函数:用于将键映射到哈希表数组的索引位置。Redis 使用了一个高效的哈希函数(如 MurmurHash)来计算键的哈希值,并将哈希值映射到哈希表数组的索引位置。
- 链表(分离链表法):当多个键值对映射到同一个哈希表数组索引时,这些键值对会以链表的形式存储,链表的每个节点包含键值对。
Redis 中 hashtable 具体由 dictht
来实现。
dictht
中,table
结构是一个 dictEntry
类型的数组,而 dictEntry
是一个单向链表中的节点,代表的是 hashtable 中的一条记录,包含了该条记录的 key
、value
以及指向下一个节点(下一条记录)的指针(hash 碰撞)。
字典以键值对的形式存储数据,每个键都与一个值相关联。
dictEntry
中的 key
是 void*
类型,意味着可以存储任意数据类型。
val
是一个 包含 void*
类型 的联合体, 也意味着可以存储任意数据类型。
所以, 任何的Redis 中一些Object 对象类型 ,都可以存储在 redisDb
中的 dict
中。
dictht
中的 size
表示 hashtable 中 bucket
的数量。而 used
则用来存储当前 hashtable 中实际存储的记录(元素)的数量。
在Redis中,键和值都可以是任意类型的数据,如字符串、整数、列表或哈希表。
字典利用哈希表实现,具备快速查找的特性。
hashtable 通过将键映射到哈希表的索引位置,字典能以常数时间复杂度(O(1))内查找、插入和删除键值对,即使在大型数据集中也能保持高效。
此外,hashtable 支持动态调整大小,随着键值对数量的变化,能自动扩展或收缩内存空间,以适应数据量的变化。
双哈希表结构 : 双表 渐进式rehash
Redis 采用单进程单线程模型,所有操作都在一个线程中顺序执行。
因此,任何耗时的操作都会阻塞整个服务,影响客户端的请求处理。
随着数据量的增长,Redis 的哈希表需要扩容以提高性能和存储能力。但扩容操作本身可能会非常耗时,导致服务阻塞。
如果扩容操作直接在主仓库里进行,那整个服务就会被卡住,客户端的请求就没办法及时处理。
为了不阻塞服务,Redis 设计了两个“仓库”(两个哈希表):ht[0]
和 ht[1]
。
为了在扩容过程中避免阻塞服务,Redis 设计了双哈希表结构 dict.ht
,其中包含两个哈希表:
- ht[0]:主哈希表,用于存储当前所有数据,并处理客户端的读写请求。
- ht[1]:备用哈希表,用于扩容操作。
双表 渐进式rehash 扩容过程
扩容过程分为以下几个阶段:
扩容准备
当检测到需要扩容时,Redis 会在 ht[1]
中创建一个新的、更大的哈希表。
这个过程不会阻塞服务,因为 ht[0]
仍然在正常工作。
rehash 过程
rehash 的目的 将 ht[0]
中的数据逐步迁移到 ht[1]
中。
逐步迁移:
为了避免一次性迁移所有数据导致阻塞,Redis 采用逐步迁移的方式:
- 每次最多迁移一个 bucket(哈希桶)的数据。
- 每次迁移操作都在客户端的读写请求中完成,利用空闲时间进行。
- 如果连续遇到 10 个空的 bucket,则本次 rehash 操作终止,等待下一次机会继续。
新数据的处理
在 rehash 过程中,所有新添加的数据直接存储到 ht[1]
中,而不是 ht[0]
。
这样可以确保新数据不会被遗漏。
rehash 完成
当 ht[0]
中的所有数据都迁移到 ht[1]
后:
- 将
ht[1]
的指针赋值给ht[0]
,使ht[1]
成为新的主哈希表。 - 重置
ht[1]
,为下一次扩容操作做准备。
双表 渐进式rehash 扩容过程 总结
Redis 的扩容机制通过以下步骤实现:
双哈希表结构:使用
ht[0]
和ht[1]
分离当前数据和扩容操作。逐步 rehash:通过每次迁移少量数据,避免一次性操作导致的阻塞。
新数据处理:在 rehash 过程中,新数据直接存储到
ht[1]
中。rehash 完成:迁移完成后,切换主哈希表并重置备用哈希表。
双表 渐进式rehash 优势
- 避免阻塞:通过逐步迁移和双哈希表结构,确保服务不会因扩容而中断。
- 高效响应:rehash 过程中遇到空 bucket 时及时终止,确保客户端请求能够快速响应。
- 无缝切换:新数据直接存储到备用哈希表,迁移完成后无缝切换主哈希表,不影响服务。
通过这种系统化的设计,Redis 在保证高性能的同时,能够灵活应对数据量的增长,确保服务的稳定性和可靠性。
高性能 神兵五: 跳表
跳表(Skip List)是一种基于链表的数据结构,它利用多级索引来加速查找操作,类似于平衡树,但实现起来更加简单,具有较好的平均查找性能。
跳表是一种采用了用空间换时间思想的数据结构。
跳表会随机地将一些节点提升到更高的层次,以创建一种逐层的数据结构,以提高操作的速度。
跳表 相关的面试题,也是大厂非常喜爱的面试题, 关于跳表的大厂面试题:
跳表的结构
跳表的做法就是给链表做索引,而且是分层索引,
跳表通过维护多级索引,每个级别的索引都是原始链表的子集,用于快速定位元素。
每个节点在不同级别的索引中都有一个指针,通过这些指针,可以在不同级别上进行快速查找,从而提高了查找效率。
单层跳表
单层跳表, 可以退化到一个链表
查找的时间复杂度是 O(N)
两层跳表
两层跳表 = 原始链表 + 一层索引
两层跳表查询
如查询id=11的数据,我们先在上层遍历,依次判断1,6,12,
很快就可以判断出11在6到12之间,
第二步,然后往下一跳,进入原始链表,就可以在遍历6,7,8,9,10,11之后,确定id=11的位置。
通过第一级索引,直接将查询范围从原来的1到11,缩小到现在的1,6,7,8,9,10,11。
三层跳表
三层跳表 = 原始链表 + 第一层索引 + 第二层索引
三层跳表查询
如果还是查询id=11的数据,就只需要查询1,6,9,10,11就能找到,比两层的时候更快一些。
在Redis中跳表 结构
在Redis中,跳表 提供了高效的有序数据存储和检索功能。
跳表的平均查找性能为O(log n),与平衡树相当,但实现起来更加简单。
跳表通过多级索引来实现快速查找,使得查找时间随着数据量的增加而呈对数增长。
但是跳表的空间复杂度相对较高,因为它需要额外的空间来维护多级索引。不过跳表的空间占用通常是合理的,且具有可控性,可以根据实际需求调整级别和索引节点的数量,以平衡空间和性能的需求。
除此之外,跳表支持动态调整大小,可以根据实际需要自动扩展或收缩内存空间。
当有序集合中的元素数量增加时,跳表会动态地增加级别和索引节点,以提高查找效率;
当元素数量减少时,可以收缩跳表的大小,以节省内存资源。
并且跳表的插入和删除操作具有较高的效率,通过维护多级索引,可以在O(log n)的时间复杂度内完成插入和删除操作。
所有的数据一锅端:全局 哈希表
Redis 的所有 键值对 , 用 Redis的全局 哈希表 存储。
其实是Redis 底层使用了一个全局哈希表
保存所有键值对,哈希表的最大好处就是 O(1) 的时间复杂度快速查找到键值对。
redisDb 结构,表示 Redis 数据库的结构,结构体里存放了指向了 dict 结构的指针;//默认有16个数据库
dict 结构,结构体里存放了 2 个哈希表,正常情况下都是用
哈希表1
,哈希表2
只有在 rehash 的时候才用;ditctht 结构,表示哈希表的结构,结构里存放了哈希表数组,数组中的每个元素都是指向一个哈希表节点结构(dictEntry)的指针;
一个 dictEntry table 哈希表其实就是一个数组, 全局哈希表就是一个dictEntry数组, 由 dictEntry组成 。
数组中的元素 dictEntry 常常 叫做哈希桶。
dictEntry 结构, 里存放了 void* key
和 void* value
指针, key 指向的是 SDS(动态String) 对象,而 value 就是指Redis的几种数据类型,比如SDS。
三、速度革命的根本1: 单线程 无锁架构
多线程的问题
多线程场景,存在的 线程同步、锁、竞争等问题, 需要花费时间和资源在多线程之间的上下文切换上。
单线程模型,纯 无锁架构
单线程的好处:没有多线程面临的共享资源的并发控制问题
看看 netty的 单线程模型,纯 无锁架构。
关于netty的学习,一定要看尼恩编著的, 清华大学出版社出版的《java高并发核心编程 卷1 加强版 》, 由浅入深、通俗易懂。
Netty 中, 一个 连接通道 会绑定 到 一个事件反应器 ,一个 事件反应器 一般会绑定 一个thread 线程 。
默认情况下,一个事件反应器 会有 一个事件处理 流水线, pipeline。
而 pipeline 流水线里边,装配的 就是咱们的 业务handler。这就是 责任链模式。
上面讲到, 一个 事件反应器 一般是 一个thread 线程, 默认 由 这个 thread线程 执行 pipeline 里的 handler处理器,执行的时候,默认是单线程的,而且是不加锁的, 这就是 无锁架构。
Netty的性能之所以高, 无锁架构 是根本 之一。
Redis速度革命的根本,也是因为 他的 单线程模型 。
Redis 单线程模型
Redis使用单线程模型,和netty 在 架构 层面, 其实是相似的,甚至是非常类似的。
Redis 单线程模型, 意味着Redis 只使用一个CPU来处理所有请求, 所以也不用加锁。
Redis采用单线程模型处理客户端请求,避免了多线程环境下的上下文切换和锁竞争问题。
虽然单线程看似“简单”,但它带来了以下优势:
原子性
每个操作都是原子执行的,无需考虑并发问题。
无锁架构
没有锁竞争,CPU利用率更高。
这使得Redis的设计和实现更简单,性能和效率更高。
那么,Redis为什么选择单线程模型呢?
主要有以下几个原因:
- Redis性能瓶颈不在于CPU,而在于内存和网络。
因为Redis使用内存存储数据,所以数据访问非常迅速,不会成为性能瓶颈。
此外,Redis的数据操作大多数都是简单的键值对操作,不包含复杂计算和逻辑,因而CPU开销很小。
相反,Redis的瓶颈在于内存的容量和网络的带宽,这些问题无法通过增加CPU核心来解决。
- Redis的单线程模型可以保证数据的一致性和原子性。
由于Redis只有一个线程来处理所有的请求,所以不会出现多个线程同时修改同一个数据的情况,也不需要使用锁或事务来保证数据的一致性和原子性。
- Redis的单线程模型可以避免多线程编程的复杂性和难度。
例如线程安全、死锁、内存泄漏、竞态条件等,降低了开发和维护的成本和风险。
Redis 6.x ,不是说多线程吗?
是的, Redis 6.x开始引入了多线程, 但是多线程仅仅是在 处理网络IO。
Redis 核心命令执行依然是单线程,确保性能和一致性。
注意: 核心命令执行依然是单线程。
或者说, Redis 6.x 的 IO线程模型,从 单线程的 Reactor 反应器, 演进到了 多线程 的 Reactor 反应器。
或者说, Redis 6.x 升级成为了 一个C语言版本的Netty。
关于单线程的 Reactor 反应器, 如何 演进到了 多线程 的 Reactor 反应器,一定要看尼恩编著的, 清华大学出版社出版的《java高并发核心编程 卷1 加强版 》, 由浅入深、通俗易懂。
四、速度革命的根本2:事件驱动EDA架构 + 多路IO复用模型
回顾一下,五大 IO模型:
1、阻塞式IO模型 :这是最传统的IO模型, sockt.read() 没有等到结果会一直等待
2、非阻塞式IO模型:不会阻塞但是,客户端会一直轮询,CPU还是被占用
3、IO复用模型:内核有一个线程会不断去轮询每个连接的状态,观察是否有事件发生
4、信号驱动IO模型:发起的sockt请求,会注册一个信号函数,然后用户线程会继续执行,准备就绪的时候会发一个信息
5、异步IO模型:用户线程发起一个read操作后可以立马去做另外的事情了,当内核返回一个成功的信号表示IO已经完成了,就可以直接去使用数据。
关于 五大 IO模型 ,一定要看尼恩编著的, 清华大学出版社出版的《java高并发核心编程 卷1 加强版 》, 由浅入深、通俗易懂。
Redis使用单线程模型来处理客户端的请求,但是它能够利用多路I/O复用技术来实现高并发和高吞吐量。
那么,什么是多路I/O复用模型?
多路I/O复用模型是指使用一个线程来监控多个文件描述符(fd)的读写状态,当某个fd准备好执行读或写操作时,就通知相应的事件处理器来处理。
多路I/O复用模型 , 就是通过少量线程 监控大量 连接,提升了 线程的利用率,减少了线程阻塞,提高了I/O效率和利用率。避免了阻塞式I/O模型中,单个线程只能等待一个fd的问题。
例如Linux系统中提供了多种多路I/O复用技术的实现方式,如select、poll、epoll等。
select/epoll等IO多路复用技术提供了一种基于事件触发的回调模式,每当有不同事件发生时,Redis能够迅速调用相应的事件处理器,始终保持在处理事件的状态,从而提升了其响应速度。
下面是 IO多路复用技术 的一个基础的 流程图:
Redis通过使用IO多路复用技术(如epoll、kqueue或select等),在一个线程内同时监听多个socket连接,当有网络事件发生时(如读写就绪),再逐一处理。
由于Redis线程并不会因为等待某个特定socket的IO操作完毕而停滞,它可以流畅地在多个客户端间切换,即时响应每个客户端的不同请求,从而实现在单线程环境下对大量并发连接的有效处理和高并发性能。
这样可以处理大量并发连接,并在单线程中高效地调度网络事件,使得单线程也能应对高并发场景。
所以Redis服务端,整体来看,就是一个以事件驱动的程序。
它的操作都是基于事件的方式进行的。客户端请求被分发给文件事件分派器,再由事件处理器处理。
Redis的事件驱动架构如图:
Redis的事件驱动架构是一种基于非阻塞I/O多路复用技术设计的高效处理并发请求的机制。
在Redis中,事件驱动架构通过监听和处理各种网络I/O事件以及定时事件,使得Redis服务端能够在一个线程内高效地服务于多个客户端连接,并执行相关的命令操作。
Redis 事件驱动架构 EDA架构
Redis利用IO多路复用技术(如epoll)在一个线程内同时监听多个客户端连接,当有事件发生时,立即处理。这种设计让单线程的Redis也能高效处理高并发请求。
Redis 事件驱动架构主要由以下几个组成部分构成:
EDA架构之1 :套接字(Socket)
套接字是客户端与Redis服务端之间进行通信的基础接口,用于双向数据传输。
EDA架构之2: I/O多路复用 底层组件
Redis服务端通过使用如epoll、kqueue等I/O多路复用技术,可以同时监听多个套接字上的读写事件。
当某个客户端的套接字上有数据可读或可写时,内核会通知Redis服务端,而无需Redis反复检查每一个套接字状态。
Redis默认使用的IO多路复用技术确实是epoll。
epoll 主要优点如下:
- 并发连接限制
相比于select和poll,epoll没有预设的并发连接数限制,能够处理的并发连接数只受限于系统资源,适合处理大规模并发连接。
- 内存拷贝优化
epoll采用事件注册机制,仅关注和通知就绪的文件描述符,无需像select和poll那样在每次调用时都拷贝整个文件描述符集合,从而减少了内存拷贝的开销。
- 活跃连接感知
epoll提供了水平触发(level-triggered)和边缘触发(edge-triggered)两种模式,可以更准确地感知活跃连接,仅当有事件发生时才唤醒处理,避免了无效的轮询操作,提升了事件处理的效率。
- 高效事件处理
epoll利用红黑树存储待监控的文件描述符,并使用内核层面的回调机制,当有文件描述符就绪时,会直接通知应用程序,从而减少了CPU空转和上下文切换的成本。
EDA架构之3 :文件事件分派器(File Event Demultiplexer):
文件事件分派器是Redis事件驱动的核心组件,它负责将内核传递过来的就绪事件分发给对应的处理器。
在Redis中,每个套接字都关联了一个或多个事件处理器,如客户端连接请求处理器、命令请求处理器和命令响应处理器等。
EDA架构之4 :事件处理器(Event Handlers):
事件处理器是Redis中处理特定事件的实际执行者。
当文件事件分派器接收到一个就绪事件时,它会调用对应的事件处理器来执行相应操作,如读取客户端的命令请求,执行命令并对结果进行编码,然后将响应数据写回客户端。
而对于Redis中设计的事件主要分为两个大类:
- 文件事件(File Events):
主要对应网络I/O操作,包括客户端连接请求(AE_READABLE事件)、客户端命令请求(AE_READABLE事件)和服务端命令回复(AE_WRITABLE事件)。
- 时间事件(Time Events):
对应定时任务,如键值对过期检查、持久化操作等。所有时间事件都被存放在一个无序链表中,每当时间事件执行器运行时,会遍历链表并处理已到达预定时间的事件。
通过事件驱动架构,Redis能够在一个线程内并发处理大量客户端请求,而无需为每个客户端创建独立的线程。此外,由于Redis的高效内存管理、数据结构优化和单线程模型,避免了多线程环境下的锁竞争和上下文切换开销,从而实现了极高的吞吐量和响应速度。
Reis EDA架构 总结
在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字;
内核会一直监听,连接请求 和数据请求,一旦有请求到达就会交给redis。
从而实现一个Redis线程处理多个IO流的效果 不断轮询,将发生的事件放到队列当中;
redis基于事件处理队列 直接处理事件;
所以如果队列中有处理慢点操作,就会影响其他
Redis EDA架构 性能优势:
- 无需为每个连接创建线程,减少资源消耗。
- 快速响应客户端请求,延迟低。
80分 (高手级) 答案
尼恩提示,讲完 纯内存 + 尖端结构 + 无锁架构 + EDA架构 , 可以得到 80分了。
但是要直接拿到大厂offer,或者 offer 直提,需要 120分答案。
尼恩带大家继续,挺进 120分,让面试官 口水直流。
五、速度 革命 的加速 :Redis 异步的日志架构
尼恩给大家总结一下。 前面讲了:
1、速度 革命 的起点 : 纯内存操作
2、速度革命的 神兵:高性能数据结构
3、速度革命的根本1: 单线程 无锁架构
4、速度革命的根本2:事件驱动EDA架构 + 多路IO复用模型
那么多Redis的 速度 革命 , 使得 Redis 单节点的redis 速度 天下无敌, 可以 轻松的上天入地, 狂飙到达 2w-10wqps。
但是,一旦当机了, 如何实现 快速 恢复 呢?
这就需要 用到 持久化机制。
Redis 提供了两种主要的日志类型,分别是 RDB(Redis Database)快照 和 AOF(Append Only File)日志。
这两种日志机制分别用于不同的场景,帮助实现数据的持久化和快速恢复。
不过 , redis 的 访问内存操作 ,才是 主角操作、或者 主路操作。 而 持续化机制 是配角 ,小配角、旁路操作。
所以 为了不影响 主路操作的 性能,持续化机制 这种 旁路操作,只能 异步,只能 异步,只能 异步,只能 异步。
接了下来 看看第 5点,redis 速度 革命 的加速 :Redis 异步的日志架构
第一大持久化机制:RDB 快照
RDB 是 Redis 的一种内存快照机制,用于定期将内存中的数据保存到磁盘上。它适合于需要快速恢复数据的场景。
RDB 实现方式:RDB 会定期生成一个包含当前内存数据的快照文件(.rdb
文件),该文件可以用于数据恢复。
RDB 日志 两个命令 save、bgsave
1 Save: 会阻塞主线程 ,这个比较古老, 不建议用了
2 Bgsave: 专门个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是Redis RDB 文件生成的默认配置。
bgsave 如何执行? 写时复制技术
1、bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。
bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件(复制的其实是基于原始数据的。虚拟- 物理 映射表,当主线程数据修改后,对应的物理地址也会修改,但是并不会影响子线程 bgsave 生成 RDB文件)
2、拍快照过程中, 产生的写操作会被复制一份,bgsave函数会将他读取到 RDB中
频繁 bgsave 执行会带来额外的开销
1、磁盘的压力
2、fork创建这个过程会阻塞主线程
RDB 日志优点:
- 快速生成快照,对性能影响较小。
- 文件体积较小,适合备份和灾难恢复。
RDB 日志缺点:
- 如果 Redis 服务器在两次快照之间崩溃,可能会丢失部分数据。
第二大异步持久化:AOF 日志
AOF 是 Redis 的一种追加日志机制,用于记录每个写操作的命令。它适合于需要高数据可靠性的场景。
AOF 实现方式:AOF 会将每个写操作的命令追加到日志文件中,Redis 在重启时会重新执行这些命令以恢复数据。
AOF 日志 的记录方式 是异步的,流程是: 执行命令>写入内存 >写日志
AOF 日志 异步 优点:避免记录错命令、不会阻塞当前操作
写日志 的操作,存在 宕机 导致 数据丢失问题,可以 通过 配置回写磁盘的策略 去解决
AOF 的 日志重写机制
RDB持久化是将进程数据写入文件,而AOF持久化,则是将Redis执行的每次写、删除命令记录到单独的日志文件中,查询操作不会记录; 当Redis重启时优先执行AOF文件中的命令来恢复数据。
与RDB相比,AOF的实时性更好,因此已成为主流的持久化方案。
AOF 持久化 的不足是什么呢?
尼恩告诉大家, AOF 存在 体积爆炸 问题。 具体说: 随着时间推进, AOF文件 会越来越大,AOF文件体积爆炸, 通过AOF文件进行还原出数据库的时间也会相应增加。
为了解决AOF体积爆炸,Redis提供了AOF重写功能。
什么是 AOF 的 日志重写机制?
很简单,创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的,但 新 AOF文件 去掉 老的 冗余命令,通常体积会较旧AOF文件小很多。
重写机制具有多变一的功能,旧日志文件中的多条命令 可以再重写,重写 后的新日志就变成一条新的日志了,具有多变一的功能。
AOF 日志重写机制 会不会阻塞主线程呢?
不会,是利用子线程进行复制拷贝,总结来说就是 一个拷贝,两处日志, 或者说 “一个复制,两份记账”。
复制过程 不会卡主线程,整个过程是让“小弟”(子进程)干活,主线程继续服务用户。
“1 一个拷贝”
- 当需要重写AOF日志时,主线程会生个小弟(fork子进程),让小弟去整理和压缩旧的AOF日志 。
- 关键点:主线程生小弟的瞬间(fork操作)会卡一下,但时间极短(除非内存特别大) 。生成小弟后,主线程继续干活,小弟在后台默默 复制 AOF 旧的数据(实际用的是“写时复制”技术:数据没改就直接用,改了再复制新副本) 。
“2 两份记账”
- 第一份账:主线程正常处理新操作,把命令记录到 AOF 缓冲区 ,异步刷新到 原来的AOF日志里(比如每秒刷一次磁盘) 。
- 第二份账:同时,新操作还会被额外记录到 AOF重做缓冲区,等小弟整理完旧日志后,这些新操作会被追加到新的AOF文件里,保证数据不丢失 。
2 两份记账 的意思是,小弟 拷贝过来后,新的有操作记录到 AOF 缓冲区,同时也会在AOF重做缓冲区,等拷贝后,新的操作也会被记录到重写AOF当中
AOF 日志重写机制 复制过程中, 是否存在阻塞风险?
1、子线程复制主线程的数据,是复制主线程 的内存表,也就是 (虚拟内存 与物理内存的映射关系),这个过程中可能会发生阻塞
2、重写复制的过程中,如果有一个bigkey进来,重新申请大块内存风险会变大,可能会产生阻塞风险。
AOF 优点:
- 数据完整性高,即使服务器崩溃,也能通过日志恢复到最近的写操作。
- 支持多种写入模式(如同步、异步等),可以根据性能需求进行调整。
AOF缺点:
- 日志文件体积较大,可能需要定期进行压缩和优化。
RDB 日志 和 AOF(Append Only File) 对比
- RDB 适合对性能要求较高,且能接受一定数据丢失的场景。
- AOF 适合对数据完整性要求较高的场景,但会占用更多磁盘空间。
RDB+ AOF混合持久模式
Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的功能,拍快照之后,到下次快照的中间,会有AOF增量变化的日志。
可以 结合RDB和AOF优势,重启时,优先加载RDB恢复数据,再重放AOF增量操作‘
RDB+ AOF混合持久模式 , 进行 恢复速度与数据完整性 的二者 平衡。
Redis的RDB和AOF进行异步持久化,并且优化了策略。提供了灵活的性能-可靠性权衡:
- RDB每秒级快照,恢复速度达100MB/s
- AOF追加写入支持fsync可配置,每秒持久化模式下性能损耗<5% 这种设计使Redis在保证数据安全的同时,将持久化对性能的影响控制在可接受范围内,主从同步带宽占用<10%,实现了高性能与可靠性的完美平衡。
Redis的 线程模型与异步机制 的大总结
Redis的异步日志架构,主要通过其持久化机制和线程模型实现,核心设计理念是非阻塞主线程以确保高性能。
1 主线程职责
负责处理客户端请求、执行命令, 执行网络IO(单线程模型),避免锁竞争和上下文切换开销。
2 后台线程
- 异步任务 :如AOF刷盘( everysec 策略)、惰性删除(UNLINK命令)等,由后台线程处理 。
- 子进程 :RDB生成、AOF重写等CPU密集型任务通过子进程实现,避免阻塞主线程 。
3 RDB异步 快照生成
RDB文件 是异步生成的,Redis通过fork
子进程生成内存数据的二进制快照(RDB文件),子进程独立于主线程完成磁盘写入,主线程继续处理客户端请求。
- 触发方式:手动执行
BGSAVE
或配置自动触发条件(如时间间隔和数据变更量)。注意 这里不用sava ,save是同步阻塞式的操作。 - 优势:生成过程完全异步,不影响主线程性能。
4 AOF 日志 异步 追加
Redis将写操作记录到AOF(Append-Only File)中,支持三种同步策略:
- always:每次写操作同步刷盘(非异步,可靠性高但性能低)。
- everysec:每秒批量同步一次(默认策略,异步执行)。
- no:由操作系统控制刷盘(异步但数据丢失风险较高)。
5 异步的 AOF重写优化
通过BGREWRITEAOF
命令或自动触发,子进程根据当前数据集生成紧凑的新AOF文件替换旧文件,避免日志爆炸。
六:速度 革命 的腾飞 : Redis Cluster 横向扩展,实现 10Wqps-100Wqps
前面讲了那么多Redis的 速度 革命, 使得单节点的redis 速度 天下无敌, 可以到达 2w-10wqps:
1、速度 革命 的起点 : 纯内存操作
2、速度革命的 神兵:高性能数据结构
- 3、速度革命的根本1: 单线程 无锁架构
- 4、速度革命的根本2:事件驱动EDA架构 + 多路IO复用模型
- 5、速度 革命 的加速 :Redis 异步的日志架构
但是:
恶虎还怕群狼 ,双拳难敌四手
恶虎还怕群狼 ,双拳难敌四手
恶虎还怕群狼 ,双拳难敌四手
要实现 10Wqps-100Wqps , 靠单个节点是不够的, 还是要兄弟们来扛,所以,接下来就是:
- 6:速度 革命 的腾飞 :单体-> 集群,实现 10Wqps-100Wqps
关于 Redis Cluster 集群架构的深度文章, 请参考 尼恩的下面的文章:
Redis Cluster 集群架构:
Redis Cluster通过分片技术将数据分散到多个节点,同时提供高可用性和横向扩展能力。
- 分片:数据被分散到多个节点,每个节点负责一部分数据。
- 高可用:每个分片有多个副本,主节点故障时自动切换到副本。
- 横向扩展 : 增加节点即可扩展集群容量 。
Redis Cluster 就像一个分小组干活的团队,把海量数据和请求拆成多个“小组”(分片),每个小组独立处理自己的任务,最终合力扛住超高并发。
1、分片:拆数据,压力均摊
- 数据分到多个节点:
假设你有 1 亿条数据,Redis Cluster 会按规则(比如哈希取模)把数据拆成 16384 个槽位(slot),再把这些槽位分配到多个主节点上。
举例:3 个主节点,每人管 5000 多个槽位,数据分散存储,每个节点只处理自己的那部分请求。
- 并发能力翻倍:
每个分片独立处理读写,假设单节点能扛 2W QPS,3 个分片就能扛 6W QPS,分片越多,总 QPS 越高35。
2、高可用:主节点挂了,小弟顶上
- 主从备份:每个主节点配 1~N 个从节点(副本),主节点负责写,从节点同步数据。
- 自动切换:主节点宕机时,Cluster 自动选一个从节点升级成新主节点,服务不中断。
3、 横向扩展:加机器就能提升容量和性能
加节点:当数据或请求量暴涨时,直接往集群里加新机器。
自动迁移槽位:新节点加入后,Cluster 会自动从现有节点转移一部分槽位到新节点,请求和数据自动分摊,无需手动干预。
4、为什么能扛住 10W+ QPS 到100Wqps?
- 分片并发:10 个分片 * 单节点 1W QPS = 10W QPS,性能随分片数量线性增长。
- 就近访问:客户端直连对应分片的主节点,减少网络跳转3。
- 无中心瓶颈:无代理层,节点间通过 Gossip 协议通信,避免单点瓶颈35。
5、举个实际例子(电商秒杀场景)
- 数据分片:1 亿商品库存,按商品ID分到 10 个 Redis 节点,每个节点存 1000W 商品数据。
- 请求分摊:10 个节点同时处理扣库存请求,总 QPS 轻松突破 10W。
- 高可用兜底:某个节点宕机时,从节点立刻顶替,秒杀活动不受影响。
Redis 集群 横向扩展操作步骤
- 加机器:部署新 Redis 实例,加入集群。
- 迁移数据:用
CLUSTER ADDSLOTS
分配槽位,数据自动从老节点迁移到新节点。 - 客户端自动感知:客户端通过
CLUSTER SLOTS
获取最新分片信息,请求直接发给新节点。
Redis Cluster 集群架构总结
Redis Cluster 通过分片拆数据、主从保高可用、加机器扩容量,实现性能和容量的双扩展。
就像把一个大仓库分成多个小库房,每个库房有自己的管理员和备份员,人多力量大,最终扛住 10W+ QPS 的暴击!
Redis为什么快(10Wqps):全面总结
Redis速度革命核心要点总结
1、速度 革命 的起点 : 纯内存操作
2、速度革命的 利器神兵:高性能数据结构
3、速度革命的根本1: 单线程 无锁架构
4、速度革命的根本2:事件驱动EDA架构 + 多路IO复用模型
5、速度 革命 的加速 :Redis 异步的日志架构
6:速度 革命 的腾飞 :单体-> 集群,实现 10Wqps-100Wqps
1、速度革命的起点:纯内存操作
Redis所有数据操作均在内存中完成,避免传统磁盘数据库的I/O瓶颈。内存操作速度可达纳秒级,相比磁盘访问(毫秒级)提升5个数量级,奠定性能基础 。
Redis将所有数据存储在内存中,直接跳过了传统数据库依赖的磁盘I/O,这带来了数量级的性能提升。
内存读写速度可达数十纳秒级别(纳秒级),固态硬盘也需微秒级(微秒级),而机械硬盘的随机读写延迟高达数 毫秒(毫秒级)。
这种存储方式使Redis在高频读写场景中,如每秒十万次以上的操作,依然能保持微秒级响应时间,相比磁盘数据库性能提升可达1000倍以上。
2、速度革命的利器神兵:高性能数据结构
内置优化数据结构:字符串、跳跃表、压缩列表、哈希表等针对不同场景设计(如压缩列表减少短数据内存占用,跳跃表实现快速范围查询)
Redis内置的8种数据结构经过精细优化:
- 哈希表采用 双表 渐进式rehash,平均时间复杂度保持O(1)
- 压缩列表(ziplist)和整数集合(intset)将小数据存储密度提升3-5倍
- 跳跃表(skiplist)在有序集合操作中,提供平均O(log n)的查询性能,空间 开销 比平衡树低40%
这些结构使Redis在相同内存下存储能力提升2-3倍,操作性能提升10-20倍。
3、速度革命的根本1:单线程无锁架构
Redis采用单线程模型处理所有客户端请求,消除了多线程环境下的上下文切换开销(每次切换约需微秒级)和锁竞争。
这种设计使CPU缓存命中率保持在90%以上,指令流水线几乎无阻断。在高并发场景下,单线程模型的吞吐量反而超过多线程方案,因为避免了线程间同步的开销,使Redis能稳定处理每秒数十万次请求。
- 单线程模型避免多线程上下文切换(每次切换约1μs)和锁竞争开销
- 原子性操作天然避免并发安全问题,指令执行无阻塞
- CPU缓存命中率更高(L1缓存访问仅0.5ns)
4. 速度革命的根本2:事件驱动EDA架构 + 多路I/O复用模型
通过epoll/kqueue等IO多路复用技术,Redis实现了单线程同时处理数万个客户端连接。这种机制将网络事件(读、写、连接)转化为非阻塞事件,CPU利用率高达95%。在实际测试中,单台Redis实例可稳定处理超过100,000个并发连接,每个连接的处理开销仅为纳秒级,使Redis成为高并发场景的首选解决方案。
- Reactor模式实现单线程处理20万+连接
- epoll/kqueue系统调用实现μs级事件触发,对比传统阻塞IO模型吞吐量提升100倍
- 网络包解析与命令执行流水线化,事件分派延迟<1ms
5. 速度革命的加速:异步日志架构
Redis的RDB和AOF进行异步持久化,并且优化了策略。提供了灵活的性能-可靠性权衡:
- 持久化采用RDB快照(fork子进程写入)与AOF日志(后台线程fsync)分离
- 日志写入延迟从同步模式的10ms级降低至异步模式100μs级
- 写操作缓冲区实现批量刷盘,吞吐量提升5-10倍
- RDB每秒级快照,恢复速度达100MB/s
- AOF追加写入支持fsync可配置,每秒持久化模式下性能损耗<5% 这种设计使Redis在保证数据安全的同时,将持久化对性能的影响控制在可接受范围内,主从同步带宽占用<10%,实现了高性能与可靠性的完美平衡。
6. 速度革命的腾飞:单体->集群
- 分片集群实现水平扩展,单集群支持100W+ QPS(对比单体10W QPS)
- Proxy层实现无感扩容,数据迁移延迟<50ms
- 多副本架构保障高可用,故障切换时间<200ms4
Redis使用内存存储数据也有一些缺点和限制:
当然,Redis使用内存存储数据也有一些缺点和限制:
内存限制:内存是非常昂贵的,容量通常只有几十GB或几百GB,而磁盘目前都是TB起步。所以我们通常只会把少量的、经常访问的数据存储在内存中。
数据类型限制:Redis不支持复杂的数据结构,比如用户对象,通常只能序列化成字符串后再存储,查询的时候再把字符串反序列化成用户对象。
数据备份问题:在服务器重启或崩溃时,存储的内存中的数据可能会丢失。通常采用持久化技术将数据保存到磁盘上,同时定期备份数据以防止数据丢失。
不放过面试官,来一个最后的彩蛋
最后, 给面试官加一句 AP 和CP 是不可调和的。
Redis 好是好, 但是是AP型的。
如果一定要 保障 可靠性, 需要设计好 数据补偿方案, 作为补充。
如何设计一个 牛逼的 数据补偿 方案,请参见尼恩的 深度 文章:
美团2面:亿级流量缓存,操作失败 如何设计 补偿?如何保证Redis与MySQL的一致性?
120分殿堂答案 (塔尖级):
尼恩提示,讲完 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构 ,可以拿 120分了, 大厂offer, 这会就到手了。
终于 逆天改命啦。
遇到问题,找老架构师取经
借助此文,尼恩给解密了一个高薪的 秘诀,大家可以 放手一试。保证 屡试不爽,涨薪 100%-200%。
后面,尼恩java面试宝典回录成视频, 给大家打造一套进大厂的塔尖视频。
通过这个问题的深度回答,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。
很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。
遇到职业难题,找老架构取经, 可以省去太多的折腾,省去太多的弯路。
尼恩指导了大量的小伙伴上岸,前段时间,刚指导一个40岁+被裁小伙伴,拿到了一个年薪100W的offer。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。