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

Redis为什么快

Redis 为什么快7个原因拆解从内存到IO从数据结构到设计哲学作者没有逆称关键词Redis 高性能、IO 多路复用、epoll、SDS、跳表、单线程模型目录一、面试被问住了二、原因一纯内存操作三、原因二单线程模型核心争议点四、原因三IO 多路复用epoll五、原因四高效的数据结构六、原因五对象共享与内存优化七、原因六渐进式 rehash八、原因七合理的事件驱动架构九、完整思维导图与总结表十、常见面试题一、面试被问住了上一场面试聊到项目缓存选型面试官冒了一句“Redis 号称 QPS 能到 10 万你说说它为什么快”我脱口而出“因为它是内存数据库……”面试官继续盯着我我硬挤了几个词——单线程、IO多路复用……就到头了。事后复盘发现Redis 快的理由远不止两三点面试官期待的是系统性回答从内存到IO、从数据结构到设计哲学一层层拆开讲。下回再被问到我可以按下面这个思路答了——先给面试官一个总览Redis 高性能 内存操作 单线程 IO多路复用 高效数据结构 内存优化 渐进式rehash 事件驱动 ┌─────────────────────────────────────┐ │ 各因素影响权重 │ ├─────────────────────────────────────┤ │ 纯内存操作 ★★★★★ 最重要 │ │ 高效数据结构 ★★★★☆ │ │ IO多路复用 ★★★★☆ │ │ 单线程模型 ★★★☆☆ 有争议 │ │ 渐进式rehash ★★★☆☆ │ │ 对象共享与编码优化 ★★★☆☆ │ │ 事件驱动架构 ★★☆☆☆ │ └─────────────────────────────────────┘下面逐条展开。二、原因一纯内存操作这是最根本的原因没有之一。2.1 内存 vs 磁盘 速度对比一个简单的数量级对比存储层级典型延迟相比内存的倍数CPU 寄存器~1 ns—内存RAM~100 ns1x基准SSD随机读~100 μs1000x磁盘机械寻道~10 ms100,000x网络请求同机房~500 μs5000xRedis 所有的数据读写都在内存中完成不需要磁盘 IO。2.2 对比 MySQLMySQL 查询路径 查询 → 解析SQL → 执行计划 → 存储引擎 ↓ 检查 Buffer Pool内存中是否有缓存页 ↓ 没有 → 磁盘 IO 加载数据页慢 ↓ 返回结果 Redis 查询路径 命令 → 解析 → 直接在内存哈希表查 ↓ 返回结果 快一句话内存访问速度比磁盘快 3~5 个数量级这是 Redis 快的先天优势。三、原因二单线程模型核心争议点3.1 Redis 的核心处理确实是单线程Redis 6.0 之前——所有网络 I/O 和命令执行全部单线程。Redis 6.0 之后——网络 I/O 引入多线程命令执行依然是单线程。很多人疑惑单线程不是更慢吗怎么反而成了高性能的原因3.2 单线程为什么快原因 1避免了上下文切换的开销多线程环境下CPU 需要在不同线程之间切换每次切换都要保存和恢复寄存器、程序计数器等带来几十到几百纳秒的额外开销。多线程模型一般应用 线程A ———→ 切换 → 线程B ———→ 切换 → 线程A ↑ ↑ 保存/恢复 保存/恢复 上下文 上下文 单线程模型Redis 线程A —→ 一直跑 —→ 完事 没有切换没有开销原因 2避免了锁竞争多线程共享数据需要加锁加锁本身有开销锁冲突更会导致线程阻塞等待。Redis 单线程执行命令天然不需要锁你们同时读 Key A排好队一个一个来不存在谁等谁的情况。3.3 单线程的瓶颈在哪怎么解决瓶颈点后果解决方案单个 key 存储超大数据big key阻塞其他命令拆分大 key、用 scan 分批CPU 密集型操作如 hgetall 千万级哈希长时间独占 CPU优化数据结构、使用 unlink 异步删除单核性能上限单个实例吞吐瓶颈Redis Cluster 分片扩容3.4 那为什么 6.0 又引入了多线程6.0 的多线程只用于网络 I/O 读写不用于命令执行Redis 6.0 多线程模型 主线程单线程——执行命令 ↕ I/O 线程池多线程——读写 socket 数据改进的原因是网络 I/O 读写在大流量下占用 CPU 时间可观用多线程分摊后主线程可以更多地处理命令。四、原因三IO 多路复用epoll4.1 什么是 IO 多路复用先看一个场景你是 Redis 服务器有 10000 个客户端连接着你。每个客户端随时可能发命令过来你该怎么知道谁发了消息方案一BIO阻塞 IO——每个连接一个线程// 伪代码每个客户端一个线程while(true){SocketclientSocketserverSocket.accept();newThread(()-{while(true){byte[]dataclientSocket.read();// 阻塞没有数据就等// 处理命令}}).start();}→ 1 万连接 1 万个线程 内存爆炸 上下文切换灾难方案二NIO epoll 多路复用Redis 的做法// 伪代码一个线程管理所有连接intepollFdepoll_create(10000);while(true){intnepoll_wait(epollFd,events,10000,-1);// 谁有数据就返回谁for(inti0;in;i){// 只处理有数据可读的 socket效率极高processCommand(events[i].fd);}}4.2 epoll 为什么比 select/poll 快机制时间复杂度最大连接数工作方式selectO(n)1024轮询所有 fdpollO(n)无限制轮询所有 fdepollO(1)无限制回调通知只返回有事件的 fdepoll 的核心优势——它不轮询只通知。你问 1 万个学生“谁有问题”select/poll遍历一遍vs有问题的人举手你只看到举手的人epoll回调通知4.3 Redis 中的实现┌─────────────────────────────────────────┐ │ Redis 事件循环 │ │ │ │ ┌─────────────┐ ┌───────────────┐ │ │ │ 文件事件 │ │ 时间事件 │ │ │ │Socket可读│ │定时任务 │ │ │ └──────┬──────┘ └──────┬────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────────────────────────┐ │ │ │ aeApiPoll(epoll_wait) │ │ │ └──────────────┬───────────────┘ │ │ │ │ │ ▼ │ │ 处理并返回结果给客户端 │ └─────────────────────────────────────────┘Redis 内部封装了ae 事件驱动库在 Linux 上使用 epoll在 macOS 上用 kqueue用统一接口隐藏差异。五、原因四高效的数据结构Redis 不只存字符串它的五种基本类型String / Hash / List / Set / ZSet底层都用了一套精心设计的数据结构针对不同场景做了不同的编码优化。5.1 底层数据结构一览┌──────────┐ │ SDS │ ← 代替 C 字符串的智能字符串 ├──────────┤ │ 双向链表 │ ← List 的底层之一 ├──────────┤ │ 压缩列表 │ ← 少量元素时的紧凑存储 ├──────────┤ │ 哈希表 │ ← Hash / Set 的底层 ├──────────┤ │ 跳表 │ ← ZSet 的核心结构 ├──────────┤ │ 整数集合 │ ← 纯整数时的 Set 优化 ├──────────┤ │ 快速列表 │ ← 压缩列表 双向链表的混合体 └──────────┘5.2 SDSSimple Dynamic StringC 语言的字符串用char[]问题很多。Redis 自己实现了 SDS// Redis 3.2 的 SDS 结构sdshdr8structsdshdr8{uint8_tlen;// 已用长度uint8_talloc;// 总分配长度unsignedcharflags;// 类型标记charbuf[];// 数据};SDS 比 C 字符串好在哪对比项C 字符串SDS获取长度O(n)遍历直到\0O(1)直接读 len缓冲区溢出容易不检查边界自动扩容预分配空间二进制安全❌\0会截断数据✅ 以 len 为准存二进制修改时重新分配每次都要空间预分配 惰性释放减少 realloc5.3 跳表Skip ListZSet 的有序数据结构使用的是跳表skip list不是平衡树。跳表的原理Level 3: 1 ─────────────────────────→ 9 ─────────→ null Level 2: 1 ─────────→ 5 ─────────→ 9 ─────────→ null Level 1: 1 ─→ 3 ─→ 5 ─→ 7 ─→ 9 ─→ 11 ─→ 13 ─→ null ↑ 查找 7从顶层开始 1 7 → 向右 9 7 → 降一层 5 7 → 向右 9 7 → 降一层 7 7 ✓ O(logN)为什么用跳表不用平衡树对比项红黑树/平衡树跳表实现复杂度复杂旋转、染色简单随意增删层数范围查询中序遍历较复杂简单底层链表直接遍历内存占用每个节点 2 个指针平均每个节点 1.33 个指针更低插入/删除O(logN)O(logN)5.4 压缩列表ziplist当 Hash / List / ZSet 的元素较少时Redis 用压缩列表代替哈希表/跳表连指针都不存整个结构就是一块连续内存压缩列表内存布局 [zlbytes] [zltail] [zllen] [entry1] [entry2] ... [entryN] [zlend] 4字节 4字节 2字节 变长 变长 1字节每个 entry 用变长编码存储前一个 entry 的长度和自身数据不存指针纯挨着放。面试重点你能说出压缩列表比哈希表好在哪—— 节省内存适合小数据量。六、原因五对象共享与内存优化6.1 整数对象共享池Redis 启动时会预创建0~9999 这 1 万个整数对象后续所有用到这些整数值的地方直接复用127.0.0.1:6379SET k1100OK127.0.0.1:6379SET k2100OK# k1 和 k2 的值指向同一个共享整数对象不重复分配内存6.2 不同编码自动转换Redis 会根据元素的数量和大小自动选择最省内存的编码方式Hash/List/ZSet 编码自动升级路径 少量元素 ────→ 压缩列表ziplist │ │ │ 元素增多或 │ │ 元素变大 │ ▼ ▼ 大量元素 ────→ 哈希表 / 跳表 / 快速列表 Set 编码自动升级 纯整数 ──→ 整数集合intset │ │ 添加非整数 ▼ 任意值 ──→ 哈希表示例你往一个 ZSet 里只塞了 10 个分数Redis 不会直接用跳表而是用压缩列表省内存。七、原因六渐进式 rehash7.1 问题哈希表要扩容怎么办哈希表元素越来越多负载因子提高必须扩容。通常的做法旧表4个桶 → 新表8个桶 1. 分配新表 2. 把旧表所有元素 rehash 到新表 ← 这一步如果元素很多会卡住 3. 释放旧表如果 Redis 在 rehash 时停下所有请求几百万 key 全部重新计算哈希位置——那快就不成立了。7.2 Redis 的做法渐进式不一次性搬完每次搬一点点┌─────────────────────────────────────────┐ │ 渐进式 rehash 示意图 │ │ │ │ rehashidx 0 │ │ │ │ 旧表[0] ──→ 新表[0] 本次搬 │ │ 旧表[1] ──→ 新表[1] 下次搬 │ │ 旧表[2] ... │ │ 旧表[3] ... │ │ 旧表[4] ... │ │ 旧表[5] ... │ │ 旧表[6] ... │ │ 旧表[7] ... │ │ │ │ 每次增删改查操作顺带搬一个桶 │ │ 搬完一个 rehashidx │ │ 搬完所有 → rehashidx -1完成 │ └─────────────────────────────────────────┘关键点不是专门起个线程搬而是每次处理客户端命令时顺带搬搬的同时不影响读写——查一个 key先查新表查不到再查旧表新 key只往新表加旧表的 key 只会越来越少一句话渐进式 rehash 把一次性阻塞变成了平滑过渡保证了 Redis 在高负载下的响应速度。八、原因七合理的事件驱动架构8.1 Redis 的完整执行流程┌─────────┐ ┌───────────────┐ ┌──────────┐ │ 客户端 1 │ ──→ │ │ │ │ └─────────┘ │ IO 多路复用 │ │ 命令执行 │ ┌─────────┐ │ (epoll_wait) │ ──→ │ (单线程) │ │ 客户端 2 │ ──→ │ │ │ │ └─────────┘ └───────────────┘ └──────────┘ ┌─────────┐ │ │ 客户端 N │ │ └─────────┘ ▼ ┌──────────────┐ │ 三种结果类型 │ │ │ │ 简单字符串 → │ │ 数组 → │ │ 整数 → │ └──────────────┘8.2 Redis 的处理流程有多轻量对比 Spring Boot Web 请求处理Spring Boot 处理一次请求 创建线程或从线程池取→ 解析HTTP → 调用Controller → Service → DAO → 数据库查询 → 序列化JSON → 写回 整个链路层层抽象框架开销大 Redis 处理一次命令 epoll_wait 返回 → 读取命令 → 查找 key → 执行操作 → 写回结果 几乎没有框架抽象层纯 C 手写极其轻量九、完整总结9.1 一图流┌──────────────────────────────┐ │ Redis 为什么快 │ └──────────────────────────────┘ │ ┌───────────────┼───────────────────┐ │ │ │ ┌────┴────┐ ┌────┴────┐ ┌──────┴──────┐ │ 内存层 │ │ IO 层 │ │ 数据结构层 │ └────┬────┘ └────┬────┘ └──────┬──────┘ │ │ │ ┌─────┴─────┐ ┌────┴─────┐ ┌──────┴──────┐ │ 纯内存操作 │ │ IO多路 │ │ SDS / 跳表 │ │ │ │ 复用epoll│ │ 压缩列表 │ └───────────┘ └──────────┘ └─────────────┘ ┌───────┐ ┌──────────────┐ ┌─────────────┐ │ 模型层 │ │ 优化层 │ │ 架构层 │ └───┬───┘ └──────┬───────┘ └──────┬──────┘ │ │ │ ┌────┴─────┐ ┌────┴──────┐ ┌──────┴──────┐ │ 单线程 │ │ 整数共享 │ │ 渐进式 │ │ 无锁设计 │ │ 编码切换 │ │ rehash │ └──────────┘ └───────────┘ └─────────────┘9.2 速记表原因一句话总结面试优先级纯内存操作比磁盘快 3~5 个数量级⭐⭐⭐⭐⭐单线程模型无上下文切换、无锁竞争⭐⭐⭐⭐IO 多路复用 epollO(1) 事件通知不轮询⭐⭐⭐⭐⭐SDS / 跳表 / 压缩列表数据结构为性能极致优化⭐⭐⭐⭐整数共享 编码切换减少内存分配、自动选择最优编码⭐⭐⭐渐进式 rehash扩容不阻塞平滑过渡⭐⭐⭐⭐轻量事件驱动C 语言实现几乎没有框架抽象开销⭐⭐十、常见面试题Q1Redis 单线程的优缺点6.0 为什么引入多线程答优点避免了线程上下文切换的开销避免了锁竞争实现简单不容易出错缺点CPU 密集型操作会阻塞所有请求单核能力有上限只能靠集群扩容6.0 多线程仅在网络 I/O 读写阶段使用多线程命令执行依然是单线程。原因是大流量下网络 I/O 本身就是瓶颈用多线程分摊后主线程 CPU 更宽裕。Q2Redis 的 IO 多路复用具体是怎么实现的答Redis 封装了ae事件驱动库底层在 Linux 上用epollmacOS 上用kqueue。工作机制是注册 socket 到 epoll然后epoll_wait阻塞等待事件有事件到达时只处理有数据可读的 socket时间复杂度 O(1)。Q3Redis 的哈希表是怎么扩容的会不会阻塞答Redis 采用渐进式 rehash。当负载因子超过阈值时分配新哈希表但不一次性搬完。每次处理客户端命令时顺带搬一个桶直到全搬完。rehash 期间查 key 先查新表再查旧表新增 key 只加新表不影响读写。Q4SDS 比 C 字符串好在哪里答O(1) 获取长度——C 字符串需要遍历到\0二进制安全——以 len 为长度存二进制数据没问题自动扩容——不需要手动管理内存空间预分配——减少内存 realloc 次数Q5ZSet 为什么用跳表不用红黑树答实现简单——跳表代码比红黑树简单得多不容易出 bug范围查询方便——底层是链表直接遍历即可红黑树中序遍历较复杂内存更低——跳表平均每个节点约 1.33 个指针红黑树需要 2 个左右子节点 1 个父节点 3 个O(logN) 性能不输红黑树Q6Redis 处理 10 万 QPS瓶颈到底在哪里答瓶颈通常不在 CPU而是网络带宽——尤其大 key 传输时内存带宽——huge key 操作频繁时big key 阻塞——单个 key 几十 MB一次操作阻塞其他请求fork 耗时——RDB 持久化时的 fork 操作在内存大时会阻塞Q7如果 Redis 是单线程那持久化RDB/AOF不会阻塞吗答核心命令执行是单线程但 Redis 用子进程fork处理持久化RDB——fork 出子进程写快照主线程继续服务AOF 重写——也是 fork 子进程处理fork 本身会阻塞主线程复制页表大内存10G时 fork 耗时明显Q8和 Memcached 比为什么 Redis 有时更快答Redis 有更丰富的数据结构跳表、压缩列表等在某些场景下比 Memcached 的纯 KV 更高效Redis 单线程无锁Memcached 多线程需要锁竞争Redis 渐进式 rehash 保证响应平稳Memcached 扩容可能抖动但纯 KV 简单场景比如只缓存 HTML 片段Memcached 多线程可能通过并行获得更高吞吐如果这篇文章对你有帮助点个赞吧参考资料Redis 官方文档 - Redis Internals《Redis 设计与实现》—黄健宏蚂蚁金服中间件 - Redis 为什么快系列
http://www.zskr.cn/news/1310707.html

相关文章:

  • 西门子GRAPH静态参数实战:从数据块解读到程序调试
  • 芯片物理验证中标准单元体端连接:从原理到LVS实践
  • 【网络诊断实战】从Ping到Traceroute:十大核心命令构建你的网络排错工具箱
  • 迭代器用错直接报ConcurrentModificationException?一份关于Java集合遍历与删除的避坑指南
  • 告别F2进BIOS:手把手教你用Dell R630的F11快捷启动菜单装Win Server 2019
  • 终极固件解密指南:Universal-IFR-Extractor快速提取EFI/UEFI内部表单
  • 2026 青岛 GEO 优化服务商全景评测:本地头部geo公司推荐选型指南 - 速递信息
  • 梯度提升树GBDT:从梯度下降到集成学习的实战推演
  • GBFR Logs:碧蓝幻想Relink伤害统计工具全攻略与故障排除指南
  • RepoMap-AI:基于LLM的代码仓库智能分析与可视化地图生成
  • Cortex-A55内存管理架构与MMU优化实践
  • Audiveris:免费开源乐谱识别神器,10分钟将纸质乐谱转换为可编辑数字格式
  • ppt模板_0027_83tm儿童节
  • 如何快速备份微博:免费高效的微博PDF导出解决方案
  • 5分钟彻底告别桌面混乱:NoFences免费分区工具终极指南
  • macOS逆向工程实战:百度网盘SVIP破解插件深度解析
  • 上海亨得利陶瓷配件专业修复评估全解析:从香奈儿J12到爱彼皇家橡树,坚硬≠不坏,一次精准诊断可能替您省下整表30%的损失 - 亨得利腕表维修中心
  • 京东商品自动化抢购终极指南:3步快速上手JDspyder脚本
  • 从游戏平衡到推荐算法:线性方程组Ax=b在真实项目里到底怎么用?
  • ESP32蓝牙键盘库(BLE-Keyboard)的另类玩法:把EC11编码器变成多媒体控制器
  • 告别玄学!用电流型补偿网络搞定开关电源环路设计(附TI/ADI仿真文件)
  • 网络故障定位慢?可能是你没用好LLDP!手把手教你排查链路层‘隐身’问题
  • 厦门奢侈品首饰多店甄选,收的顶正规门店结算效率出众 - 奢侈品回收测评
  • 窗口尺寸自由掌控:SRWE如何让任意程序窗口随心所欲
  • DBSync:解锁异构数据库实时同步的通用利器
  • 别再只用热图了!用R语言这5种可视化方法,让你的样本相似性分析更直观
  • 现在不掌握NotebookLM航天科研工作流,你将错过下一轮国家重大专项申报窗口期——3大航天高校已启用的AI原生课题孵化模板首次解密
  • 【uniapp】告别静态focus:动态控制input聚焦的实战与思考
  • 多集群编排利器mco:统一管理Kubernetes混合云应用部署
  • 【原书 PDF + 中文版 下载】创始人手册:打造AI原生初创公司《 The founder‘s playbook: Building an AI-native startup》