目录
缓存设计思想 底层数据结构 单线程模型 RDB 与 AOF 持久化 主从复制 哨兵 (Sentinel) Cluster 集群分片 缓存一致性
01

缓存设计思想

背景 为什么需要缓存?

在计算机系统中,存储层次是一个金字塔结构:寄存器 > 缓存 > 内存 > SSD > 磁盘。越靠近 CPU 的存储越快,但容量越小、成本越高。缓存设计的核心思想就是利用局部性原理——程序倾向于访问最近访问过的数据(时间局部性)或邻近的数据(空间局部性)。Redis 作为内存数据库,将数据存储在内存中,并提供了持久化、集群、高可用等企业级特性,成为现代应用架构中的缓存标准组件

第一性原理: 缓存的本质是「用空间换时间」——使用更快的存储介质(内存)存储频繁访问的数据,以牺牲存储空间为代价换取访问速度的提升。缓存的命中率直接决定系统的性能表现。缓存设计的核心挑战在于:如何在有限的内存空间内,最大化缓存命中率。这需要合理的淘汰策略(如 LRU、LFU)和预加载策略(如热点数据预热)。

原理 缓存策略 · 淘汰算法 · 缓存问题

常见缓存淘汰策略:

  • LRU (Least Recently Used): 淘汰最近最少使用的数据
  • LFU (Least Frequently Used): 淘汰使用频率最低的数据
  • FIFO (First In First Out): 淘汰最早进入缓存的数据
  • TTL (Time To Live): 数据过期自动淘汰
缓存设计核心要素 缓存金字塔 L1/L2/L3 CPU 缓存 (纳秒级) 内存 (Redis) (微秒级) SSD / 磁盘 (毫秒级) LRU (最近最少使用) 链表 + 哈希表 LFU (最不经常使用) 计数 + 衰减 TTL (过期淘汰) 定时 + 惰性删除 图:缓存金字塔与常用淘汰策略
图:缓存金字塔与常用淘汰策略

缓存问题:

  • 缓存穿透: 查询不存在的数据(如 ID = -1),绕过缓存直接访问数据库
  • 缓存击穿: 热点数据过期,大量请求同时访问数据库
  • 缓存雪崩: 大量缓存同时过期,导致数据库压力激增
▸ Redis 缓存策略配置
# Redis 内存淘汰策略 # maxmemory-policy 可选值 # volatile-lru: 从设置了过期时间的数据中淘汰 LRU # allkeys-lru: 从所有数据中淘汰 LRU # volatile-lfu: 从设置了过期时间的数据中淘汰 LFU # allkeys-lfu: 从所有数据中淘汰 LFU # volatile-ttl: 淘汰过期时间最近的数据 # noeviction: 不淘汰,写满后返回错误 maxmemory 4gb maxmemory-policy allkeys-lru # 设置 TTL (过期时间) SET user:123 "data" EX 3600 # 1 小时后过期 # 缓存穿透解决方案: 布隆过滤器 BF.ADD user_filter 12345 BF.EXISTS user_filter 12345

演进 本地缓存 → 分布式缓存 → 多级缓存

  • 本地缓存 (Local Cache): 应用内缓存(如 Guava、Ehcache),速度快但无法跨应用共享
  • 分布式缓存 (Distributed Cache): Redis、Memcached 等独立缓存服务,支持跨应用共享
  • 多级缓存 (Multi-level Cache): 结合本地缓存 + Redis + CDN,逐级缓存,最大化性能
  • 缓存策略演进: 从简单的 FIFO 到 LRU、LFU,再到自适应策略(如 ARC)
"缓存设计的演进体现了『分层』『智能化』的趋势。从单一的本地缓存到多级缓存,从固定策略到自适应策略,缓存系统越来越复杂,但也越来越高效。Redis 的崛起标志着分布式缓存成为现代应用架构的标准组件。"
—— 缓存设计哲学

取舍 设计中的权衡

📊 命中率 vs 内存占用
更大的缓存空间提升命中率,但占用更多内存。需要根据数据访问特征和成本限制合理设置缓存大小。
⚡ 淘汰策略 vs 实现复杂度
LRU 实现简单但可能淘汰热点数据;LFU 更精准但需要维护计数,实现复杂。Redis 提供了多种策略供选择。
🔧 缓存问题 vs 解决方案
缓存穿透、击穿、雪崩等问题需要针对性解决:布隆过滤器、互斥锁、随机过期时间等。但解决方案会增加复杂度或延迟。
02

底层数据结构

背景 Redis 为什么这么快?

Redis 的高性能不仅来自于内存存储,还来自于精心设计的数据结构。Redis 没有直接使用 C 语言的标准字符串和链表,而是自己实现了SDS (Simple Dynamic String)跳表 (Skip List)字典 (Hash Table) 等结构。这些数据结构针对内存效率操作性能进行了优化,使得 Redis 在读写操作上具有极高的效率。

第一性原理: 数据结构的选择直接决定了操作的时间复杂度和空间效率。Redis 的设计哲学是「针对不同场景选择最合适的数据结构」:字符串用 SDS 实现 O(1) 的获取长度和自动扩容;有序集合用跳表实现 O(log N) 的插入和范围查询;字典用哈希表实现 O(1) 的读写。每种数据结构都有其适用的场景,Redis 通过「多态」的方式根据数据规模自动切换底层实现(如小数据用压缩列表,大数据用字典)。

原理 SDS · 跳表 · 字典 · 压缩列表

SDS (Simple Dynamic String): 改进的字符串实现,记录了字符串长度和剩余空间,支持 O(1) 获取长度和自动扩容。

跳表 (Skip List): 一种多层链表结构,支持 O(log N) 的搜索、插入、删除,底层是双向链表,上层是多级索引。

字典 (Hash Table): Redis 的哈希表实现,使用链地址法解决冲突,支持渐进式 rehash 避免阻塞。

Redis 核心数据结构 SDS 结构 len (长度) free (剩余空间) buf[] (数据) 跳表 (Skip List) Level 3: → → → → Level 2: → → → → Level 1: → → → → O(log N) 搜索 · 插入 · 删除 字典 (Hash Table) Bucket 0 → key → value Bucket 1 → key → value 链地址法 · 渐进式 rehash 图:Redis 核心数据结构,针对内存和性能优化 数据结构 → Redis 对象映射 SDS → String 字典 → Hash 跳表 → ZSet 压缩列表 → List/Hash/ZSet (小数据)
图:Redis 核心数据结构与对象映射关系
▸ Redis 对象系统与数据结构
# Redis 对象结构 (redisObject) typedef struct redisObject { unsigned type:4; // 类型 (string, list, hash, set, zset) unsigned encoding:4; // 编码方式 unsigned lru:24; // LRU 时间 int refcount; // 引用计数 void *ptr; // 指向实际数据结构的指针 } robj; # 编码方式 (encoding) 决定底层结构 # OBJ_ENCODING_INT: 整数 (int) # OBJ_ENCODING_EMBSTR: 短字符串 (SDS) # OBJ_ENCODING_RAW: 长字符串 (SDS) # OBJ_ENCODING_HT: 字典 (hash table) # OBJ_ENCODING_SKIPLIST: 跳表 # OBJ_ENCODING_ZIPLIST: 压缩列表 # OBJ_ENCODING_QUICKLIST: 快速列表 # 查看对象编码 OBJECT ENCODING mykey

演进 压缩列表 → 快速列表 → 列表包

  • 压缩列表 (ziplist): 早期版本用于 List、Hash、ZSet 的小数据存储,内存紧凑但插入删除性能差
  • 快速列表 (quicklist): 压缩列表 + 双向链表,平衡了内存和性能,成为 Redis 3.2+ 的默认 List 实现
  • 列表包 (listpack): 新的紧凑数据结构,比压缩列表更高效,用于替代 ziplist
  • 渐进式 rehash: 字典扩容时逐步迁移,避免阻塞
"Redis 数据结构的演进体现了『内存紧凑性』『操作性能』之间的平衡。从压缩列表到快速列表,Redis 在保持内存效率的同时提升了写操作性能。对象系统的多态设计使得 Redis 可以根据数据规模自动选择最优的底层结构。"
—— 数据结构设计哲学

取舍 设计中的权衡

📦 内存效率 vs 性能
压缩列表内存紧凑但插入删除 O(N);跳表操作 O(log N) 但内存开销更大。Redis 根据数据规模自动选择,小数据用压缩列表,大数据用跳表。
⚡ rehash 阻塞 vs 渐进式
字典扩容如果一次性完成会阻塞服务,渐进式 rehash 将扩容分摊到多次操作,提高了响应性但增加了实现复杂度。
🔗 编码转换开销
数据结构编码转换(如 ziplist → hashtable)需要重新构建,有 CPU 开销。Redis 设置了转换阈值,避免频繁转换。
03

单线程模型

背景 为什么 Redis 使用单线程?

在大多数服务器软件中,多线程是提升并发能力的常用手段。但 Redis 选择了单线程 + 事件驱动的模型。这看似反直觉,但实际上 Redis 的设计目标内存操作,主要瓶颈是网络 I/O,而不是 CPU。

第一性原理: 单线程模型的本质是「用事件驱动避免锁和上下文切换」。Redis 使用 epoll 事件循环机制处理网络连接,所有命令在单线程中按顺序执行。这种设计避免了锁竞争死锁上下文切换等并发问题,使得代码更简单、更稳定。同时,单线程保证了 Redis 命令的原子性——单个命令执行期间不会被其他命令打断。

原理 事件循环 · IO 多路复用 · 文件事件处理器

Redis 事件循环由 文件事件处理器时间事件处理器 组成:

  • 文件事件: 处理网络连接、客户端命令、数据读写等
  • 时间事件: 处理定时任务,如键过期、统计更新等
Redis 单线程事件循环 主线程 (Main Thread) epoll_wait() 等待事件 事件就绪 返回就绪列表 处理命令 执行 · 回复 客户端命令队列 (单线程串行执行) Client 1: SET key value Client 2: INCR counter Client 3: GET mykey Client 4: HMSET hash ... 图:Redis 单线程事件循环,所有命令串行执行,无锁竞争
图:Redis 单线程事件循环,所有命令串行执行,无锁竞争
▸ Redis 事件循环伪代码
// 主循环 (aeMain) void aeMain(aeEventLoop *eventLoop) { while (!eventLoop->stop) { // 处理文件事件(网络 I/O) aeProcessEvents(eventLoop, AE_ALL_EVENTS); } } // 文件事件处理 int aeProcessEvents(aeEventLoop *eventLoop, int flags) { // 调用 epoll_wait 等待事件 int nfds = epoll_wait(eventLoop->epfd, events, maxEvents, timeout); for (int i = 0; i < nfds; ++i) { // 根据事件类型调用对应的处理函数 aeFileEvent *fe = &eventLoop->events[events[i].data.fd]; if (events[i].events & EPOLLIN) { fe->rfileProc(eventLoop, fe->clientData); // 读事件 } if (events[i].events & EPOLLOUT) { fe->wfileProc(eventLoop, fe->clientData); // 写事件 } } return processed; }

演进 单线程 → 多线程 I/O → 多线程命令

  • Redis 1.0-4.0: 纯单线程模型,网络 I/O 和命令执行均在主线程
  • Redis 4.0 (2017): 引入 Lazy Free,使用后台线程处理大键删除
  • Redis 6.0 (2020): 引入 多线程 I/O,网络读取和写入由多个线程处理,命令执行仍在主线程
  • Redis 7.0 (2022): 优化多线程 I/O,减少锁竞争,提升性能
"Redis 单线程模型的核心优势是『简单』『稳定』。随着硬件多核普及,Redis 开始引入多线程 I/O 来充分利用 CPU,但命令执行依然保持单线程,保证了原子性可预测性。这是对『单线程 vs 多线程』的最佳平衡。"
—— 并发模型设计哲学

取舍 设计中的权衡

⚡ 性能 vs 复杂度
单线程模型避免了锁和上下文切换,在高并发场景下性能极佳,但无法利用多核 CPU。多线程 I/O 提高了网络吞吐量,但增加了代码复杂度。
🔒 原子性 vs 并发
单线程保证了每个命令的原子性,适合需要原子操作的场景。但对于耗时命令(如 KEYS *),会阻塞事件循环,需要谨慎使用。
📊 网络 vs CPU 瓶颈
Redis 主要受限于网络 I/O 而非 CPU。多线程 I/O 将网络处理分散到多个核心,但命令执行仍在主线程,适配了 Redis 的实际瓶颈。
04

RDB 与 AOF 持久化

背景 内存数据如何持久化?

Redis 是内存数据库,数据存储在内存中,一旦进程退出或断电,数据就会丢失。为了数据持久化,Redis 提供了 RDB 和 AOF 两种机制:RDB 是将内存数据定期生成快照文件;AOF 是将每条写命令记录到日志文件中。

第一性原理: 持久化的本质是「将内存数据转换为磁盘文件」。RDB 是数据快照,记录了某个时间点的全量数据;AOF 是操作日志,记录了所有修改操作。两者的选择是在数据安全恢复速度性能开销之间的平衡。RDB 恢复快但可能丢失最后一段时间的数据;AOF 数据更安全但恢复慢且文件较大。Redis 4.0 后支持混合持久化,结合了 RDB 和 AOF 的优点。

原理 RDB 快照 · AOF 日志 · 混合持久化

RDB (Redis Database): 通过 fork() 创建子进程,子进程将内存数据写入临时文件,完成后替换旧文件。RDB 文件是一个压缩的二进制文件,加载速度快。

AOF (Append Only File): 每条写命令追加到 AOF 文件中。AOF 通过重写 (rewrite) 机制压缩日志,去除冗余命令。

RDB vs AOF 持久化对比 RDB (快照) 内存数据 → 子进程 fork 写入临时文件 RDB 文件 (dump.rdb) 压缩二进制格式 ✓ 恢复快 (文件小) ✗ 可能丢失数据 ✗ fork 开销大 AOF (日志) 写命令 → 追加到日志 appendonly.aof AOF 重写 (rewrite) 压缩日志 · 去除冗余 ✓ 数据更安全 ✓ 日志可读 ✗ 恢复慢 (文件大) 图:RDB 和 AOF 持久化各有优劣,Redis 4.0+ 支持混合持久化
图:RDB 和 AOF 持久化各有优劣,Redis 4.0+ 支持混合持久化
▸ Redis 持久化配置
# RDB 配置 save 900 1 # 900 秒内至少 1 次修改 → 触发 RDB save 300 10 # 300 秒内至少 10 次修改 → 触发 RDB save 60 10000 # 60 秒内至少 10000 次修改 → 触发 RDB dbfilename dump.rdb dir /var/lib/redis # AOF 配置 appendonly yes appendfilename "appendonly.aof" appendfsync everysec # 每秒同步 (折中方案) # appendfsync always # 每次写入都同步 (最安全) # appendfsync no # 由操作系统决定 (最快) # 混合持久化 (Redis 4.0+) aof-use-rdb-preamble yes # AOF 文件头部使用 RDB 格式

演进 RDB only → AOF only → 混合持久化

  • Redis 1.0 (2009): 仅支持 RDB 持久化,数据安全不足
  • Redis 1.3 (2010): 引入 AOF 持久化,提供更安全的数据保护
  • Redis 4.0 (2017): 引入混合持久化,AOF 文件头部使用 RDB 格式,结合两者的优点
  • Redis 7.0 (2022): 优化 AOF 重写机制,支持无盘复制和 AOF 写时复制
"持久化的演进是 Redis 在『数据安全』『性能』之间不断平衡的过程。从 RDB 到 AOF 再到混合持久化,Redis 提供了越来越灵活的配置选项,让开发者可以根据业务需求选择合适的持久化策略。"
—— 持久化设计哲学

取舍 设计中的权衡

⚡ 性能 vs 安全性
RDB 定期快照性能开销小但可能丢数据;AOF 每秒同步折中性能和安全。需要根据数据重要性选择合适的配置。
📦 文件大小 vs 恢复速度
RDB 文件小,恢复快;AOF 文件大,恢复慢。混合持久化平衡了文件大小和恢复速度。
🔧 同步 vs 异步
AOF 的 appendfsync always 保证最强持久性但性能最低;everysec 是常用折中方案。
05

主从复制

背景 如何实现高可用?

Redis 单机存在单点故障风险。一旦主节点宕机,服务将不可用。主从复制通过将数据复制到多个从节点,实现了数据冗余。从节点可以实时同步主节点的数据,在主节点故障时可以提升为新的主节点,保证服务连续性。

第一性原理: 主从复制的本质是「数据复制 + 状态同步」。主节点负责处理写请求,并将写操作传播到从节点;从节点接收并执行这些操作,保持与主节点的数据一致。复制可以全量同步(初次或断线重连)或增量同步(持续复制)。这种设计将读写分离数据冗余结合,是构建高可用系统的基石。

原理 全量同步 · 增量同步 · 复制积压缓冲区

全量同步: 从节点首次连接或断线重连时,主节点生成 RDB 快照发送给从节点,从节点加载后继续接收增量命令。

增量同步: 主节点将写命令发送到复制积压缓冲区(repl_backlog),从节点定期拉取。如果从节点断开时间较短,可以通过偏移量恢复增量同步。

Redis 主从复制流程 主节点 (Master) 处理写请求 复制积压缓冲区 repl_backlog 从节点 (Slave 1) 处理读请求 从节点 (Slave 2) 处理读请求 增量复制 增量复制 图:主从复制流程,主节点将写操作传播到从节点
图:主从复制流程,主节点将写操作传播到从节点
▸ Redis 主从复制配置
# 主节点配置 (默认) # 从节点配置 replicaof 192.168.1.10 6379 # 指定主节点 slave-read-only yes # 从节点只读 # 查看复制信息 INFO replication # 输出: role:master 或 role:slave # master_repl_offset: 12345 # slave_repl_offset: 12345 # 设置复制积压缓冲区大小 repl-backlog-size 10mb # 手动触发重新同步 SLAVEOF NO ONE # 解除复制,从节点成为主节点 SLAVEOF 192.168.1.20 6379 # 指定新的主节点

演进 SYNC → PSYNC → PSYNC2

  • SYNC (Redis 1.0): 全量同步,断线后必须重新全量同步,效率低
  • PSYNC (Redis 2.8): 部分同步,利用复制偏移量实现断点续传
  • PSYNC2 (Redis 4.0): 支持更灵活的复制策略,减少全量同步次数
  • 无盘复制 (Redis 5.0): 从节点直接接收数据流,无需中间文件
"主从复制的演进是从『全量』『增量』的转变。SYNC 时代断线就要重新全量同步,PSYNC 时代实现了断点续传,大大提高了复制的效率和稳定性。复制积压缓冲区的设计是 PSYNC 的核心,它使得从节点可以在短暂断开后快速恢复。"
—— 复制设计哲学

取舍 设计中的权衡

📊 同步 vs 异步
主从复制默认是异步的,主节点不等待从节点确认,性能高但可能丢失数据。可以配置 WAIT 命令实现一定程度的同步。
⚡ 全量同步 vs 增量同步
全量同步在数据量大时耗时较长,可能阻塞主节点。增量同步可以持续进行,但需要足够的复制积压缓冲区。
🔧 只读 vs 可写
从节点默认只读,保证数据一致性。但某些场景(如缓存)允许从节点可写,需注意数据冲突和同步问题。
06

哨兵 (Sentinel)

背景 主从复制如何实现自动故障转移?

主从复制虽然提供了数据冗余,但故障切换需要人工介入。如果主节点宕机,需要手动将从节点提升为主节点,并重新配置其他从节点。哨兵 (Sentinel) 是一个高可用解决方案,它自动监控 Redis 节点状态,在故障时自动执行故障转移,保证系统的高可用。

第一性原理: 哨兵的本质是「分布式监控 + 自动决策系统」。它通过心跳检测监控节点状态,通过投票机制判断节点是否故障,通过选举算法选择新的主节点。哨兵集群(通常至少 3 个节点)保证了决策的高可用防脑裂。哨兵的设计将高可用的自动化与分布式系统的决策机制结合,极大降低了运维成本。

原理 监控 · 故障检测 · 故障转移 · 通知

哨兵核心功能:

  • 监控: 定期检测主从节点状态,包括响应时间、角色变化
  • 故障检测: 多个哨兵节点共同判断,避免误判
  • 故障转移: 自动将从节点提升为主节点,更新其他从节点的配置
  • 通知: 通过 Pub/Sub 或脚本通知应用系统
哨兵高可用架构 哨兵集群 (Sentinel Cluster) Sentinel 1 Sentinel 2 Sentinel 3 Sentinel N 主节点 (Master) 处理写请求 从节点 (Slave 1) 处理读请求 从节点 (Slave 2) 处理读请求 哨兵监控所有节点,故障时自动切换主节点 故障转移流程: 哨兵选举 → 判定主节点故障 → 选举新主节点 → 更新配置
图:哨兵高可用架构,监控 Redis 节点并自动执行故障转移
▸ 哨兵配置示例
# sentinel.conf port 26379 daemonize yes # 监控主节点 (mymaster: 主节点名称) # 2 表示需要至少 2 个哨兵同意才能判定故障 sentinel monitor mymaster 127.0.0.1 6379 2 # 故障转移超时设置 sentinel down-after-milliseconds mymaster 30000 # 30 秒无响应判定为主观下线 sentinel failover-timeout mymaster 180000 # 故障转移超时 180 秒 # 从节点同步配置 sentinel parallel-syncs mymaster 1 # 同时同步的从节点数 # 启动哨兵 redis-sentinel /etc/redis/sentinel.conf # 查看哨兵状态 redis-cli -p 26379 sentinel masters redis-cli -p 26379 sentinel slaves mymaster

演进 手动切换 → 哨兵 → 哨兵集群

  • 手动切换 (Redis 2.0): 主节点故障需要人工介入,恢复时间较长
  • 哨兵 (Redis 2.8): 自动故障转移,减少人工干预
  • 哨兵集群 (Redis 3.0): 多个哨兵节点消除单点故障,决策更可靠
  • 哨兵 + 集群 (Redis 3.0+): 哨兵监控 Cluster 集群,实现更大规模的高可用
"哨兵是 Redis 高可用体系中的『自动化大脑』。它将故障检测和故障转移的流程标准化、自动化,使得运维人员可以从繁琐的人工切换中解放出来。哨兵集群的设计保证了决策的高可用性和可靠性。"
—— 高可用设计哲学

取舍 设计中的权衡

⚡ 故障检测 vs 误判
哨兵通过主观下线客观下线两个阶段判断故障,避免误判。多个哨兵投票降低了误判概率,但增加了检测延迟。
📊 集群大小 vs 可靠性
哨兵集群至少需要 3 个节点,2 个节点无法形成多数派。更多的哨兵节点提高可靠性,但增加了网络开销和部署成本。
🔧 自动 vs 手动
自动故障转移减少了人工介入,但可能在某些特殊场景下不适用(如计划内维护)。可以通过配置 sentinel failover 命令手动触发故障转移。
07

Cluster 集群分片

背景 单机内存不够用怎么办?

即使使用最高配置的服务器,Redis 单机的内存也有上限(通常几十 GB)。当数据量超过单机内存时,需要数据分片(Sharding)——将数据分布到多个节点上。Redis Cluster 是官方提供的分布式解决方案,它通过哈希槽 (Hash Slot) 将数据均匀分布到多个节点,并提供了高可用自动故障转移能力。

第一性原理: Redis Cluster 的本质是「基于哈希槽的分片 + 节点间通信 + 自动故障转移」。它使用 CRC16 算法将键映射到 16384 个哈希槽,每个节点负责一部分槽位。客户端通过重定向 (Redirect) 机制找到正确的节点。这种设计使得 Redis 可以线性扩展——增加节点只需迁移部分槽位,无需停机。同时,Cluster 内置了主从复制,每个主节点有至少一个从节点,实现了高可用。

原理 哈希槽 · 节点通信 · 重定向 · 故障转移

哈希槽 (Hash Slot): 16384 个槽位,每个键通过 CRC16(key) & 16383 映射到指定槽位。每个节点负责一个连续的槽位范围。

节点通信: 节点间使用 Gossip 协议 交换状态信息,每个节点定期发送 PING 消息给其他节点。

重定向: 客户端请求的键不在当前节点时,节点返回 MOVEDASK 响应,客户端重新定位到正确的节点。

Redis Cluster 架构 Node A 槽位 0-5000 主节点 Node B 槽位 5001-10000 主节点 Node C 槽位 10001-16383 主节点 Node A-slave 从节点 Node B-slave 从节点 Node C-slave 从节点 Gossip 协议: 节点间定期交换状态信息 客户端 → 键哈希 → 定位节点 → 发送请求 图:Redis Cluster 分片架构,主节点负责槽位,从节点负责备份
图:Redis Cluster 分片架构,主节点负责槽位,从节点负责备份
▸ Redis Cluster 操作命令
# 创建集群 (使用 redis-cli) redis-cli --cluster create \ 192.168.1.10:6379 192.168.1.11:6379 192.168.1.12:6379 \ 192.168.1.13:6379 192.168.1.14:6379 192.168.1.15:6379 \ --cluster-replicas 1 # 查看集群信息 redis-cli -h 192.168.1.10 -p 6379 cluster info redis-cli -h 192.168.1.10 -p 6379 cluster nodes # 手动迁移槽位 redis-cli --cluster rebalance 192.168.1.10:6379 # 添加节点 redis-cli --cluster add-node 192.168.1.16:6379 192.168.1.10:6379 # 迁移槽位到新节点 redis-cli --cluster reshard 192.168.1.10:6379 # 删除节点 redis-cli --cluster del-node 192.168.1.16:6379 node_id

演进 客户端分片 → 代理分片 → Redis Cluster

  • 客户端分片 (Client-side Sharding): 应用层实现哈希路由,数据分布简单但需修改代码
  • 代理分片 (Proxy-based): 如 Twemproxy,通过代理层分片,无代码入侵但增加延迟
  • Redis Cluster (Redis 3.0, 2015): 官方内置分片方案,完整的高可用和扩展能力
  • Redis Cluster 改进 (Redis 4.0+): 优化故障转移速度,支持更灵活的节点管理
"Redis Cluster 的推出标志着 Redis 从『单机缓存』『分布式数据库』的跨越。它内置了分片、复制、故障转移等特性,使得 Redis 可以处理 TB 级的数据。Cluster 的哈希槽设计降低了数据迁移的复杂度,Gossip 协议保证了节点间的状态同步。"
—— 分布式设计哲学

取舍 设计中的权衡

📦 16384 槽位 vs 一致性哈希
16384 槽位固定分区,迁移简单但需要预设槽位范围;一致性哈希支持动态扩展但迁移复杂。Redis 选择了固定槽位配合迁移工具。
⚡ 重定向 vs 路由表
客户端通过 MOVED 重定向定位节点,实现简单但客户端需处理重定向。代理方案透明但增加一跳延迟。
🔧 跨槽操作限制
Cluster 不支持跨槽位操作(如 MGET 跨节点),需要客户端拆分或使用 Hash Tag 将相关键放到同一个槽位。
08

缓存一致性

背景 缓存和数据库如何保持一致?

在使用缓存(如 Redis)作为数据库查询层的加速器时,缓存和数据库之间的一致性是一个核心问题。当数据在数据库中更新时,缓存中的数据也需要更新或失效。缓存一致性策略的选择直接影响到系统的数据正确性性能

第一性原理: 缓存一致性的本质是「在多个数据副本之间维护一致性」。缓存是数据库的副本,两者之间存在数据延迟。解决一致性问题的核心思路是「更新数据库后,及时更新或失效缓存」。常见策略包括更新缓存(写后更新)、删除缓存(写后删除)、双写一致性(写数据后同时写缓存)、延迟双删(删除缓存后延迟再删一次)。每种策略都在一致性强度性能开销之间做出权衡。

原理 读写策略 · 双写一致性 · 延迟双删

读策略 (Read Strategy):

  • Cache Aside: 先读缓存,命中则返回;未命中则读数据库,再写入缓存
  • Read Through: 缓存层代为读数据库,应用不直接操作数据库
  • Write Through: 写数据库时同步写缓存

写策略 (Write Strategy):

  • 更新数据库,删除缓存: 最常用策略,先更新数据库,再删除缓存(最终一致性)
  • 更新数据库,更新缓存: 并发更新可能导致数据不一致(较旧数据覆盖新数据)
  • 延迟双删: 先删除缓存,再更新数据库,延迟一段时间后再次删除缓存
缓存一致性策略 Cache Aside 1. 读缓存 → 命中 → 返回 2. 读缓存 → 未命中 → 读数据库 3. 写缓存 (更新) 4. 写数据库 → 删除缓存 延迟双删 (Delay Double Delete) 1. 删除缓存 2. 更新数据库 3. 延迟 N 毫秒 4. 再次删除缓存 读写分离 + 缓存 1. 主库写 → 删除缓存 2. 从库读 → 缓存未命中 3. 从库读数据库 → 写缓存 4. 保证最终一致性 图:三种缓存一致性策略对比 缓存一致性问题 ❌ 缓存穿透: 查询不存在的数据 ❌ 缓存击穿: 热点数据过期 ❌ 缓存雪崩: 大量缓存同时过期 ❌ 数据不一致: 缓存与数据库不同
图:缓存一致性策略与常见问题
▸ 缓存一致性实现示例
# Cache Aside 策略 (伪代码) function get(key) { # 1. 从缓存读取 value = redis.get(key); if (value != null) { return value; } # 2. 缓存未命中,从数据库读取 value = db.get(key); if (value == null) { return null; } # 3. 写入缓存,设置过期时间 redis.set(key, value, 'EX', 3600); return value; } function update(key, new_value) { # 1. 更新数据库 db.update(key, new_value); # 2. 删除缓存 (写后删除策略) redis.del(key); # 3. 延迟双删 (可选) sleep(500); # 等待 500ms redis.del(key); }

演进 更新缓存 → 删除缓存 → 延迟双删 → 异步同步

  • 更新缓存 (早期): 写数据库后立即更新缓存,并发时可能导致数据不一致
  • 删除缓存 (中期): 写数据库后删除缓存,下次读时重新加载,减少不一致窗口
  • 延迟双删 (改进): 删除缓存 → 更新数据库 → 延迟再删,消除并发导致的脏数据
  • 异步同步 (消息队列): 通过消息队列异步更新缓存,实现最终一致性
"缓存一致性的演进是一个『从立即到延迟』的过程。早期追求强一致性,但发现强一致性在高并发场景下难以实现且性能差。最终一致性通过适当的延迟窗口,在一致性和性能之间找到了平衡。延迟双删策略成为业界广泛采用的方案。"
—— 一致性设计哲学

取舍 设计中的权衡

⚖️ 强一致性 vs 最终一致性
强一致性(写后立即同步)保证数据正确但性能差;最终一致性(延迟同步)性能好但数据可能暂时不一致。需要根据业务容忍度选择。
🔧 更新 vs 删除
更新缓存策略在并发时可能覆盖新数据;删除缓存策略让缓存失效后由读操作重新加载,避免了覆盖问题但增加了读延迟。
⏱ 延迟时间设定
延迟双删的延迟时间需要合理设置,太短无法消除并发问题,太长增加数据不一致窗口。通常设置为读操作耗时 + 网络延迟的 2-3 倍。