Redis 为什么是单线程?为什么这么快?
一、澄清Redis 真的是单线程吗------------------------ 主线程命令处理 网络监听 | redis-server | - 单线程部分 ------------------------ ------------------------ 后台线程 | bio-close-file | - 异步关闭大文件 | bio-aof-fsync | - 异步 AOF 刷盘 | bio-lazy-free | - 异步删除大内存 | jemalloc-bg-threads | - jemalloc 后台线程 ------------------------ ------------------------ IO 线程可选 | io-threads | - 辅助 IO 处理 ------------------------真正的单线程redis-server 主线程负责所有命令处理和网络事件监听。所以准确说法是Redis 的命令处理和网络 IO 在主线程中执行其他耗时操作异步化到后台线程。二、为什么采用单线程2.1 核心原因操作是内存密集型而非 CPU 密集型Redis 是内存数据库操作的是内存数据内存操作的速度纳秒级CPU 不是瓶颈瓶颈在 IO多线程的优势是利用多核 CPU 并行处理但 Redis 的操作不涉及复杂计算多线程带来的收益有限。2.2 多线程的问题锁的复杂性Redis 支持多种对象类型对象类型数据结构实现stringint / embstr / rawlistziplist / linkedlisthashziplist / dictsetintset / dictzsetziplist / skiplist dict每个对象类型有多种底层实现如果采用多线程需要对每个对象加锁锁的粒度难以控制加锁解锁本身有开销锁粒度问题临界资源的操作需要尽量短的持有时间否则影响并发性能。2.3 多线程的问题频繁上下文切换Redis 请求量不可控可能出现请求A - 线程1 - 休眠 请求B - 线程2 - 休眠 请求C - 线程3 - 休眠 | 线程不断切换争取 CPU 时间片频繁的上下文切换会抵消多线程带来的好处单线程反而更高效。2.4 单线程的局限不能有耗时操作主线程执行 [请求1] - [请求2] - [耗时操作] - [请求3] - ... | 阻塞如果主线程中有耗时操作后续所有请求都会被阻塞。所以 Redis 将所有可能阻塞的操作异步化到后台线程。三、为什么单线程这么快3.1 内存数据库自身特性数据存储在内存中内存访问速度远高于磁盘内存纳秒级访问SSD微秒级访问HDD毫秒级访问内存操作本身已经足够快不需要多线程并行。3.2 高效的数据结构每种对象类型针对不同场景选择最优数据结构对象类型小数据量大数据量权衡点hashziplistdict节点数 512 用 ziplistzsetziplistskiplist dict节点数 128 用 ziplistlistziplistlinkedlist节省空间 vs 快速插入setintsetdict整数集合优化内存核心思想根据数据量动态选择数据结构在时间和空间上取得平衡。3.3 Reactor 网络模型Redis 采用经典的多路复用 IO 模型[文件描述符就绪事件] | ---------------------------------------------------------- | 主线程 | | [socket1] [socket2] [socket3] ... [socketN] | | | | | | | | [IO多路复用 epoll/select] | | | | | [就绪事件分派] | | | | | [命令处理器] | ----------------------------------------------------------为什么快IO 多路复用一个线程同时监听多条连接非阻塞 IOIO 操作不会阻塞主线程事件驱动只处理就绪的事件3.4 异步化设计将可能阻塞的操作全部异步化到后台线程操作异步方式说明关闭大文件bio-close-fileclose() 需要释放 FD 资源AOF 刷盘bio-aof-fsyncfsync 是阻塞操作删除大内存bio-lazy-free释放大块内存也是一种阻塞操作内存分配jemalloc-bg-threadsjemalloc 后台线程四、Redis 做了哪些优化4.1 渐进式 rehashdict 扩容采用渐进式 rehash避免一次性拷贝大量数据第一阶段两个哈希表同时存在 -------------------- | 旧表 | 新表 | (新表为空) -------------------- 第二阶段渐进迁移 -------------------- | 旧表 | 新表 | (逐个迁移) -------------------- 第三阶段迁移完成 -------------------- | | 新表 | (旧表为空) --------------------具体策略每次操作迁移一个桶或者定时任务每次迁移 1ms分散阻塞操作4.2 IO 多线程可选Redis 6.0 引入了 IO 多线程可以辅助处理 IO主线程负责命令处理 网络事件监听 IO线程辅助处理 read/write/decode/encode注意命令处理仍在主线程IO 线程只处理网络层的读写。4.3 扩容策略翻倍扩容容量64 - 128 - 256 - 512 - 1024 ...采用翻倍扩容策略减少扩容次数摊销扩容成本空间换时间五、面试追问 FAQ问题回答要点Q: Redis 真的只有单线程吗不是主线程单线程但有 bio 后台线程处理异步任务Q: 为什么 Redis 用单线程而不是多线程Redis 是内存密集型不是 CPU 密集型多线程会带来锁和上下文切换的开销Q: 单线程如何保证高性能内存操作 IO 多路复用 高效数据结构 异步化Q: 什么操作不能放在主线程耗时操作关闭大文件、刷盘、删除大内存、扩容Q: IO 多线程的作用是什么只辅助处理网络 IOread/write/encode/decode命令处理仍在主线程Q: dict 渐进式 rehash 是什么扩容时不是一次性迁移而是分散到每次操作中避免阻塞六、相关题目题目考察点Redis 6.0 多线程了解吗IO 多线程 vs 命令处理单线程Redis 为什么不用 B 树实现复杂度 vs 内存数据库场景Redis 持久化会阻塞主线程吗AOF fsync 后台线程RDB 是 fork 子进程Redis 的 key 过期后内存会立即释放吗惰性删除 定时删除不会立即释放七、总结为什么是单线程操作是内存密集型多线程收益有限多对象类型 多种数据结构锁复杂度高请求量不可控上下文切换开销大为什么这么快内存数据库内存操作足够快高效数据结构动态选择最优实现IO 多路复用非阻塞 IO异步化设计将耗时操作放到后台线程核心结论Redis 的单线程设计是基于其内存数据库特性和场景需求的优化而非能力限制。通过 IO 多路复用和异步化Redis 在单线程下实现了高性能和高吞吐。根据零声教育教学写作https://github.com/0voice