ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Redis进阶实战:从底层原理到高并发架构与面试

Redis进阶实战:从底层原理到高并发架构与面试 这次我们来看一个非常典型的 Redis 面试进阶实战内容体系。它不满足于“背八股”而是把 Redis 底层原理、源码实现和企业高并发架构实践打包在一起目标是解决一个问题面试官问 Redis 时不只是在背书而是真的能讲清楚它为什么快、怎么用对、挂了怎么处理。如果你是准备跳槽的 Java 后端、还在用 RedisTemplate 却说不清 SDS 和 ziplist 的同学或者想在系统设计里把缓存、分布式锁、高可用讲明白的开发这套内容值得按下面的路线完整过一遍。这篇文章我会把整套学习内容拆成可执行的路径先看核心知识模块再讲源码怎么读然后落到企业高并发场景的架构实践最后给出一份面试题拆解和回答思路。文章里会包含可直接复制的 Redis 操作命令、Lua 脚本、Spring Boot 配置示例以及一套通用的验证流程方便你学完每个章节后在本地环境里立刻验证。1. 教程核心内容速览这套教程的主线很清晰底层原理、源码分析、架构实践、面试应答。下面用一张表把内容体系拆开方便你判断哪些章节需要重点投入时间。知识模块核心知识点面试高频度工程落地程度数据结构与对象SDS、双向链表、压缩列表、跳表、整数集合、哈希表高中持久化机制RDB、AOF、混合持久化、fork 与写时复制高高网络模型与线程模型单线程事件循环、I/O 多路复用、Redis 6.x 多线程高中内存管理与淘汰过期键删除策略、8 种内存淘汰策略高高分布式架构主从复制、哨兵、Cluster 集群、分片与槽位高高缓存三大问题穿透、击穿、雪崩布隆过滤器互斥锁高高分布式锁SETNX 演进、Redisson 看门狗、Lua 原子性高高缓存一致性Cache Aside、延迟双删、binlog 订阅中高高高级用法发布订阅、Stream、Lua 脚本、限流中中从这张表可以看出这套教程不是只讲命令怎么敲而是把“面试会问什么”和“生产环境怎么用”绑在一起讲。对你来说最关键的是不要跳过底层原理部分因为后面架构实践里的很多决策比如为什么用 Cluster、为什么用 Lua 脚本、为什么缓存不能存太多热点 key根因都在底层数据结构与线程模型那一层。2. 适用人群与使用边界这套教程适合以下几类人第一类是准备中高级后端岗位面试的开发者。Redis 几乎是后端技术面必问项从 String 类型底层结构到缓存雪崩处理再到分布式锁的 Redisson 实现都属于高频考点。只靠背题不够你需要能讲清楚源码层面的设计取舍。第二类是已经在负责业务系统、准备做架构优化的开发。比如你们系统里有热点缓存、需要做分布式锁、或者正在从单机 Redis 迁移到哨兵和 Cluster这套内容涉及的架构实践可以直接作为方案参考。第三类是刚接触 Redis 但想建立系统认知的初学者。虽然标题写“进阶”但内容组织是从底层结构逐步向上建只要你能看懂基本的 Java/C 概念按顺序学不会出现断档。使用边界也要说明清楚。这套教程讲的是 Redis 服务端原理和通用实践不涉及 Redis 客户端源码级分析也不包含具体公司的业务代码。涉及分布式锁、数据一致性、缓存降级等场景时在生产环境上线前必须结合你们自己的业务做压测和评审不能直接照搬网上所有方案。另外教程中涉及源码分析的部分建议自行从 Redis 官方仓库拉取对应版本代码核对不要只依赖二手讲解。3. Redis 底层原理拆解从数据结构到源码这一部分是整个教程的地基。面试官之所以喜欢从底层结构问起是因为数据结构决定了 Redis 能做什么、不能做什么也决定了性能和内存的取舍。3.1 SDS 简单动态字符串Redis 没有直接使用 C 语言的字符串而是自己封装了 SDS。从源码sds.h和sds.c里可以看到SDS 结构包含了长度、已分配空间、flags 和字符数组。相比 C 字符串SDS 的核心优势有三个获取长度是 O(1)因为 len 字段直接记录避免缓冲区溢出修改前会检查空间减少内存重新分配次数通过预分配和惰性释放来优化。这套教程讲解 SDS 时通常会对比 C 字符串建议你也从这三个角度去记忆。面试题里经常出现“Redis 为什么快”SDS 设计就是其中一个加分点。# 可以本地验证 SDS 的编码展示效果 # 注意 Type 和 Encoding 的对应关系 127.0.0.1:6379 SET name hello redis OK 127.0.0.1:6379 OBJECT ENCODING name embstr3.2 压缩列表与快速列表List 对象在元素数量少、元素长度短时使用 ziplist 编码元素量大了以后会转换为 quicklist。Hash 对象在字段少、值长度短时也走 ziplist。教程讲到这里时会强调一个关键点ziplist 是一种紧凑型内存结构用连续内存块存储数据但它不适合存储大量数据因为更新可能引发连锁更新性能退化明显。从面试角度看你需要回答清楚“ziplist 和 linkedlist 的区别”“什么时候转 quicklist”“为什么 Redis 3.2 引入 quicklist”。quicklist 的设计本质是双向链表加 ziplist 节点是一个时间换空间的折中方案。# 查看 Redis 默认的 list 编码转换参数 127.0.0.1:6379 CONFIG GET list-max-ziplist-size 1) list-max-ziplist-size 2) -23.3 跳表与有序集合有序集合 ZSET 的底层实现是 skiplist 加哈希表跳表用于按分数排序哈希表用于按成员查找。跳表之所以成为 Redis 的选择而不是红黑树主要原因是实现简单、范围查询方便、插入删除时只需要调整前后节点的指针。教程里的源码分析会给你展示t_zset.c中跳跃表节点的结构定义重点看level数组和span字段前者体现随机层高后者记录跨度这是 ZRANGEBYSCORE 能高效实现的关键。# 验证 ZSET 的有序性和范围查询 127.0.0.1:6379 ZADD ranking 100 user:1 200 user:2 50 user:3 (integer) 3 127.0.0.1:6379 ZRANGEBYSCORE ranking 0 150 WITHSCORES 1) user:3 2) 50 3) user:1 4) 1003.4 字典与哈希Redis 的 Hash 对象底层是 dict 结构与 Java 的 HashMap 有点类似也是数组加链表并且在扩容时采用渐进式 rehash。教程讲解 dict 时会重点分析dict.h里的dictht数组为什么需要两张哈希表渐进式 rehash 如何避免一次性迁移产生的阻塞。面试题里如果问到“哈希冲突怎么解决”“rehash 过程会不会阻塞”你需要答出链地址法、负载因子、渐进式迁移这四个要点。3.5 事件驱动与单线程模型Redis 为什么用单线程却还能支撑高并发这是最经典的面试题。从源码看Redis 基于 ae 事件循环库通过 epoll 实现 I/O 多路复用在单线程中处理文件事件和时间事件。核心原因是 Redis 的操作集中在内存中CPU 不是瓶颈真正的瓶颈是网络 I/O 和内存大小所以单线程避免了锁竞争和上下文切换开销。教程还会提到 Redis 6.x 引入的 I/O 多线程但你要注意多线程只用来处理网络读写的读写和协议解析命令执行依然是单线程。这就是为什么你还能在一个线程模型下保证原子性不需要额外的锁。回答这类问题时把“网络 I/O 多线程 命令执行单线程”讲清楚面试官会认为你读过源码而不是只背结论。# 查看 Redis 线程模型相关配置 127.0.0.1:6379 CONFIG GET io-threads 1) io-threads 2) 14. Redis 源码阅读方法论教程会带你深入源码但你自己也要有一套阅读方法否则容易迷失在 C 代码里。这里给出一套通用流程配合教程使用效率更高。4.1 先看目录结构拉取 Redis 源码后先建立整体概念。核心目录如下src/ ├── server.c # 服务端主逻辑入口函数 main() ├── networking.c # 网络连接与命令读取 ├── db.c # 数据库键空间操作 ├── dict.c # 哈希表实现 ├── sds.c # 简单动态字符串实现 ├── t_string.c # String 类型命令实现 ├── t_list.c # List 类型命令实现 ├── t_hash.c # Hash 类型命令实现 ├── t_zset.c # ZSET 命令实现 ├── ae.c # 事件循环核心 ├── rdb.c # RDB 持久化 ├── aof.c # AOF 持久化4.2 按调用链阅读不要从头到尾读而是按“命令入口 - 数据结构 - 底层实现”的调用链去读。比如你想搞懂 SET 命令就以setCommand函数为入口看它如何调用setGenericCommand最终如何写入 dict。教程里讲源码时也是这么组织的。# 快速搜索源码中的命令入口 grep -n setCommand src/server.c grep -n struct redisCommand src/server.c | head -204.3 用本地实例验证源码光读不跑很难形成深刻记忆。建议本地启动一个 Redis 实例对照源码学习时用OBJECT ENCODING、INFO、DEBUG JMAP等命令观察内部结构变化。# 启动一个仅本地访问的 Redis 实例 redis-server --port 6380 --save --appendonly no # 观察键的内部编码和内存信息 redis-cli -p 6380 127.0.0.1:6380 SET user:profile {name:test} OK 127.0.0.1:6380 OBJECT ENCODING user:profile raw 127.0.0.1:6380 MEMORY USAGE user:profile (integer) 1045. 企业高并发场景架构实践这是整套教程里工程价值最高的部分。面试中问“缓存穿透怎么解决”“Redis 挂了怎么办”实际就是在考察你在生产环境的设计能力。下面按高并发场景拆出五个重点。5.1 缓存穿透、击穿、雪崩与防护缓存穿透是查询一个根本不存在的数据请求直接打到数据库。常见方案有两个缓存空值并设置较短过期时间使用布隆过滤器在缓存层之前拦截不存在的 key。布隆过滤器存在误判率所以教程里会强调要把 key 预估数量 n 和误判率 p 作为调参依据。缓存击穿是某个热点 key 过期瞬间大量请求同时打到数据库。解决办法是互斥锁只让一个请求去重建缓存其他请求等锁后重新查询缓存。从工程实现看可以用SET NX PX或 RedisTemplate 的锁方法实现。缓存雪崩是大量 key 同时过期或者 Redis 实例宕机。应对策略包括过期时间加随机值、使用多级缓存、Redis Cluster 做高可用部署、服务接口做熔断降级。教程会给出的一个核心原则是不要让所有热点 key 在同一时刻过期也不要让缓存成为单点。# Spring Boot 中给缓存过期时间加随机值的示例配置思路 spring: cache: redis: time-to-live: 600000 cache-null-values: true5.2 分布式锁的正确姿势使用SETNX加锁容易踩坑。只加锁不设过期时间宕机会导致死锁只设过期时间但业务执行太长锁提前释放导致并发问题。正确定位是使用SET key value NX PX 30000保证原子性再配合 Lua 脚本在释放时检查 value 是不是自己的。Redisson 的实现更符合生产标准它的看门狗机制会自动续期避免业务没执行完锁就过期。从面试角度你要能画清楚下面这段逻辑-- 释放锁的 Lua 脚本保证比较 key 值和删除 key 的原子性 if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end// Redisson 分布式锁使用示例 RLock lock redissonClient.getLock(order:pay: orderId); try { // waitTimeout 获取锁等待时间leaseTime 锁持有时间 boolean locked lock.tryLock(3, 30, TimeUnit.SECONDS); if (locked) { // 核心业务逻辑 } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }5.3 哨兵与 Cluster 集群高可用单机 Redis 出现故障业务直接不可用。教程会带你分别理解两种高可用方案。哨兵模式负责监控主从架构自动故障转移客户端只感知一个逻辑入口Cluster 模式则解决数据量过大、单点写入瓶颈的问题通过分片把数据分布到 16384 个哈希槽上每个节点负责一部分槽位。从部署角度看哨兵模式一般适合数据量可控、读多写少的场景Cluster 适合需要水平扩展、数据量较大的场景。Cluster 客户端要处理MOVED重定向和ASK重定向这也常被作为面试题提问。建议你本地起一个三主三从的 Cluster 集群通过redis-cli -c验证槽位迁移和数据访问。# 创建 Redis Cluster 的简化命令示例 redis-cli --cluster create \ 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \ 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \ --cluster-replicas 15.4 缓存与数据库一致性这是高并发场景里最难处理的问题之一。主流方案是 Cache Aside Pattern先更新数据库再删除缓存。为什么不是先删缓存再更新库因为更新库期间会有并发请求把旧数据写回缓存导致缓存中是旧值。延迟双删方案是在删除缓存后间隔较短时间再次删除缓存目的是清理并发期间写回缓存的数据。但这种方案对删除间隔、重试机制要求比较高。教程还会提到基于 MySQL binlog 订阅的最终一致性方案比如 Canal 把数据变更同步到 Redis。你要知道没有绝对强一致的缓存方案生产环境只能根据业务容忍度做取舍。// Cache Aside 模式的简化流程 // 1. 更新数据库 updateDb(entity); // 2. 删除缓存 redisTemplate.delete(cacheKey);5.5 异步队列与限流Redis 除了做缓存还能承担轻量级异步队列和限流。List 的LPUSH和BRPOP可以构造阻塞队列Stream则更适合复杂消费组场景。限流场景中固定窗口计数可以用INCR加过期时间滑动窗口可以用ZSET统计时间范围内的请求数。漏桶和令牌桶算法也可以基于 Lua 脚本实现教程里通常会有完整脚本示例。-- 基于 ZSET 的滑动窗口限流 Lua 脚本示例 local key KEYS[1] local limit tonumber(ARGV[1]) local window tonumber(ARGV[2]) local current redis.call(ZCOUNT, key, -inf, inf) if current limit then redis.call(ZADD, key, current, current) redis.call(EXPIRE, key, window) return 1 end return 06. Redis 面试高频题拆解与回答思路很多同学在面试时能说出 Redis 是什么但回答深度不够。下面整理一批教程中覆盖的高频题以及它们的核心回答角度。面试题主要考察点建议回答思路Redis 为什么快内存访问、I/O 多路复用、单线程模型、数据结构设计内存操作 epoll 单线程避免锁和上下文切换 SDS/跳表优化String 的底层编码有哪些SDS、embstr、raw 的差异长度短用 embstr超过阈值转 raw内存分配方式不同什么是缓存穿透、击穿、雪崩三类故障的区别和防护方案空值缓存/布隆过滤器、互斥锁、过期随机、高可用Redis 持久化 RDB 和 AOF 怎么选持久化原理、数据安全性、性能影响RDB 快照恢复快但可能丢数据AOF 安全但文件大生产多用混合Redis 分布式锁怎么做SETNX、过期时间、Lua、RedissonSET NX PX 唯一值 Lua 释放 Redisson 看门狗主从复制的实现原理是什么全量复制、增量复制、replid、backlog从节点发送 PSYNC全量同步 RDB后续增量通过 backlogCluster 集群写入一个不存在的 key 会不会报错槽位计算、MOVED 重定向CRC16(key) % 16384算槽客户端处理 MOVEDRedis 内存淘汰策略有哪些内存管理、LRU/LFU 区别noeviction、allkeys-lru、allkeys-lfu 等 8 种缓存和数据库一致性怎么做双写一致性、最终一致性Cache Aside 延迟双删 binlog 异步同步回答面试题时不要只背结论一定要补一句“为什么”。比如答缓存穿透不只说“布隆过滤器”要补充“用 Redis 的 bitmap 结构在数据不存在时直接拦截牺牲一点误判率换数据库安全”。相比之下网上很多内容碎片化严重教程的价值在于把结论背后的推理过程补完整。7. 学习与实战中的常见问题排查在学习这套教程或本地做实验时比较容易碰到下面几个问题提前知道排查思路能少走弯路。问题现象可能原因排查方式解决方案本地启动 redis-server 后端口被占用已有实例或端口冲突lsof -i:6379或netstat -ano换端口启动或用--port指定连接 Redis 报错Connection refused服务未启动或绑定地址限制检查进程和bind配置启动服务确认protected-modeOBJECT ENCODING返回结果与教程不一致Redis 版本不同编码阈值不同查看redis.conf中相关参数按版本调整配置或理解差异分布式锁释放时报错key 值不是当前线程的值检查 Lua 脚本中 value 传入加锁时生成唯一 value释放时校验Cluster 写入报MOVED错误客户端没有自动跳转使用redis-cli -c或支持集群的客户端检查客户端配置内存淘汰不生效未配置maxmemoryCONFIG GET maxmemory设置maxmemory-policy主从复制断连后重新同步失败backlog 太小INFO replication查看backlog_active调大repl-backlog-sizeLua 脚本在 Cluster 中报错key 不在同一个槽位检查 key 的哈希标签使用{prefix}让 key 落同一槽位另外建议本地准备一份统一的实验环境。用 Docker 起 Redis 最省事不但可以快速切换版本还能避免污染宿主机环境。# 使用 Docker 启动 Redis 指定版本实例 docker run --name redis-local -p 6379:6379 -d redis:7.0 # 进入容器内执行 redis-cli docker exec -it redis-local redis-cli8. 最佳实践与工程建议结合教程内容和你自己的项目下面这些工程习惯建议保持。第一次做 Redis 实验时先小规模验证。比如测试数据结构转换阈值就只用少量数据观察测试持久化先用临时目录不要直接在生产配置上改。始终保留一份可运行的最小配置改乱时能快速恢复。模型文件、配置、脚本、数据备份要分目录管理。Redis 的redis.conf、RDB 文件、AOF 文件、Lua 脚本应该放不同目录并用 Git 管理配置变更。批量操作前先评估影响比如使用KEYS命令会阻塞生产环境一定换成SCAN。# 生产环境禁止使用 KEYS 遍历使用 SCAN 替代 redis-cli --scan --pattern user:* | head -20接口服务接入 Redis 时要设置合理的连接池和超时时间避免连接耗尽。Redis 连接池的大小不是越大越好一般结合 QPS 和 Redis 处理能力设置。如果涉及多团队共享 Redis命名空间和 namespace 规划要提前做好防止 key 冲突。比如用order:pay:userId:orderId这样的结构。缓存和数据库一致性方案上线前最好做并发测试观察是否出现脏数据。对分布式锁这种核心能力要加日志记录获取锁、释放锁、等待时间方便排查死锁和性能瓶颈。涉及用户数据、商业数据时注意最小权限和访问控制。Redis 6 起支持 ACL生产环境不要用默认账号不加密码直接暴露端口。9. 总结与下一步Redis 面试进阶这条线最值得投入时间的是底层原理和一致性方案。底层结构决定性能和内存模型面试官能从你的表述里判断你是背题还是真的理解一致性方案则直接体现架构能力缓存穿透、击穿、雪崩、分布式锁、缓存一致性这几个问题几乎是每家公司在高并发场景都会遇到的。最先要验证的功能是OBJECT ENCODING和数据结构转换。你可以在本地分别插入不同长度的字符串、不同数量的列表元素观察编码变化。这一步跑通后你会对 Redis 的“内存高效”有直观感受后面再学持久化和 Cluster 会顺畅很多。最容易踩的坑有两个一是用 Windows 环境学习时版本差异导致配置和命令表现不一致建议统一用 Docker 起 Linux 容器二是分布式锁和缓存一致性方案直接照搬网络文章没有结合业务做并发测试上线后容易出问题。后续可以继续扩展的方向包括Redis 7.x 的底层优化、Redisson 源码分析、Cluster 全量迁移方案、基于 Redis 的延迟队列设计、多级缓存与本地缓存 Caffeine 的整合以及把 Redis 接入全链路压测体系。学会这套内容后不只是在面试里多拿几个加分点在实际系统遇到缓存故障时你也能比其他人更快定位原因并给出方案。建议把这份进阶路线收藏备用按底层原理、架构实践、面试拆解三个模块逐步推进。每学完一块在本地起一个对应版本 Redis 做验证知识才能真正变成你的。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进