跳至正文
来两杯美式
返回

Redis 知识系列(四):集群与缓存架构

By 来两杯美式
发布于

为什么需要集群

单机 Redis 存在三个天花板:

解决思路:从”主从 + 哨兵”(解决高可用)升级到”集群”(解决扩展性)。


数据分布算法

数据如何分散到多个节点上,决定了集群的能力上限。Redis 经历了三代分布算法:

顺序分区(客户端分片)

数据分散度易倾斜,键值业务相关,可顺序访问。

顺序分区

哈希分布之节点取余

# 客户端 hash(key) % N
node = hash(key) % node_count

优点:实现简单、数据分散度高、键值分布与业务无关。 缺点:无法顺序访问,节点伸缩时大量数据漂移——3 节点扩到 4 节点约 80% 数据迁移。

缓解办法:翻倍扩容(3 → 6 节点),迁移量约 50%。

节点取余扩容示意

哈希分布之一致性哈希

将数据和服务节点都映射到一个 0 ~ 2^32 的闭合哈希环上,key 顺时针找最近节点。

一致性哈希环

问题:加减节点时,相邻节点之间的请求会被全打到下一个节点,造成热点压力

解决:虚拟节点

虚拟节点解决热点

哈希分布之虚拟槽分区(Redis Cluster 采用)

虚拟槽分区

预设 16384 个虚拟槽,每个槽映射一个数据子集,槽数量大于机器节点数。对 key 用 CRC16 哈希后取 16384 余数,落到对应槽,由管理该槽的节点负责数据。

虚拟槽分区的特点

虚拟槽分区的限制


Redis Cluster 官方集群

集群基础:meet

节点 meet 握手

A 向 B 发送 meet,B 回应;A 向 C 发送 meet,C 回应;然后 B 就能找到 C。最终所有节点两两互通

集群节点间通过 Gossip 协议(ping/pong 消息)维持集群视图。每个节点每秒选择若干节点发 ping,pong 响应包含已知节点信息,最终全集群收敛到一致状态。

指派槽

指派槽示意

启动时或运行时通过 CLUSTER ADDSLOTS 给每个 master 节点指派一段槽号。所有槽都被指派后集群进入 online 状态

集群伸缩之扩容

  1. 准备新节点(启动一个空 Redis 实例)。
  2. 新节点加入集群(在新节点执行 CLUSTER MEET <ip> <port> 与任一现有节点握手)。
  3. 新节点是 master 且没有槽。
  4. 从现有节点迁移部分槽到新节点:
    1. 制定迁移计划(哪些槽迁移)。
    2. 逐 key 迁移(MIGRATE 命令),边迁移边接收新写入。
  5. 给新节点添加从节点(高可用)。

扩容-迁移槽数据

集群伸缩之缩容

  1. 下线迁移槽(如果该节点有槽):把槽全部分给其他节点。
  2. 忘记节点:向集群每个节点发 CLUSTER FORGET <node-id>,需要在指定时间内向每个节点头发送通知才有效
  3. 关闭节点:最后一步下线物理节点。

缩容流程

客户端路由:moved

moved 重定向

客户端向任意节点发送命令:

  1. 服务端计算槽和对应节点。
  2. 如果指向自身,直接执行命令
  3. 如果不指向自身,回复 -MOVED <slot> <target-ip>:<port>,客户端重定向到目标节点重新发送命令。

redis-cli -c 已帮我们封装:自动捕获 moved 异常并跳转目标槽节点。

# 计算 hello 的槽值
cluster keyslot hello
# 192.168.0.1:6379> CLUSTER KEYSLOT hello
# (integer) 1346

# 集群模式客户端
redis-cli -c -p 6379

ask 重定向(槽迁移过程中的特殊场景)

moved 是客户端主动重定向并修改目标节点缓存;ask 是客户端只对本次命令重定向,不修改缓存(因为迁移完成后目标节点可能变了)。

smart 客户端(Jedis)

Redis 作者的设计建议:直连目标槽,不要用代理(代理层会成为性能瓶颈)。Jedis 的 smart 客户端设计:

  1. 启动之初连接任意节点。
  2. 发送 CLUSTER SLOTS 命令获取所有槽与节点的映射关系。
  3. 客户端缓存这个映射,直接路由到目标节点。
  4. 监听集群变动(订阅节点的配置变更消息)刷新本地映射。
  5. 批量操作自动按节点分组(见下文优化)。

集群批量操作优化

因为 mget/mset 要求所有 key 在同一节点,所以需要特殊处理:

方案原理网络次数复杂度
串行 m拆分 key 单独执行N简单
串行 IO按节点分组 + Pipeline节点数
并行 IO分组后多线程并发ceil(节点数 / 并发数)复杂
hash_tag包装 key 保证同 slot1简单

hash_tag 用法user:{1001}:profileuser:{1001}:orders 这两个 key 用 {} 包裹同样的内容,Redis 计算槽时只对 {} 内部分哈希,保证落在同一槽。

代理方案对比

方案类型优点缺点
Redis Cluster官方无中心性能高、社区活跃不支持多数据库、批处理受限、运维复杂
Codis(豌豆荚)客户端代理兼容单机 Redis 协议、支持平滑扩容引入代理层、Proxy 单点
Twemproxy(Twitter)代理简单稳定不支持在线扩容、运维复杂

选型建议:新项目直接用 Redis Cluster;老项目需要平滑扩容选 Codis;对延迟极其敏感可以考虑自研 Proxy。


分布式锁

四特性

setnx 实现

SETNX key val     # 设置成功返回 1,失败返回 0

为防止死锁,需要给 key 设置过期时间:

EXPIRE key 60     # 但这两步不是原子的,中间崩溃会死锁

原子解决方案:SET NX EX

SET key val [EX seconds] [PX milliseconds] [NX|XX]

防过期集体失效

缓存集体失效会造成雪崩,给过期时间加随机值

SET lock:order:1234 uuid NX PX 30000
# 30 秒过期 + 随机抖动(如 ±5 秒),避免多 key 同时过期

Redlock 算法(多实例高可用)

针对 Redis 单点故障下 SET NX EX 仍然不够安全的问题(master 挂了还没同步到 slave 时锁会丢失),Antirez 提出 Redlock

  1. 客户端获取当前时间 T1。
  2. 依次向 N 个独立的 Redis 实例请求加锁(设置 NX PX)。
  3. 客户端记录当前时间 T2,计算 T2 - T1 < 锁超时时间 才算加锁成功。
  4. 加锁成功的实际有效时间 = 锁超时时间 - (T2 - T1)。
  5. 如果任一实例加锁失败(或超时),则向所有实例发送解锁命令。

争议:Martin Kleppmann 在《How to do distributed locking》中指出 Redlock 在时钟跳跃、网络分区等场景下并不安全,推荐用 ZooKeeperetcd 等强一致性系统实现分布式锁。

实战选型:业务锁(允许偶尔失效)用 Redis SET NX PX;强一致锁(资金/库存核心扣减)用 ZooKeeper 或 etcd。


缓存的问题点

缓存穿透

问题描述:缓存架构中请求先查缓存,未命中查 DB。如果请求的 key 根本不存在(爬虫、攻击),则每次都不能命中缓存,全部打到 DB。

怎么发现

解决方案

缓存雪崩

描述:缓存层集体宕机或不稳定,或大量 key 同时过期,所有流量打到后端,造成级联故障。

两种典型场景

  1. 缓存宕机雪崩:Redis 主节点挂掉,从节点顶上需要时间,期间所有请求直达 DB。
  2. 集中失效雪崩:大批 key 用了相同 TTL(如整点刷新),到期瞬间全部失效,请求洪峰打向 DB。

解决方案

实战选型:核心交易链路必须多级缓存 + 限流 + 降级三件套齐备,不能依赖单一 Redis。

无底洞

描述:分布式节点越多,单个 mget 操作越慢——客户端需要向多个节点分别获取数据,节点数 N 增长时延迟也跟着增长。

这不是 Redis 的 bug,是分布式系统的固有特性。增加节点只能增加吞吐量,不能降低单次延迟。

优化方向:见上文”集群批量操作优化”章节。

热点 key 重建

描述:某个 key 缓存未命中时,多个线程同时查 DB 并回写缓存,造成 DB 被打爆。

热点 key 重建流程

两种解决方案

方案 1:互斥锁(mutex)

方案 2:逻辑过期

方案 2 的优势:不阻塞用户线程,可用性更高,适合读多写少、允许短暂数据不一致的场景(商品详情、热门资讯等)。

缓存一致性

问题描述:DB 与缓存是两个独立的数据源,双写一致性是分布式系统的经典难题。任何一边更新失败,都会导致数据不一致。

四种典型异常场景(分析读写顺序与失败点):

  1. 更新 DB 成功,更新缓存失败 → 缓存是旧值,读请求拿到旧数据(脏读)。
  2. 更新缓存成功,更新 DB 失败 → 缓存是新值但 DB 没更新,事务回滚后缓存无法回滚,数据错误。
  3. 更新 DB 成功,淘汰(delete)缓存失败 → 同场景 1,缓存仍是旧值。
  4. 淘汰缓存成功,更新 DB 失败 → 缓存没了,下次查询会从 DB 读旧值回写,但至少不会”短时拿到新值后又回滚”,是相对安全的顺序。

关键结论先淘汰缓存、再更新 DB(Cache-Aside 模式)比”先更新 DB、再更新缓存”更安全,因为场景 4 只会导致 cache miss,而不会导致缓存是错误新值。

业界三种主流一致性策略

策略 1:Cache-Aside 旁路缓存(最常用)

读流程

  1. 读缓存,命中直接返回。
  2. 未命中,读 DB。
  3. 把 DB 值回写缓存。

写流程

  1. 先淘汰缓存DEL)。
  2. 再写 DB。
  3. (可选)延迟双删:异步延迟一段时间(如 500ms)再删一次缓存,兜底”主从同步延迟”导致的脏数据。
// 写操作:先删缓存,再写 DB
public void updateUser(User u) {
    redis.del("user:" + u.getId());
    userDao.update(u);
    // 延迟双删:兜底主从延迟
    scheduledExecutor.schedule(
        () -> redis.del("user:" + u.getId()),
        500, TimeUnit.MILLISECONDS);
}

// 读操作:miss 时回写
public User getById(long id) {
    User u = (User) redis.get("user:" + id);
    if (u == null) {
        u = userDao.findById(id);
        if (u != null) {
            redis.setex("user:" + id, 3600, u);
        }
    }
    return u;
}

优缺点:实现简单,业务侵入小;是最终一致性(不是强一致),适合绝大多数读多写少业务。

策略 2:Read/Write Through 读写穿透

把缓存当作主数据源,应用层只跟缓存交互,缓存自己同步写 DB。

需要专门的缓存代理或框架支持(如 Spring Cache + 自定义 CacheLoader),业务代码简单但依赖中间件。

策略 3:Write Behind 异步写回

应用只写缓存,缓存异步批量写 DB(类似 OS 页缓存回写磁盘)。

性能最高(写路径只需更新内存),但一致性最弱——缓存宕机时未刷盘的数据会丢失。适合允许少量数据丢失的场景(日志、点赞数、浏览量等非关键计数)。

实战选型决策树

业务特征推荐策略
通用业务,读多写少Cache-Aside(默认选)
强一致要求(资金/库存)不走缓存,直接 DB + 分布式锁
高频写、可丢少量数据Write Behind(点赞/计数)
想减少业务侵入Read/Write Through(需框架支持)

核心原则没有任何缓存策略能保证强一致。如需强一致(资金扣减、库存),就别用缓存,直接走 DB + 事务。


命令文档

redisdoc.com


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
Redis
第 4 / 4 篇
查看系列全部文章
  1. 01.Redis 知识系列(一):数据结构与单机原理
  2. 02.Redis 知识系列(二):高效使用
  3. 03.Redis 知识系列(三):持久化与高可用
  4. 04.Redis 知识系列(四):集群与缓存架构

上一篇
MySQL 知识系列(七):SQL 调优(一)—— EXPLAIN 执行计划
下一篇
MySQL 知识系列(六):SQL 注入、版本特性与容量评估