当前位置: 首页 > ops >正文

面试官常问:Redis 为什么快?这篇回答满分!

在百万 QPS 的微服务系统中,Redis 几乎是无可替代的高性能组件。无论是做缓存、限流、排行榜还是唯一用户统计,它总能以微秒级响应速度完成任务。

但 Redis 为何如此之快?它到底快在哪里?如果你只知道回答“因为Redis是基于内存实现的”,那你真的要认真看看这篇文章了。

本文从存储结构、线程模型、持久化机制到底层数据结构实现,系统地揭示 Redis 性能背后的秘密,相信你看完一定会有收获。

文章字数:6000字

阅读时长:15mins

文章目录

    • 1. Redis基于内存的存储数据
    • 2. Redis的线程协作模型
      • 2.1 Redis是单线程的吗
      • 2.2 I/O多线程机制
      • 2.3 后台线程
      • 2.4 总结
    • 3. Redis的异步数据持久化
      • 3.1 RDB持久化
      • 3.2 AOF持久化
      • 3.3 RDB + AOF混合持久化
    • 4.Redis的高效数据结构实现
      • 4.1 前置数据结构
      • 4.2 Redis数据类型及实现

1. Redis基于内存的存储数据

最直接也是最根本的原因在于:Redis 是一个内存数据库。与传统关系型数据库将数据存储在磁盘不同,Redis 将所有数据存储于内存中。内存访问的速度远远高于磁盘,通常在数百纳秒级别,而磁盘 I/O 即使采用 SSD 也存在毫秒级延迟。

那么为什么不直接用本地缓存呢,也就是直接在应用服务器或客户端上存储数据副本的缓存机制,从纯粹性能角度来看,本地缓存相对于Redis更快:

  • 本地缓存通常以键值对的形式存储数据,查找、更新操作时间复杂度 O(1)。
  • 数据就保存在当前JVM进程中,无任何网络通信延迟。
  • 无需序列化/反序列化过程。

然而性能并不是唯一要考虑的维度,尽管本地缓存更快,但Redis 提供了本地缓存难以具备的多种能力。

  • 跨节点共享数据,统一缓存中心:Redis 作为远程缓存,服务于整个集群,是共享缓存中心。本地缓存只能服务于当前节点,每个服务节点都维护一份缓存副本,带来维护与一致性问题。
  • 丰富的数据结构与操作:Redis 支持 String、List、Hash、Set、ZSet、Bitmap等结构,提供多种场景应用,如范围查询、排行榜、去重统计、布隆过滤器等。而本地缓存多数仅支持简单的 KV 结构,扩展能力受限。
  • 数据容量更大,淘汰策略灵活:Redis 存储于专用服务中,可配置数10GB 内存并通过淘汰策略(LRU/LFU)自动清理低频数据。而本地缓存受限于 JVM Heap 大小,过多数据易造成 OOM,
  • 支持高可用与持久化:Redis 支持主从复制、哨兵模式、集群分片,支持持久化(RDB、AOF),可以用于持久缓存、降级缓存等场景。而本地缓存断电即失,服务重启则缓存失效。
  • 适用于微服务架构:Redis 具备“远程访问能力”,适用于微服务之间共享缓存数据。而本地缓存更适合“单体服务”或副本隔离不敏感的业务场景。

在实际工程中,两种缓存并非互斥,而是结合使用,构建“多级缓存架构”:

  • 一级缓存:本地缓存,优先从内存中快速命中,适用于极端性能要求、数据量小、节点间不需共享的场景,比如用户Session、本地热数据。
  • 二级缓存:Redis,作为统一缓存中心和兜底,适用于跨节点共享、数据结构复杂或缓存策略多样的场景,比如商品详情、用户画像、热点推荐。

2. Redis的线程协作模型

在并发系统中多线程几乎是性能优化的代名词,你是否有看到过说“Redis是单线程模型”的文章,这听起来有些不可思议,Redis真的是单线程模型吗?别着急,接着往下看。

2.1 Redis是单线程的吗

Redis整体不是单线程的,所说的Redis单线程是指所有命令执行操作(包括读取、写入、过期检查等)都由主线程串行处理,不管有多少条连接去操作redis的数据,redis对命令的处理都在一个线程完成。

这归因于Redis最基本的特性——所有核心数据结构均为内存操作,访问速度极快,主线程串行执行命令可以避免多线程并发带来的复杂性(加锁、死锁、竞态条件)。

它之所以能够处理高并发连接而不阻塞,关键在于其高效的 事件循环机制,这个机制本质上是 Reactor 模式 的一种实现,它通过 I/O 多路复用技术(如 epoll、kqueue、select)来统一管理多个客户端连接的读写事件。

epoll是Linux内核为处理大批量文件描述符而改进的I/O多路复用机制,高效管理大量的文件描述符(FDs)并响应各类I/O事件。它通过事件驱动的方式,监听多个文件描述符上的事件(如可读、可写、错误等),并在事件发生时通知应用程序进行处理。

整个事件循环的工作流程大致可以划分为以下几个阶段:

  • 监听:主线程在空闲时会调用操作系统底层的 I/O 多路复用接口,持续监听一组文件描述符上是否发生以下事件:客户端连接、客户端发来的读/写请求、定时任务(键过期检查)、文件事件(如持久化操作)。
  • 唤醒:当有一个或多个事件发生时,操作系统会通知 Redis,Redis 主线程立即被唤醒,进入事件处理流程。
  • 分发:Redis 会遍历这批就绪事件,并根据事件的类型将它们分发到对应的处理函数,涉及到内存管理和持久化的操作都会交给后台线程处理。
  • 执行:事件被分发后,Redis 会依次执行每个事件的处理逻辑(接收命令、命令解析、数据操作、写回响应),执行过程是串行的,避免了加锁与并发控制的问题。
  • 处理完一轮所有事件后,Redis 会清理状态,释放临时资源,再次进入监听状态,继续等待下一批事件。

从上面的过程可以总结出,Redis的主线程负责:接收客户端命令→解析命令→执行命令:操作内存数据结构→写回响应结果。

2.2 I/O多线程机制

随着客户端数量不断上升,网络通信开始成为 Redis 的性能瓶颈。为了解决这一问题,从 Redis 6.0 起引入了 I/O 多线程机制,用于并发处理网络请求的读写操作。

原来Redis主线程职责:“接收客户端命令→解析命令→执行命令:操作内存数据结构→写回响应结果。”中的客户端命令→解析命令和写回响应结果交由I/O多线程来完成。

总结:

  • 读阶段(读 socket + 协议解析):由多个 I/O 线程并行处理;
  • 执行阶段(命令执行、数据访问):仍由主线程串行完成。
  • 写阶段(将响应写回 socket):也由 I/O 线程并行执行;

I/O多线程机制充分利用了多核 CPU 的处理能力,使 Redis 在连接数庞大、请求密集的情况下依然保持高吞吐。

启用方式:

io-threads 4               # 开启 4 个 I/O 线程
io-threads-do-reads yes    # 开启 I/O 多线程读

2.3 后台线程

Redis中存在一些无法避免的耗时操作(阻塞IO或者CPU运算数据的时间比较长),比如:持久化写盘、文件关闭、内存回收。

如果这些操作由主线程直接执行,必然会引起卡顿。因此 Redis 提供了专用的后台线程(BIO thread)来完成这些工作, Redis 中的后台线程包括:

  • bio_aof_fsync:将 AOF 日志刷入磁盘(执行 fsync 等操作)。
  • bio_close_file :异步关闭文件,释放文件描述符。
  • bio_lazy_free:异步释放大对象的内存,避免长时间 free 阻塞主线程。
  • jemalloc_bg_thd:内存池管理、碎片整理

此外,Redis 在进行 RDB 快照或 AOF 重写时,会 fork 子进程来执行磁盘写操作,这些任务同样是异步的,主线程可以继续正常提供服务,进一步确保服务不中断。

2.4 总结

总的来说,Redis的线程协作由主线程、I/O多线程、后台线程三部分组成。

  • 命令执行过程由主线程来完成。
  • 网络请求的接受、解析、写回由I/O多线程来完成。
  • 文件管理、内存管理由后台线程来完成。

3. Redis的异步数据持久化

Redis保证数据库高性能读取的基础上,也要保障数据在系统异常时不丢失,也就是数据持久化,而Redis 的所有写入操作并不是直接写入磁盘,而是**异步地将数据持久化,从而避免了主线程阻塞,最大限度提升响应速度。

Redis 提供了两种主要的持久化方式:RDB(快照持久化)和 AOF(追加日志持久化),二者都可以配置为异步进行,即在主线程外通过子进程或后台线程完成持久化操作。

3.1 RDB持久化

RDB 全称为 Redis DataBase 文件,它的本质是一种“数据库的快照”,即在某一时刻将 Redis 中全部键值数据生成快照,保存为一个 .rdb 二进制文件(通常是 dump.rdb),存储在磁盘上。常用命令有两个:

  • SAVE:阻塞主线程,立即生成 RDB 文件。
  • BGSAVE在后台 fork 子进程生成快照,主线程继续处理请求。

当执行 BGSAVE 时:

  • Redis 会 fork 出一个子进程。
  • 子进程负责遍历数据库中所有键值对,生成一个独立的 RDB 文件。
  • 生成完成后替换旧文件。

RDB持久化是全量快照,执行频率不能太频繁。如果设置成5分钟保存一份快照,这时Redis宕机,最多可能丢失5 分钟数据。

3.2 AOF持久化

AOF 全称为 Append Only File,AOF 会以日志追加方式记录 Redis 所执行的写命令(如 SETINCR 等),保存到文件中(通常是 appendonly.aof)。Redis 重启时可通过重新执行 AOF 文件中的命令恢复数据状态。

AOF 使用了缓冲区 + 异步刷新机制,写入过程和MySQL的日志持久化过程有“异曲同工”之妙:

  • 主线程写完命令后,会将其转为 RESP 文本协议格式,写入aof_buf(主线程缓冲区)。
  • 后台线程首先将 aof_buf 写入操作系统的页缓存,此时还未真正刷到物理磁盘。
  • 只有在调用 fsync 后,操作系统才会将数据从页缓存 flush 到磁盘,这一步非常耗时,因此 Redis 提供了异步选项来控制刷盘策略。

可以通过 appendfsync 参数配置写入策略:

  • always:每个命令都同步写入磁盘(最安全但最慢)
  • everysec(默认):每秒异步 fsync 一次,权衡性能与持久性
  • no:完全依赖操作系统调度(性能高,但不可靠)

为避免 AOF 文件过大,当 AOF 文件大于 64M 时,Redis 会在后台异步进行 AOF 文件重写(rewrite)

  • 重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。

    重写机制的妙处在于,尽管某个键值对被多条写命令反复修改,最终也只需要根据这个「键值对」当前的最新状态,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这样就减少了 AOF 文件中的命令数量。

  • 重写过程由子进程 bgrewriteaof完成,写期间仍接收命令并缓存在临时缓冲区。

3.3 RDB + AOF混合持久化

对比RDB和AOF的特性,可以总结出它们的优缺点:

RDBAOF
数据丢失风险全量快照,执行频率不能太频繁。设置成5分钟保存一份快照,最多可能丢失5 分钟数据。以秒级的方式记录操作命令,所以丢失的数据就相对更少。
启动速度RDB 文件是压缩的二进制文件,加载时速度快。恢复数据时需要重新执行所有写命令,启动时间比 RDB 长。
磁盘占用RDB 文件较小,适合备份和数据迁移。通常比 RDB 文件大,尤其在高频写操作的场景下。
可重放通过重放 AOF 中的命令,可以恢复数据到任意时间点。
其他重写可能导致产生碎片

上述特性中最重要的就是数据丢失风险和启动速度这两项了,那么应该怎么把RDB启动速度快和AOF数据丢失风险小的优点结合起来呢?

从 Redis 4.0 起,支持“混合持久化模式”,这个过程涉及两个阶段:

  • 生成RDB快照:Redis fork出子进程,子进程遍历主线程共享的内存数据,并将当前内存快照以RDB二进制格式写入到新的AOF临时文件(前半段)。
  • 记录增量操作:在子进程写入RDB快照的同时,主线程仍在处理客户端请求,所有写命令会同步写入AOF缓冲区,当子进程写完RDB部分后,Redis会将重写缓冲区中的命令追加到AOF临时文件的末尾(后半段)。
  • 最后使用新的混合 AOF 文件原子替换旧文件

当Redis重启并加载该混合AOF文件时,首先解析前半段RDB 部分,恢复数据库的主干数据结构,然后 replay 后半段 AOF 命令,恢复最后阶段的增量更新,这样既保证了快速加载,又保留了近实时持久化能力

redis.conf 中开启混合持久化模式:

aof-use-rdb-preamble yes

4.Redis的高效数据结构实现

Redis是一个键值存储系统,它提供了多种基础和复合的数据类型,每一种数据类型都对应不同的数据结构实现,它们在设计上都优先保证查询、更新、插入、删除操作的时间复杂度控制在 O(1) 或 O(logN),以此支撑其高吞吐的性能需求。

4.1 前置数据结构

在讲Redis数据类型之前,先来看看实现它们所依赖的五种数据结构:SDS、ziplist、intset、skiplist、hashtable。

  1. SDS

    SDS(Simple Dynamic String)是简单动态字符串。本质上是一个结构体,它内部记录了字符串的长度、分配容量和实际存储字符内容。所有String类型,Hash、List、Set中的字段和值都可以用SDS实现。

    struct sdshdr {int len;       // 已使用长度int alloc;     // 已分配总长度(不含 '\0')char buf[];    // 实际存储字符内容
    };
    
    • 获取长度时间复杂度O(1):不需要 strlen 遍历。
    • 支持预分配内存的机制,在字符串扩容时,不是只分配刚好所需的空间,而是按比例增长容量,从而减少频繁 realloc 的开销。
    • SDS 是二进制安全的,允许字符串中包含 \0 字节,可以用它来保存压缩数据、图片等非文本内容。
  2. ziplist

    ziplist是一种连续内存块结构,数据紧密排列、无指针跳转。在存储小型集合(如字段较少的 Hash、元素较少的 List或ZSet)时,Redis 并不直接使用链表或哈希表,而是优先使用 ziplist(压缩列表)。

    ziplist结构组成如下:

    • zlbytes:ziplist 总字节数;
    • zltail:最后一个 entry 的偏移量,便于快速从尾部插入;
    • zllen:元素个数;
    • entry:存储实际数据(可能是整数或字符串);
    • zlend:结尾标志 0xFF

    Redis 对 ziplist 的使用是自动的,当 Hash、List、ZSet 中的元素个数或元素长度超过配置的阈值时(默认超过 512 个元素,或任意元素长度超过 64 字节),Redis 会自动将 ziplist 升级为更强大的数据结构(如 hashtable 或 skiplist)。

  3. inset

    当 Redis 的Set集合只包含整数值时,为了节省内存和提升效率,会使用 intset(整数集合)代替通用哈希表,intset结构体定义如下:

    typedef struct intset {uint32_t encoding;   // 每个元素的编码方式:16/32/64 位uint32_t length;     // 当前元素个数int8_t contents[];   // 存储数据(升序排列)
    } intset;
    
    • intset 是类型感知的,根据存储的整数范围动态选择使用 16 位、32 位或 64 位编码,从而最大限度压缩内存。
    • 内部所有元素按照升序排列,支持快速查找和插入。
  4. skiplist

    跳表是一种查找结构,可以作为key或者key/value的查找模型,它是一种多层索引链表,在有序链表的基础上发展而来,每个节点根据随机概率生成多个“层级指针”,顶层索引稀疏,底层索引密集。

    跳表在平均情况下可以在 O(logN) 的时间复杂度内完成插入、删除和查找操作。具体结构可以参考下图。

    详细设计思路可以看这篇文章:https://blog.csdn.net/2301_76269963/article/details/136194737。

  5. hashtable

    hashtable我们都比较了解,通常用来实现key-value映射,这里重点讲一下它的rehash过程(扩容过程)。

    当负载因子超过阈值,开始rehash,新哈希表ht[1]的容量通常是旧哈希表ht[0]容量的两倍大小。

    Redis 采用的是渐进式 rehash 策略,而不是一次性将所有键值对从旧表移动到新表。具体实施如下:

    • 设置一个 rehash 索引计数器 rehashidx,初始值设为 0。

    • 在后续执行键值对的添加、删除、查找或更新操作时,除了正常执行相应操作之外,还会检查 rehashidx 是否小于旧哈希表 ht[0] 的大小。

      查找一个 key 的值,先会在 ht[0] 里面进行查找,如果没找到,就会继续到 ht[1] 里面进行找到。

    • 若满足条件,则将 ht[0] 在 rehashidx 索引处的所有键值对 rehash 到 ht[1] 中,然后递增 rehashidx。

    • 如此反复,每次操作都会推进 rehash 进程一点点,直至 rehashidx 达到 ht[0] 的大小,表明 rehash 完成。

    当 rehash 过程完成后,Redis 会将旧的哈希表 ht[0] 替换掉,并将新表 ht[1] 设为新的 ht[0]。

    渐进式 rehash 可以有效地分散 rehash 所带来的内存操作的压力,避免在某一瞬间阻塞服务的可能性。

4.2 Redis数据类型及实现

Redis的基础数据类型包括String字符串、Hash散列、List列表、Set集合、Zset有序集合,其它数据类型包括Bitmap、HyperLogLog、GEO等。

  1. String字符串:SDS

    • String 可以存储文本、二进制数据、整数或浮点数等,功能涵盖了计数器、缓存数据、分布式锁等场景。
    • String字符串基于SDS结构实现,内部记录了字符串长度,避免每次遍历计算。
  2. List列表:quicklist:ziplist + 双向链表

    • List 是一个有序的链表结构,支持从两端插入和弹出元素,常用于消息队列、任务列表等场景。

    • List 不再使用原始的双向链表,而是用 quicklist 替代,quicklist 是 ziplist + 双向链表 的混合结构,每个 quicklist node 是一个压缩列表(ziplist),节省内存,并通过双向链表串联,支持高效插入与删除,还可以配置 ziplist 的最大长度与压缩深度。

  3. Hash散列:ziplist → hashtable

    • Hash 是用于存储对象属性或结构化数据(如用户资料)的键值对集合,每个字段和对应的值都以字符串形式存储。

    • 数据结构实现由ziplist和hashtable组成。

      ①字段少、数据小(默认字段数 < 512 且每个值 < 64 字节)时采用ziplist

      ②字段多或任一字段较大时自动转为hashtable,查找时间复杂度为 O(1)。

  4. Set无序集合:intset → hashtable

    • Set 是一个不允许重复元素的无序集合,常用于用户标签、权限集合、抽奖池等。

    • 数据结构实现由intset 与 hashtable组成。

      ①当所有元素是整数且数量较少时使用intset结构(默认 < 512 个),它是一种紧凑数组结构。

      ②元素多或出现非整数时自动转为hashtable,查找和插入时间复杂度O(1) 。

  5. ZSet有序集合:ziplist → skiplist + hashtable

    • Sorted Set 是每个元素带有一个 score(分值)的集合,按照 score 进行排序,常用于排行榜、延时队列、区间查询等。

    • 数据结构实现由hashtable和skiplist、ziplist组成。

      ①hashtable存储 member → score 映射,支持快速判断元素是否存在。跳表有序存储元素,支持按 score 查询、范围查找与排名。

      ②当元素数量不多时(默认当元素数量小于128,每个元素都小于64时),hashtable和skipList的优势不明显,而且更耗内存,ZSet会采用ziplist结构。

  6. Bitmap:String

    • Bitmap 是通过位操作压缩布尔状态的结构,每个 bit 代表一个元素的状态,常用于签到、唯一用户统计、布尔标记等。
    • Bitmap基于String实现,String 是一个二进制安全的字节数组(SDS 实现),Bitmap 就是以 bit 为最小单位,在这个字节数组上进行按位访问、设置、统计的过程。
  7. HyperLogLog:String

    • HyperLogLog是一种算法,redis将它用于统计海量数据中不重复元素的数量(基数),可用于 UV 去重、唯一对象估算。

    • 基于概率算法实现,使用多个寄存器记录哈希值前缀中 0 的最大个数,空间恒定(约 12 KB),误差控制在 0.81% 左右,适合大数据量去重估算,不能获取具体元素集合。

      详细原理可以看这篇文章:https://juejin.cn/post/6844903785744056333#heading-1。

  8. Geo:ZSet

    • 用于存储经纬度坐标,可进行地理位置存储、距离计算、范围查询等。
    • 基于 ZSet 存储,通过 GeoHash 编码将二维经纬度编码成 一维score,通过跳表实现排序与范围检索。

Redis 的快,不止因为它“跑在内存里”,更在于它对每一处性能细节的极致优化:线程协作机制、异步持久化策略、数据结构设计,从运行机制到底层结构,都为高性能系统提供了基础。

如果这篇文章对你有帮助,欢迎点赞、转发、留言

http://www.xdnf.cn/news/19662.html

相关文章:

  • 【数据结构探秘】手把手用单链表实现增删查改:一篇面向 C 程序员的实战指南
  • C#枚举类型的定义及其用法
  • WS2812灯带效果设计器上位机
  • 微服务的编程测评系统19-我的消息功能-竞赛排名功能
  • ChartView的基本使用
  • 【学Python自动化】 7.1 Python 与 Rust 输入输出对比学习笔记
  • Linux系统shell脚本(二)
  • 【Python - 基础 - 工具】解决pycharm“No Python interpreter configured for the project”问题
  • 机器学习入门,支持向量机
  • Vite + React + Tailwind v4 正确配置指南(避免掉进 v3 的老坑)
  • 为什么程序员总是发现不了自己的Bug?
  • Flutter 3.35.2 主题颜色设置指南
  • 使用 qmake 生成 Makefile,Makefile 转换为 Qt 的 .pro 文件
  • Redis核心数据类型解析——string篇
  • 基于YOLO8的番茄成熟度检测系统(数据集+源码+文章)
  • 2025年女性最实用的IT行业证书推荐:赋能职业发展的8大选择
  • Elasticsearch面试精讲 Day 5:倒排索引原理与实现
  • IoTDB对比传统数据库的五大核心优势
  • 深度估计:单目视觉实现车距测量和车速估计(含完整项目代码)
  • ubantu20.04 git clone 无法连接问题与解决方法
  • netstat用法
  • 别再让分散 IO 拖慢性能!struct iovec:高效处理聚集 IO 的底层利器
  • pikachu之 unsafe upfileupload (不安全的文件上传漏洞)
  • 力扣hot100:除自身以外数组的乘积(除法思路和左右前缀乘积)(238)
  • 毕业项目推荐:70-基于yolov8/yolov5/yolo11的苹果成熟度检测识别系统(Python+卷积神经网络)
  • 【无人机三维路径规划】基于遗传算法GA结合粒子群算法PSO无人机复杂环境避障三维路径规划(含GA和PSO对比)研究
  • 基于单片机醉酒驾驶检测系统/酒精检测/防疲劳驾驶设计
  • 基于单片机雏鸡孵化恒温系统/孵化环境检测系统设计
  • WPF启动窗体的三种方式
  • 【Day 42】Shell-expect和sed