阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构

本文的 原始地址 ,传送门

下面的正文,如果 出现图片 缺少, 请去原文查找:

本文的 原始地址 ,传送门

说在前面

在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的 核心模块架构

图1-10

  • 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 上层 每一种 集合类型,都有两种底层实现结构,

img

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中的双端链表是一种经过优化的数据结构,用于存储有序的元素集合。

双端链表 的 链表整体结构

除了节点外,双端链表还有描述链表整体属性的元数据,也就是 链表整体结构

链表整体结构 ,是 描述链表整体属性的元数据 ,主要包括:

  1. 头节点(head node)‌:链表的第一个节点,通常用于快速定位链表的起始位置。头节点的prev指针为NULL
  2. 尾节点(tail node)‌:链表的最后一个节点,通常用于快速定位链表的结束位置。尾节点的next指针为NULL
  3. 长度(len)‌:记录链表的当前长度。每当链表中添加或删除节点时,这个值会相应更新,以反映链表的当前长度。

len字段,用于记录链表的当前长度。这个字段的优势在于:‌快速获取长度

获取链表长度时,不需要遍历整个链表,直接读取len字段即可,时间复杂度为O(1)。

头节点与尾节点的作用

  • 头节点‌:作为链表的入口,方便进行头部插入和删除操作。
  • 尾节点‌:作为链表的出口,方便进行尾部插入和删除操作。

双端链表 节点结构

节点是双端链表的基本构建单元,每个节点包含以下关键部分:

  1. prev指针‌:指向前一个节点的指针。如果当前节点是链表的头节点,则此指针为NULL
  2. next指针‌:指向后一个节点的指针。如果当前节点是链表的尾节点,则此指针为NULL
  3. value数据域‌:用于存储链表节点的数据元素。这个数据元素可以是任意类型,因此使用void *类型来表示,使得双端链表具有通用性。

它具有双向链接的特性,每个节点都包含指向前一个节点和后一个节点的指针。

typedef struct list {
    listNode *head;  // 头节点指针
    listNode *tail;  // 尾节点指针
    unsigned long len;  // 链表长度
    // 其他字段...
} list;
AI 代码解读

从结构中可以看出元数据中还有两个特殊的节点:头节点(head node)和尾节点(tail node),它们分别位于链表的头部和尾部。

通过这些属性,双端链表节点构成了链表的基本组成部分,它们通过prevnext指针连接在一起,形成了双向链接的链表结构。

双端链表的 优势

通过头节点和尾节点,可以方便地对双端链表进行头部插入、尾部插入、头部删除、尾部删除等操作,从而实现了对双端链表的高效操作。

  • 头部插入‌:在链表的头部添加新节点。
  • 尾部插入‌:在链表的尾部添加新节点。
  • 头部删除‌:从链表的头部删除节点。
  • 尾部删除‌:从链表的尾部删除节点。

这些操作使得双端链表在处理有序元素集合时非常高效。

高性能 神兵四: 渐进式扩容 双 哈希结构(/字典)

Redis 的底层数据结构有六种,简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组

上层的 Hash、Set 两种 对象类型 ,都 用到了 下层的 哈希表(/字典)

在Redis中,字典(dictionary)是一种用于存储键值对数据的数据结构,也称为哈希表(hash table)。

字典是Redis中最常用的数据结构之一,具有快速查找、动态调整大小、哈希冲突处理、迭代器支持等特点,适用于各种数据存储和操作需求,实现键值对存储和快速查找。

在Redis中hashtable就是字典dict

通过源码,可以看到dict是这样定义的:

哈希表的实现

Redis 的哈希表实现了一个“分离链表法”的哈希表结构,具体如下:

  • 槽位数组(buckets):一个数组,数组的每个元素是一个链表的头节点,链表用于存储发生哈希冲突的键值对。每个数组元素是一个链表的头节点。
  • 链表节点:每个节点包含一个键值对,以及指向下一个节点的指针。
  • 哈希函数:用于将键映射到哈希表数组的索引位置。Redis 使用了一个高效的哈希函数(如 MurmurHash)来计算键的哈希值,并将哈希值映射到哈希表数组的索引位置。
  • 链表(分离链表法):当多个键值对映射到同一个哈希表数组索引时,这些键值对会以链表的形式存储,链表的每个节点包含键值对。

Redis 中 hashtable 具体由 dictht 来实现。

dictht 中,table 结构是一个 dictEntry 类型的数组,而 dictEntry 是一个单向链表中的节点,代表的是 hashtable 中的一条记录,包含了该条记录的 keyvalue 以及指向下一个节点(下一条记录)的指针(hash 碰撞)。

字典以键值对的形式存储数据,每个键都与一个值相关联。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

dictEntry 中的 keyvoid* 类型,意味着可以存储任意数据类型。

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)是一种基于链表的数据结构,它利用多级索引来加速查找操作,类似于平衡树,但实现起来更加简单,具有较好的平均查找性能。

跳表是一种采用了用空间换时间思想的数据结构。

跳表会随机地将一些节点提升到更高的层次,以创建一种逐层的数据结构,以提高操作的速度。

跳表 相关的面试题,也是大厂非常喜爱的面试题, 关于跳表的大厂面试题:

字节面试:手写一个跳表。 我直接跪了。。

字节面试:Mysql为什么用B+树,不用跳表?

跳表的结构

跳表的做法就是给链表做索引,而且是分层索引,

跳表通过维护多级索引,每个级别的索引都是原始链表的子集,用于快速定位元素。

每个节点在不同级别的索引中都有一个指针,通过这些指针,可以在不同级别上进行快速查找,从而提高了查找效率。

单层跳表

单层跳表, 可以退化到一个链表

查找的时间复杂度是 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* keyvoid* 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用哈希槽,不用一致性哈希?

美团面试:Redis怎么做高可用、高并发架构?

京东面试:Redis主从切换,锁失效 怎么办?

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 集群 横向扩展操作步骤‌

  1. 加机器:部署新 Redis 实例,加入集群。
  2. 迁移数据:用 CLUSTER ADDSLOTS 分配槽位,数据自动从老节点迁移到新节点。
  3. 客户端自动感知:客户端通过 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
  • 多副本架构保障高可用,故障切换时间<200ms‌4

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自由” 。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
打赏
0
1
1
0
210
分享
相关文章
阿里二面:10亿级分库分表,如何丝滑扩容、如何双写灰度?阿里P8方案+ 架构图,看完直接上offer!
阿里二面:10亿级分库分表,如何丝滑扩容、如何双写灰度?阿里P8方案+ 架构图,看完直接上offer!
阿里二面:10亿级分库分表,如何丝滑扩容、如何双写灰度?阿里P8方案+ 架构图,看完直接上offer!
阿里开源多模态全能王 Qwen2.5-Omni:创新Thinker-Talker架构,全面超越Gemini-1.5-Pro等竞品
阿里开源Qwen2.5-Omni多模态大模型,支持文本、图像、音频和视频输入,具备实时语音合成与流式响应能力,在OmniBench等基准测试中全面超越Gemini-1.5-Pro等竞品,提供免费商用授权。
570 7
阿里开源多模态全能王 Qwen2.5-Omni:创新Thinker-Talker架构,全面超越Gemini-1.5-Pro等竞品
面试中的难题:线程异步执行后如何共享数据?
本文通过一个面试故事,详细讲解了Java中线程内部开启异步操作后如何安全地共享数据。介绍了异步操作的基本概念及常见实现方式(如CompletableFuture、ExecutorService),并重点探讨了volatile关键字、CountDownLatch和CompletableFuture等工具在线程间数据共享中的应用,帮助读者理解线程安全和内存可见性问题。通过这些方法,可以有效解决多线程环境下的数据共享挑战,提升编程效率和代码健壮性。
135 6
【赵渝强老师】Redis的慢查询日志
Redis慢查询日志用于记录执行时间超过预设阈值的命令,帮助开发和运维人员定位性能问题。每条慢查询日志包含标识ID、发生时间戳、命令耗时及详细信息。配置参数包括`slowlog-max-len`(默认128)和`slowlog-log-slower-than`(默认10000微秒)。实战中可通过`slowlog get`获取日志、`slowlog len`查看长度、`slowlog reset`重置日志。建议线上环境将`slowlog-max-len`设为1000以上,并根据并发量调整`slowlog-log-slower-than`。需要注意的是,慢查询只记录命令执行时间。
239 5
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
1775 31
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
什么是Apache日志?为什么Apache日志分析很重要?
Apache是全球广泛使用的Web服务器软件,支持超过30%的活跃网站。它通过接收和处理HTTP请求,与后端服务器通信,返回响应并记录日志,确保网页请求的快速准确处理。Apache日志分为访问日志和错误日志,对提升用户体验、保障安全及优化性能至关重要。EventLog Analyzer等工具可有效管理和分析这些日志,增强Web服务的安全性和可靠性。
131 9
MySQL日志详解——日志分类、二进制日志bin log、回滚日志undo log、重做日志redo log
MySQL日志详解——日志分类、二进制日志bin log、回滚日志undo log、重做日志redo log、原理、写入过程;binlog与redolog区别、update语句的执行流程、两阶段提交、主从复制、三种日志的使用场景;查询日志、慢查询日志、错误日志等其他几类日志
211 35
MySQL日志详解——日志分类、二进制日志bin log、回滚日志undo log、重做日志redo log
Tomcat log日志解析
理解和解析Tomcat日志文件对于诊断和解决Web应用中的问题至关重要。通过分析 `catalina.out`、`localhost.log`、`localhost_access_log.*.txt`、`manager.log`和 `host-manager.log`等日志文件,可以快速定位和解决问题,确保Tomcat服务器的稳定运行。掌握这些日志解析技巧,可以显著提高运维和开发效率。
126 13

热门文章

最新文章