Redis 核心原理:为什么快?五种数据结构 + 持久化双引擎

本文解决什么问题:Redis 是后端开发的核心组件,但多数人仅能说出"Redis 是单线程的内存数据库"------深入理解时需追问:单线程为什么比多线程快?本文从四层原理、底层编码、持久化双引擎到高可用演进,帮助你将认知从"会用"提升到"理解内部机制"。

适合谁读:日常使用 Redis 但未系统研究过内部机制的 Java 后端开发。

核心内容:Redis 快的四层原因(内存 + 单线程 + IO 多路复用 + 精巧数据结构);五种数据类型的底层编码及 skiplist 不选红黑树的原因;过期删除策略(惰性+定期)及八种内存淘汰策略的选型依据;RDB vs AOF vs 混合持久化的设计取舍;高可用演进路线(主从 → 哨兵 → Cluster);缓存穿透/击穿/雪崩的完整应对方案。


一、Redis 为什么快?

Redis 的快不是单一原因,而是四层叠加的结果。

第一层:纯内存操作

数据驻留在内存中,读写不经过磁盘,单次操作达到微秒级。这是最快的硬件基础。

第二层:单线程模型

Redis 6.0 之前,网络 IO 和命令执行均由单线程完成。单线程带来了两个关键收益:

  • 零上下文切换开销:多线程模式下,CPU 在线程间切换需要保存/恢复寄存器状态、刷新 TLB,这些都是额外开销
  • 零锁竞争 :不存在"两个线程同时修改同一个 Key"的问题,数据操作天然具备原子性------INCR 不需要额外加锁

第三层:IO 多路复用

虽然是单线程,但可以同时监听多个客户端连接。核心在于操作系统的 epoll(Linux)/select/kqueue(BSD)机制------哪个连接有数据到达就处理哪个,不会阻塞在某个慢连接上。

第四层:高效数据结构

每种数据类型都有专门优化的底层编码,并非直接使用 C 原生数据结构:

  • SDS(简单动态字符串):相比 C 字符串增加了长度记录,获取长度 O(1),从设计上杜绝了缓冲区溢出
  • ziplist(压缩列表):以连续内存存储小数据,极致节省内存
  • skiplist(跳表):多层索引结构,支持 O(logN) 范围查询

"Redis 的快 = 内存(硬件层)+ 单线程(消除锁竞争)+ IO 多路复用(高并发连接)+ 精巧数据结构(编码优化)。四层叠加,缺一不可。"

Redis 6.0 的多线程改了什么?

Redis 6.0 引入了 IO 多路线程,但命令执行仍然是单线程。为什么要改?因为单线程的网络 IO 已成为瓶颈------大量客户端连接的数据读写量过大,主线程将大量时间消耗在解析网络包上。

改进方案:IO 线程负责读取客户端请求数据(解析 TCP 包)和写回响应数据(发送给客户端),命令执行保持单线程(以保证原子性)。

"Redis 6.0 的多线程本质是'读写外包,核心自持'------搬货可以多人协作,但算账的只能一个人。"


二、五种数据类型 + 底层编码

类型 场景 底层编码 注意事项
String 缓存、计数器(INCR)、分布式锁(SETNX) int / embstr(≤44 字节)/ raw 最大 512MB,INCR 原子自增无需加锁
Hash 存储对象(用户信息、配置项) ziplist(Redis 7.0 起升级为 listpack,原理相似但结构更紧凑)/ hashtable(两个 dict 实现渐进式 rehash) 适合部分字段更新------仅改名字无需改整个 JSON
List 消息队列(LPUSH+BRPOP 阻塞消费)、时间线 quicklist(linkedlist+ziplist 混合) 左右均可 push/pop
Set 标签、去重、共同好友 intset(纯整数)/ hashtable SINTER 交集、SUNION 并集、SDIFF 差集
ZSet 排行榜、延时队列、带权重的去重 ziplist / skiplist+dict ZRANGE 按分值排序、ZRANK 按排名查询

ZSet 为什么用跳表而不用红黑树?

三个原因:

  1. 实现简单:跳表的插入/删除/查找逻辑比红黑树的旋转加染色直观得多
  2. 天然支持范围查询:跳表的 O(logN) 范围查询天然高效,红黑树则需要中序遍历
  3. 天然支持按排名查询:ZRANK 命令需要知道元素在有序集合中的位置排名,跳表在节点中维护了 span(跨度)字段,天然支持此类查询

跳表原理:多层索引结构,Redis 实现中每层节点数约为上一层的 1/4(概率因子 P=0.25),查找时从最高层开始跳跃,时间复杂度 O(logN)。


三、过期删除 + 内存淘汰

过期删除:惰性 + 定期双管齐下

Redis 未采用基于定时器的精确删除,而是两种策略组合:

(1)惰性删除(被动) :Key 过期后不会立即被删除,等到下次访问该 Key 时检查是否过期,过期则删除并返回空。优点是对 CPU 友好;缺点是过期但未被访问的 Key 会持续占用内存。

(2)定期删除(主动) :Redis 默认每秒执行 10 次 activeExpireCycle------每次随机抽取 20 个设置了过期时间的 Key 检查是否过期,若过期比例超过 25% 则继续抽取下一批,直至过期比例降低或达到时间上限(25ms)。优点是能清理"冷"过期数据;缺点是随机抽取可能漏掉部分过期 Key。

内存淘汰:八种策略如何选择?

策略 行为 适用场景
noeviction(默认) 内存满后直接返回错误,不删除任何数据 Redis 用作数据库,配合持久化使用
allkeys-lru 在所有 Key 中淘汰最近最少使用的 缓存场景的首选
volatile-lru 仅在设置了过期时间的 Key 中淘汰 LRU 混合场景(部分 Key 需持久保留)
allkeys-lfu 淘汰访问频率最低的 Key 热点数据保护(Redis 4.0+)
volatile-lfu 在设置了过期时间的 Key 中淘汰 LFU 同上,混合场景
volatile-ttl 淘汰剩余 TTL 最短的 Key 对时效性敏感的数据
allkeys-random / volatile-random 随机淘汰 不推荐,除非 Key 大小和访问频率均匀分布

LRU vs LFU:LRU 淘汰"最久没被访问的"(时间维度),LFU 淘汰"访问次数最少的"(频率维度)。一个刚创建但只访问过一次的 Key,在 LRU 下可能因"新"而被保留,在 LFU 下则可能因频率低而被淘汰。LFU 更擅长区分热点数据与非热点数据。

"缓存场景用 allkeys-lru;需要热点数据保护用 allkeys-lfu;作为数据库使用选 noeviction 并配置持久化。"


四、持久化双引擎:RDB + AOF + 混合模式

RDB(快照)

SAVE(阻塞主线程)/ BGSAVE(fork 子进程写 RDB 文件)------生成某个时间点的全量数据快照。

  • 优点:文件紧凑、恢复速度快、fork 后子进程不影响主进程
  • 缺点:两次 RDB 之间的数据存在丢失风险(间隔通常为 5 分钟)、fork 大内存进程时可能出现短暂阻塞
  • 机制:基于 Linux Copy-On-Write------fork 时父子进程共享物理内存页,父进程写入时才触发页面复制
  • 配置save 900 1(900 秒内至少 1 次修改即触发 BGSAVE)

AOF(追加日志)

每条写命令追加到 AOF 文件末尾,重启时重放 AOF 命令以恢复数据。

三种刷盘策略:always(每条命令刷盘,最安全但最慢)→ everysec(每秒刷一次,默认推荐,至多丢失 1 秒数据)→ no(交由操作系统决定刷盘时机)。

AOF 重写 :随着时间推移 AOF 文件不断增大,BGREWRITEAOF 将多条命令压缩为最少命令(如将 5 次 INCR counter 1 合并为 INCRBY counter 5)。重写同样通过 fork 子进程完成,基于当前内存数据生成最少命令,而非重放旧 AOF 文件。

混合持久化(Redis 4.0+,生产环境推荐

AOF 文件的前半部分为 RDB 格式的快照,后半部分为快照之后的 AOF 增量日志。兼具 RDB 的恢复速度和 AOF 的数据安全性。

"生产环境使用混合持久化(RDB 全量 + AOF 增量),everysec 刷盘策略,即使宕机至多丢失 1 秒数据。"


五、高可用演进:主从 → 哨兵 → Cluster

第一代:主从复制

一主多从,读写分离。从节点只读,主节点写入后异步同步到从节点。问题:主节点宕机后需要手动切换从节点------高可用性不足。

同步流程:全量同步(RDB + replication buffer)→ 增量同步(replication backlog buffer + 偏移量)。

第二代:哨兵 Sentinel

在主从基础上增加自动故障转移能力。Sentinel 集群(至少 3 个节点,奇数个)监控主节点 → 主节点失联 → 主观下线 → 客观下线(不少于 quorum 个 Sentinel 确认)→ 选举新主(按优先级 → offset → runId 排序)→ 通知客户端切换连接。

第三代:Cluster 集群

分布式数据分片存储。共 16384 个 Hash Slot,CRC16(key) % 16384 定位到具体 Slot 再找到对应节点。自带故障转移,通过 Gossip 协议通信。支持多主多从水平扩展。

为什么是 16384 个 Slot? 集群节点间通过 Gossip 协议周期性交换状态信息,心跳包中包含本节点负责的 Slot 位图。16384 个 Slot 的位图占用 16384/8 = 2KB------这个大小在心跳包中是可接受的。如果采用 65536(CRC16 的理论上限),位图膨胀至 8KB,在 1000 节点规模的集群中 Gossip 消息的带宽开销将显著增大。16384 是在"足够均匀分布数据"和"心跳消息大小可控"之间取的工程平衡点。

分片注意事项

  • 跨 Slot 的多 Key 操作(如 MSET key1 key2)要求 key1 和 key2 位于同一 Slot,可使用 {hash_tag} 强制映射(如 user:{1001}:nameuser:{1001}:age 将被分配到同一 Slot)
  • 客户端需感知 Slot 映射变化:MOVED(永久迁移)vs ASK(临时迁移中的重定向)

"主从解决读扩展,哨兵解决高可用,Cluster 解决写扩展。三者是一步步演进出来的。技术选型的核心判断不是'这个技术能不能用',而是'这个技术是否匹配当前的规模和复杂度'。"


六、缓存三大问题及应对

缓存穿透

现象:查询一个数据库和缓存中均不存在的数据,每次请求直接打到数据库。

解决方案

  1. 布隆过滤器:将所有存在的 key 存入布隆过滤器,查询前先经过布隆过滤器------返回"不存在"则一定不存在(100% 准确),返回"可能存在"才继续查缓存/数据库(存在小概率误判)。原理:多个哈希函数映射到位数组,全为 1 则"可能存在",有 0 则"一定不存在"
  2. 缓存空值:查不到也缓存一个 null 值,设置较短的过期时间(如 5 分钟)

缓存击穿

现象:某个热点 Key 过期的瞬间,大量并发请求同时打到数据库。

解决方案

  1. 互斥锁 :获取缓存失败后加锁,拿到锁的线程负责查询数据库并更新缓存,其他线程等待锁释放后从缓存获取(双重检查:加锁后需再次读取缓存,避免重复查询数据库)
  2. 逻辑过期:在 value 中存储过期时间戳,不设置 TTL。发现已过期后获取锁去更新,其他线程先返回旧值(容忍短暂的不一致)
  3. 永不过期 + 主动刷新:热点 Key 不设置 TTL,通过消息驱动(如 XNet 广播、Canal binlog)主动推送更新------从根源上消除"过期瞬间的并发冲击"

缓存雪崩

现象:大量 Key 同时过期,或 Redis 集群宕机,导致全部请求压向数据库。

解决方案

  1. 过期时间加随机偏移:TTL = 基础值 + random(0, 300s),避免大批量 Key 同时过期
  2. 多级缓存:本地 Caffeine + 远程 Redis 双层架构
  3. 降级熔断:Redis 不可用时,降级返回兜底数据或直接限流以保护数据库

七、分布式锁演进:从 SETNX 到 Redisson

vbnet 复制代码
第一代:SETNX + EXPIRE(非原子操作,可能死锁)
  SETNX lock_key value → EXPIRE lock_key 30
  问题:SETNX 成功后、EXPIRE 执行前进程 crash,锁永远不释放,形成死锁
​
第二代:SET key value NX EX 30(原子操作,基本可用)
  SET lock_key uuid NX EX 30
  优点:加锁与设置过期时间一步完成
  问题:(1)锁已过期但业务仍在执行,两个线程同时持有锁
        (2)误删他人的锁:A 的锁过期后 B 获取到锁,A 执行完毕后 DEL 删除了 B 的锁
​
第三代:Redisson 看门狗 + Lua 脚本释放(生产环境推荐)
  看门狗每 10s 检查锁持有状态,自动续期(默认 30s),避免锁在业务完成前过期
  释放时通过 Lua 脚本原子判断:
    if redis.call('get', lock_key) == uuid then
        return redis.call('del', lock_key)
    else return 0 end
  仅自己持有的锁才能被自己删除,杜绝误删
​
第四代:RedLock(多节点分布式锁,存在较大争议)
  向 N 个独立 Redis 节点分别申请锁,超过半数成功则视为加锁成功
  缺点:性能开销大、存在时钟漂移问题、antirez 本人也表示并非银弹
  实际项目:第三代(Redisson)已能满足绝大多数场景

什么时候用 Redis 分布式锁,什么时候不用?

判断标准:锁的持有时间是否跨越多个数据库操作或跨服务。

若在单个数据库事务内(如审核并发),使用数据库乐观锁(version 字段)更合适------复用事务连接,减少外部依赖。若涉及多个独立服务之间的协调,则使用 Redisson 看门狗机制。


八、缓存一致性

数据库更新后,缓存如何保持一致?四种策略递进:

  1. 先删缓存再更新数据库(问题:删缓存后被另一请求读到旧数据并写回缓存)------不推荐
  2. 先更新数据库再删缓存(问题:删缓存失败将导致数据不一致)------基本方案
  3. 先更新数据库再删缓存 + 重试机制(删缓存失败后放入重试队列异步重试)------生产环境常用
  4. 订阅数据库 Binlog(Canal 解析 MySQL binlog 后更新/删除对应 Redis Key)------最终一致性的最佳方案

"不存在绝对的强一致性,只有从弱到强的一致性级别选择。金融场景中'先更新数据库 + 删缓存 + 重试'通常已足够,对一致性要求更高的场景可使用 Canal 监听 binlog。"


九、Redis 7.x 新特性速览

项目使用 6.x 是正常的,但能谈论新版本的变化可以体现技术视野。

Sharded Pub/Sub(7.0):此前 Pub/Sub 消息会广播到集群中的每个节点,网络开销较大。Sharded Pub/Sub 将 Channel 按 slot 分配到不同节点,消息仅在所在节点处理,无需跨节点广播。

ACL v2 (7.0):支持 Channel/Key 级别的细粒度权限控制------ACL SETUSER app_user ~app:* %P~app:* 限制该用户只能访问 app: 前缀的 key 和 Channel。金融场景中可实现交易服务仅访问 trade:* key、客户服务仅访问 customer:* key。

Redis Functions(7.0):Lua 脚本的升级形态------将脚本组织为"库"(library),支持版本管理、按需加载,存储在 RDB/AOF 中,重启后仍然可用。

WAITAOF(7.2):等待指定数量的从节点将写入刷到 AOF 文件后再返回。相比 WAIT(仅等待同步到 replicas 内存),WAITAOF 确保数据已持久化到磁盘,对金融场景的可靠性保障具有直接价值。

"Redis 7.x 的方向 = 更省带宽(Sharded Pub/Sub)+ 更安全(ACL v2)+ 更工程化(Functions 替代 Lua 脚本)。这标志着 Redis 正在从缓存工具向企业级数据平台演进------这是后端工程师应当看到的趋势。"


总结

Redis 的快是四层叠加 的结果------纯内存操作(硬件基础)消除磁盘 IO、单线程模型消除锁竞争和上下文切换开销、IO 多路复用(epoll)支撑高并发连接、精巧数据结构(SDS/ziplist/skiplist)极致优化内存与 CPU。五种数据类型 各有专属底层编码:String 最通用(int/embstr/raw 三级编码)、Hash 适合部分字段更新的对象存储、ZSet 选用 skiplist 而非红黑树的原因是实现更简单、天然支持范围查询、天然支持按 span 排名。过期删除 采用惰性(访问时检查)+ 定期(每秒 10 次随机抽取 20 个)双策略组合,不存在基于定时器的精确删除。持久化 推荐混合模式(RDB 全量 + AOF 增量)加 everysec 刷盘,宕机至多丢失 1 秒数据。高可用 按业务规模逐级演进:主从解决读扩展、哨兵解决自动故障转移、Cluster 解决写扩展(16384 个 Hash Slot 分片)。缓存穿透 用布隆过滤器加缓存空值应对,缓存击穿 靠互斥锁(双重检查)加逻辑过期加永不过期三者递进防护,缓存雪崩 通过过期时间随机化加多级缓存加降级熔断三层兜底。分布式锁从 SETNX+EXPIRE(非原子)演进到 SET NX EX(原子操作)再到 Redisson 看门狗自动续期加 Lua 脚本原子释放,第三代是生产标准,RedLock 因性能开销和时钟漂移问题存在较大争议。


下一篇预告

《MySQL 数据库原理:B+Tree + MVCC + 行锁 + EXPLAIN 实战》 ------数据库是后端开发的另一核心领域。下一篇从 B+Tree 为什么是"矮胖树"讲起,拆解聚簇索引与二级索引的回表代价、MVCC 的 undo log 版本链加 ReadView 快照机制、四种行锁(Record/Gap/Next-Key/Insert Intention),以及 EXPLAIN 的 type+key+rows+Extra 四列驱动的优化实战。

系列说明:本文是《Java 后端核心知识图谱》系列第 2 篇。全系列共 17 篇正刊 + 2 篇番外,覆盖 JVM/并发/MySQL/Redis/消息队列/Spring/分布式/设计模式/分库分表/网络/负载均衡/容器与K8s/微服务治理。

相关推荐
霸道流氓气质1 小时前
CodeBuddy中配置Redis MCP 连接与排错实战
数据库·redis·缓存
数行拙笔1 小时前
Redis---hash类型
数据库·redis·哈希算法
霸道流氓气质3 小时前
Kiro 中配置 Redis MCP Server 完整指南
数据库·redis·缓存
9523618 小时前
Redis - 应用
数据库·redis·分布式·缓存
青山木18 小时前
一把 Redis 分布式锁,踩透四个坑:锁争抢、僵尸锁、锁过期、锁丢失
java·数据库·redis·后端
吴声子夜歌18 小时前
Redis 3.x——客户端
数据库·redis
衣乌安、19 小时前
Redis 分布式锁与任务状态
数据库·redis·分布式
不能只会打代码1 天前
Day 002 — Python 工程化 & FastAPI & 数据库速通
数据库·redis·python·docker·fastapi·测试
吴声子夜歌1 天前
Redis 3.x——事务与Lua
redis·lua