首页 / 资讯中心 / 文章详情

Redis Cluster与Memcached架构差异:分布式缓存选型核心解析

Redis Cluster与Memcached架构差异:分布式缓存选型核心解析 ★ FEATURED ARTICLE
做后端架构这些年被问得最多的问题一个是“为什么 Redis 都出 Cluster 了还有人在用 Memcached”另一个是“这俩都是分布式缓存到底差在哪”。这两个问题的答案恰恰藏在它们的架构差异里。Redis Cluster 是服务端主导的分片加高可用方案Memcached 则是把分布式策略完全前置到客户端服务器本身保持极简。再加上 Memcached 的 slab 分配器这种内存管理机制两者的行为模式、运维方式、故障表现完全不同。这篇文章我想从架构设计的取舍出发把 Redis Cluster 和 Memcached 的差异拆开揉碎顺带说说那些文档不会写、但线上一定会遇到的细节。1. 先建立坐标系这两个缓存系统到底在解决什么问题很多人一听到“分布式缓存”下意识就把 Redis 和 Memcached 拉到同一张对比表里然后逐项比较读写速度、线程模型、支持的数据类型。这当然有参考价值但很容易忽略一个前提Redis Cluster 和 Memcached 从诞生那天起就不是同一类物种它们解决的问题域不同优势边界也不同。1.1 Redis Cluster 的定位Redis 本身是单机内存数据结构服务支持字符串、Hash、List、Set、ZSet 等丰富结构并且能做持久化。单机 Redis 能扛的容量取决于物理内存流量一大或者数据量一大就必然要往分布式架构演进。Redis Cluster 正是官方提供的分布式形态通过 16384 个槽位把数据打散到多个主节点上每个主节点又可以挂从节点做冗余节点之间用 Gossip 协议维持对集群状态的认知客户端访问任意节点都能通过重定向拿到正确数据。这个设计决定了 Redis Cluster 不是一个简单的“多个 Redis 拼起来”它引入了槽位路由、集群总线、故障检测与自动故障转移机制本质上是把“协调者”的角色隐式地嵌进了节点自身。好处是没有独立协调组件部署简单坏处是节点数量越多Gossip 消息量越大集群内部的协调成本也在上升。1.2 Memcached 的定位Memcached 要朴素得多它就是一块巨大的分布式内存哈希表value 在最经典的用法里就是一段字节流。服务器端不负责数据分布在哪个节点不维护集群成员关系也没有故障转移的概念。所有分片逻辑都在客户端实现客户端通过一致性哈希算法把 key 映射到某个 Memcached 节点请求直接打过去节点只负责存和取。这种极简架构有非常明显的历史背景。Memcached 进入大众视野时互联网应用正在经历一轮数据库压力爆发大家急需一个“扛得住高并发读取”的纯缓存层而不是一个功能繁重的数据底座。因此它把“分布式复杂性”推给了客户端换来的是服务器端极低的延迟、极稳定的读取表现以及非常好的横向扩展能力——加机器只需要更新客户端配置。1.3 架构差异背后的取舍逻辑理解了定位差异再看两者的架构比较就清晰了。Redis Cluster 更像是一个完整的数据平台它帮用户承担了路由、高可用、部分集群管理职责适合你对数据可靠性有要求、或者业务需要复杂数据结构的场景。Memcached 则像一个随时可以腾出空间的临时仓库它不承诺持久化不在节点间复制数据节点挂了缓存就没了而且这种“消失”很容易被业务接受——因为缓存本来就是可以重建的。这个取舍逻辑非常重要因为很多人选型时会陷入“Redis 功能多所以一定更好”的误区。实际上如果你的业务只需要简单的 key-value 读取且对缓存命中率极度敏感Memcached 的简洁反而能带来更低的运维复杂度和更稳定的性能表现。没有唯一正确的缓存只有符合当前业务阶段和团队能力的缓存。2. 数据分发机制槽位映射与一致性哈希的分水岭分布式缓存核心要解决的一个问题是给定一个 key到底去哪台机器上找数据这个“寻路”机制直接决定了客户端复杂度、扩缩容成本、以及数据迁移方式。Redis Cluster 和 Memcached 在这个问题上走的是两条完全不同的路线。2.1 Redis Cluster槽位映射与服务端路由Redis Cluster 把整个 keyspace 划分为 16384 个哈希槽每个 key 通过 CRC16 算法计算出一个 16 位的值再对 16384 取模得到槽位编号。每个主节点负责一段连续的槽位区间比如三节点集群常见的分配是 0-5460、5461-10922、10923-16383。槽位和数据是绑定的key 永远落在一个固定槽槽位又明确属于某个节点所以服务端能做精确路由。客户端请求任何一个节点时如果 key 对应的槽正好在本节点就直接处理如果不在节点会返回 MOVED 重定向错误并带上目标节点的地址。这里有一个容易踩的坑普通模式下客户端拿到 MOVED 后如果不重新发起请求这次操作就失败了。这也是为什么官方提供了redis-cli -c这种集群模式客户端以及各种语言 SDK 都内置了槽位缓存和重定向跟随逻辑。在集群模式下多个 key 的操作会受到槽位置约束。Redis Cluster 只保证同一槽内的多个 key 能被原子执行跨槽的 multi-key 操作比如MGET、DEL多个 key默认是不允许的除非这些 key 带有相同的 hash tag。hash tag 的做法是key 里用花括号把某一段包起来比如user:{9527}:followersRedis 在计算槽位时只对花括号里的部分做 CRC16。我见过不少团队把这条规则理解成“所有 key 带 hashtag 就行”结果为了跨槽事务把所有 key 塞进同一个花括号最终导致数据倾斜一个节点扛了几乎全部流量。2.2 Memcached客户端分区与一致性哈希Memcached 没有任何服务端路由客户端拿到 key 后先通过哈希函数算出一个值再用一致性哈希环决定这个 key 属于哪台服务器。所谓一致性哈希是把服务器节点映射到一个 0 到 2^32-1 的环形空间中key 的哈希值也落在环上然后顺时针找到第一个服务器节点。相比简单的取模算法一致性哈希最大的优势是当节点增删时只有环上相邻区间的 key 需要重新映射大部分 key 不受影响。实际工程里的实现会比教科书复杂一些。为了均衡负载客户端通常会给每个物理节点生成多个虚拟节点比如 160 个让它们在哈希环上均匀分布否则节点少时容易产生倾斜。开源的 ketama 算法是这个领域的经典实现很多语言和代理层比如 twemproxy都借鉴了它的思路。另有 mcrouter 这类 Facebook 开源的 Memcached 路由代理它把一致性哈希从应用客户端收拢到了代理层这样业务方不用在每个语言里各自维护分片逻辑但也引入了一个新的代理层需要额外部署和保障它的高可用。2.3 集群扩容时两者分别会发生什么Redis Cluster 在线扩容是节奏化的新增节点后通过redis-cli --cluster reshard指定要迁移多少槽位然后数据以槽为单位在不同节点间搬运。移动过程中源节点负责把数据搬运到目标节点并继续服务读请求迁移完成前后节点会通过 ASK 重定向告知客户端去哪个节点访问旧数据或者新数据。整个流程可以做得很平滑但槽位迁移的粒度、并发度都需要精细控制迁移期间网络和 CPU 开销会明显上升。Memcached 扩容则非常直接新节点启动后加入一致性哈希环客户端重新初始化哈希环一部分 key 的映射关系发生变化。这里有个冷酷的现实——Memcached 节点之间不复制数据新增节点后原先落在 A 节点的部分 key 重新映射到了新节点那部分对应的旧缓存会变成“孤儿数据”客户端再访问时会发现 miss需要回源数据库重建缓存。这意味着 Memcached 扩容必然伴随一次小规模的缓存失效如果被牵涉的 key 流量特别大会产生一次缓存击穿压力。团队在做扩容时必须选择一个流量低谷并准备好数据库端的限流与降级策略。3. 内存管理的内功从 Memcached Slab 到 Redis 内存模型内存是缓存系统的命根子。一个缓存节点能存多少数据、会不会被逐出、内存碎片有多严重这些都会直接反映在命中率和高可用性上。Redis Cluster 和 Memcached 的内存管理哲学完全不同其中最值得展开的就是 Memcached 的 slab 分配器。3.1 Memcached 的 Slab Allocator 是怎样运作的很多人第一次听到“slab”这个概念时容易把它理解成一种简单的内存池其实 slab 分配器的设计初衷是解决长时间运行后的内存碎片问题。如果每次 set 一个 value 都用 malloc 分配一块精确大小的内存删除后再 free内存会变得七零八落系统性能会下降。Memcached 的做法是启动时把内存划分为一系列 slab class每个 slab class 管理一组固定大小的内存块不同 class 的块大小按比例递增常见的增长因子是 1.25。比如 slab class 1 的块大小是 96 字节class 2 是 120 字节class 3 是 152 字节依此类推。存入一个 100 字节的 value 时Memcached 会把它放进 120 字节的块里。这种方式大幅减少了内存分配次数和碎片但代价是内存浪费100 字节的数据占用 120 字节块那 20 字节就成了内部碎片。如果你 start 时的-f增长因子设置不当比如设得过大内部碎片率会非常难看。每个 slab class 内部的块会组织成 pagepage 默认大小是 1MB。Memcached 初始化时并不把内存均匀预分给所有 class而是按需分配某个 class 的 page 用完后再向全局内存申请新 page。当一个 class 的内存配额不够时它只能通过 LRU 驱逐旧数据来腾位置不能动用其他 class 的空闲块。这里有个典型问题如果业务 key 的 value 大小分布非常分散大 value 会占据某几个 slab class 的 page小 value 所在的 class 即使还有很多内存余量大 value 对应的 class 也可能会频繁逐出导致命中率骤降。我调过的一个案例里就是因为某个业务把 100KB 的大对象也往 Memcached 里塞导致负责 96KB 以上块大小的 slab class 反复驱逐而其他 class 还有大量空闲缓存命中率掉到不到 50%。3.2 Redis 的内存管理和与众不同的淘汰策略Redis 的内存分配默认依赖 jemalloc和 Memcached 固定块大小分配不同jemalloc 会按实际需要采用多级 size class 分配兼顾减少碎片和降低分配开销。Redis 对整个内存没有一个“预分区”的概念所有 key 共享同一片内存空间这在内存使用弹性上更友好。但 Redis 的内存淘汰机制是全局的当maxmemory达到阈值时会根据配置的淘汰策略挑选 key 逐出。常见的淘汰策略包括 noeviction、allkeys-lru、volatile-lru、allkeys-lfu、volatile-lfu 等。比如 allkeys-lru 会对整个 keyspace 按 LRU 近似算法淘汰而 volatile-lru 只淘汰设置了过期时间的 key。Redis 的 LRU 并不是严格意义上的真实 LRU而是采样近似算法默认采样 5 个 key 挑最久没访问的淘汰通过maxmemory-samples可以调整采样数。采样数越大结果越精确但淘汰时的 CPU 开销也越高生产环境通常不建议盲目调到 10 以上。这项差异在 Redis Cluster 中会被放大由于集群节点的内存管理是独立的全局 maxmemory 策略是每个节点各自生效数据倾斜会导致某个节点先触发淘汰而其他节点还很空闲。因此做容量规划时不能简单用“总数据量除以节点数”要留出足够的倾斜余量。3.3 内存碎片与大 Value 处理Redis 和 Memcached 都有内存碎片问题只是来源不同。Memcached 的 slab 分配器通过固定块来避免外部碎片但引入了内部碎片而且 slab class 之间不能借用内存资源不能被高效整合。Redis 用 jemalloc 做精细分配外部碎片平均更小但遇到大量频繁更新且长度多变的 value 时也会出现碎片率上涨。Redis 4.0 以后支持MEMORY PURGE来整理碎片也有自动碎片整理机制但开启后对峰值 QPS 有一定影响建议在低峰期操作。大 value 对两者都是雷区。Memcached 的默认最大 value 长度是 1MB超过会直接报错需要调整-I参数。Redis 的单个 value 理论上可以到 512MB但这会带来序列化成本、内存拷贝成本和网络传输压力。在分布式架构里一个超大 key 还会导致槽位所在节点负载急剧上升并且一旦集群需要迁移槽位大 key 迁移会拖慢整个 reshard 流程。我的建议是任何缓存系统都别放超过 100KB 的 value 型数据如果业务确实要缓存大文本或大对象先做业务层压缩或者把大对象拆成小数据块只有索引和热数据进缓存。4. 高可用、数据安全与一致性模型如果说前几部分还只是“实现细节差异”那到高可用和一致性就是影响线上事故级别的分水岭了。很多人把 Redis Cluster 当成“高可用缓存”的默认解把 Memcached 当成“临时但不可靠的缓存”这个判断大方向没问题但细节远比这个粗糙结论复杂。4.1 Redis Cluster 的高可用能力与故障转移Redis Cluster 的每个主节点可以配置多个从节点从节点实时复制主节点的数据。当主节点发生故障时集群需要先完成客观下线判定集群中的节点通过 Gossip 消息互相交换状态如果某个节点被大多数持有它槽位的主节点标记为疑似下线才会被确认为客观下线然后触发自动故障转移。自动故障转移的本质是从节点发起选举竞选出新的主节点。这里有几个关键参数cluster-node-timeout控制节点间通信超时默认 15000 毫秒故障转移的开销由延迟和从节点的复制进度共同决定。从节点会优先选择复制偏移量最新也就是数据最接近主节点的节点来晋升。整个设计思路是尽量做到主从切换后不丢数据但要注意如果主节点宕机时数据还没来得及同步给从节点这部分数据就是丢失的。还有一点容易被忽视Redis Cluster 的读写都走主节点从节点默认只做冗余和分担部分只读流量需要通过READONLY命令显式开启读分摊。我见过有些团队把从节点当成“第二个主节点”来抗读结果从节点读到的是稍旧的数据业务把缓存写回和读取的时序理解错了出现奇怪的脏读现象。缓存场景读旧一点往往能接受但你需要明确这一点是有意为之而不是误配。4.2 Memcached 的故障语义缓存失效的代价Memcached 没有主从复制也没有自动故障转移。一个节点宕机它之前承载的 key 全部丢失客户端一致性哈希环上对应的虚拟节点会摘除原先映射到该节点的请求会被重新路由到相邻节点。这会产生两个问题一是相邻节点会突然承受额外的请求压力如果该节点本身已经接近内存上限会发生连锁驱逐进一步拉低整体命中率二是故障节点上的缓存数据需要回源数据库重建流量可能瞬间集中到数据库上引发慢查询甚至宕机。在 Memcached 架构里“故障”不是可选的极端情况而是运维常态。因此业务侧的缓存重建机制必须做得足够健壮。我自己的习惯是回源数据库前先做请求合并和本地短时缓存比如 1 秒内的进程内锁避免同一时刻大量线程同时回源。这个手段在 Redis 单机或者 Cluster 故障转移期间同样适用只是 Memcached 的故障频率通常更高更需要这种兜底方案。4.3 一致性模型与原子操作Redis 是单线程模型所有指令在单节点上是串行执行这一点保证了单个 key 的操作原子性。Redis 还提供 Lua 脚本和事务来保证多个 key 的原子性。但在 Cluster 模式下事务和 Lua 脚本都有一个限制涉及的 key 必须都在同一个槽。这一点前面提过但它是理解 Redis Cluster 一致性的关键。你能保证的强一致性范围是以槽为边界的跨槽的一致性基本靠业务补偿。Memoized 的单个 key 操作在服务器上同样是原子的因为 Memcached 也有类似并发的锁机制。但 Memcached 没有事务也没有 Lua 脚本它给业务提供的就是 get、set、add、replace、append、prepend、cas 这些指令。其中 cas 是一个有实际价值的原子操作它通过版本号标记 value执行 cas 时对比版本号如果版本号变了就失败。这个能力可以用来实现分布式锁或者乐观更新的缓存。比如多个客户端并发更新同一份缓存时用 cas 保证只有一个客户端能写进去避免后写覆盖先写的脏数据问题。不过Memcached 的数据一致性近似“最终可丢”模型它不保证异常情况下的缓存数据仍然存在。所以我一直认为它的定位应该是“可丢失、可重建、可容忍短期不一致”的加速层而不是“数据重要不能丢”的存储层。这个边界想清楚Memcached 其实很安稳。5. 分布式缓存选型的实用清单聊了这么多架构差异最终要落到“我们系统到底该选谁”。选型没有银弹但根据我的实战观察大多数团队纠结的其实不是技术参数而是把业务需求映射到架构能力的匹配度。5.1 用 Redis Cluster 的场景如果你的业务出现下面这些特征Redis Cluster 是更稳妥的选择需要存储的数据结构不仅仅是字符串还涉及 Hash、ZSet、Set、List 等需要分布式环境下做延时队列、发布订阅、分布式锁、排行榜等复杂操作或者业务希望缓存系统具备一定持久化能力哪怕只是周期性 RDB 快照也能帮助冷启动时快速预热。另一个常被忽略的因素是团队运维能力。Redis Cluster 的安装、节点管理、槽位迁移、故障转移调优都需要更细的运维技能和监控颗粒度。如果团队已经有比较成熟的 Redis 运维体系Cluster 带来的复杂度是可控的。比如我用redis-cli --cluster check做过常规巡检用CLUSTER INFO观察集群状态这些工具本身都比较成熟但要做到生产级可靠还必须有内存、延迟、慢日志等监控配合。5.2 用 Memcached 的场景Memcached 更适合的往往是那些“要缓存但不想背太多概念包袱”的场景value 结构简单基本都是序列化后的字符串或二进制只做 get/set不需要事务和 Lua也不需要 Redis 的数据结构。如果你的团队使用的是多语言异构系统想让 Java、Go、Python 服务共享同一套缓存Memcached 的客户端分片反而更轻因为每个语言客户端只需要维护一致性哈希即可。从性能指标看Memcached 在纯读场景下延迟非常稳因为它的线程模型是多线程处理 I/O而且没有持久化和 RDB fork 引起的阻塞不会有 Redis 在 bgsave 时出现的瞬时抖动。另外它的内存管理是 slab 池式分配启动后内存预占明确部署和容量评估都简单。如果你的集群规模不算小而且缓存 access pattern 非常规整Memcached 是一个性价比很高的选择。5.3 两种方案的混用与迁移案例现实中很多系统不会二选一而是把两者组合使用。一个常见组合热点数据、排序类数据、分布式锁等放入 Redis Cluster简单但量大的 key-value 型数据放入 Memcached让两种系统各自发挥优势。过渡层可以先用统一封装类屏蔽底层差异上层只暴露 get/set/delete 接口这样后续要迁移也不至于伤筋动骨。我做过一次从 Memcached 迁移到 Redis Cluster 的改造最大的坑并不是数据搬运而是应用代码的语义差异。Memcached 的 add 是“key 不存在时才成功”Redis 里有SETNXMemcached 的 cas 对应 Redis 的WATCH/MULTI/EXEC或者 Lua 脚本Memcached 对过期时间超过 30 天会称为绝对时间戳Redis 的EXPIRE接受相对秒数。这些语义细节如果不逐个对照梳理线上很容易在迁移后发现某些操作行为不一致。所以要迁移先把客户端封装层做厚再按流速分批发布别指望一把梭。6. 实战中的坑与排查记录架构差异讲得再多都不如记录一些实际踩过的坑来得有用。这几条是我在线上环境真实遇到并排查过的问题希望能帮你少走一段弯路。6.1 Redis Cluster 跨 Slot 操作的坑曾经有个活动系统使用了 Redis Cluster业务团队为了让一次活动页的多个 key 在事务里原子更新给所有 key 加了同一个 hash tag比如act:2024bx:{20240701}:awards。结果活动上线后某个节点 CPU 接近打满其他节点却很空闲。排查后发现所有 key 都被映射到同一个槽位流量和内存全部集中在一个节点上。这个案例的教训是hash tag 是解决跨 key 原子性的工具但使用前必须预判 key 的分布宽度尤其要避免把所有用户相关的 key 都塞进同一个 hashtag。另一个常见问题是客户端未正确跟随 MOVED 重定向。如果使用比较老的客户端版本或者自行实现协议解析时没有处理 ASK 重定向缓存操作会在数据迁移期间频繁失败。定位这种问题很简单开启客户端日志观察是否大量出现MOVED或ASK响应再看看 SDK 版本是否支持集群模式。6.2 Memcached Slab 内存浪费与逐出异常前面提到的 slab 内存不均衡是我见过最多的问题。定位方法其实不难通过stats slabs查看每个 slab class 的内存分配情况通过stats items查看每个 class 的 item 数量和过期情况。如果发现某个 class 的evicted数量持续增加而其他 class 还有空闲 page就能基本确认是 value 大小分布不均导致的局部驱逐。解决办法有几种如果 value 大小本来就非常分散可以考虑将大 value 拆分为多个小 value 存储然后客户端重组避免大对象长期占用某几个 slab class或者在业务层做大小分级不同大小的数据使用不同的缓存实例。还可以调-f增长因子比如从默认 1.25 改成 1.15让 slab class 粒度更细减少内部碎片但这会带来更多 slab class内存管理 overhead 也会增加。6.3 缓存穿透、击穿、雪崩的兜底套路无论选哪一种分布式缓存这三个问题都躲不掉。缓存穿透是指请求一个完全不存在的数据缓存和数据库都没有导致每次请求都打到数据库处理办法是缓存空值并设置短过期时间或者在业务入口做布隆过滤器拦截。缓存击穿是指某个热点 key 过期瞬间大量并发请求同时回源处理办法是加互斥锁同一时刻只允许一个线程回源建缓存。缓存雪崩是指大面积 key 在同一时间段过期或者缓存节点整体故障导致大量请求直冲数据库。对付过期导致的雪崩可以在业务中给缓存过期时间加随机抖动比如基础 TTL 上叠加 1%-5% 随机量让过期时间自然分散。对付节点故障导致的雪崩就得靠部署层面的容灾和限流比如 Redis Cluster 的从节点切换、Memcached 的多节点冗余以及统一接入层的 Rate Limiter。7. 常见问题速查表平时沙龙里总有人问一些反复出现的问题我整理成一个速查表放在这里方便直接对应故障现象和处理思路。现象可能原因排查工具与手段处理建议Redis Cluster 某些节点负载极高槽位分配不均匀或 key 分布倾斜CLUSTER NODES、CLUSTER SLOTS配合热点 key 分析执行 reshard必要时拆分 hashtag 使用范围Redis Cluster 大量 MOVED 响应客户端未正确缓存槽位表或节点变更中开启客户端集群模式检查 SDK 是否自动跟随重定向升级客户端版本或检查连接配置Memcached 命中率突然下降节点重启、扩缩容导致 key 重新映射stats观察命中率stats items查看驱逐情况扩容前评估影响面做好请求合并回源Memcached 某个 slab class 持续驱逐Value 大小分布不均局部内存紧张stats slabs比较各 class 分配情况拆大 value或调低-f增长因子缓存节点重启后大量回源缓存数据丢失、冷启动无数据监控回源量与数据库 QPS提前预热热点 key或并行做本地缓存兜底缓存过期瞬间数据库压力尖峰大量 key 同时过期检查 key 的 TTL 分布过期时间加随机抖动打散过期时刻跨槽 multi-key 操作报错槽位不在同一节点跨槽事务受限检查报错信息和 key 的槽位计算结果使用 hash tag并确认 key 分布仍均匀这个表格不是给你背下来的而是建议收藏一份等线上出问题时再对照。有些问题光看名称很难第一时间想到根因比如 Memcached 命中率下降表象是业务流量上涨实际是 slab class 的内存分配出现了结构性失衡。这类问题需要一定的经验积累但方向对了排查速度能快很多。最后再多说一句个人体会做分布式缓存选型别只看对方的宣传口径也别只盯着 benchmark 数字。Redis Cluster 和 Memcached 的架构差异背后是一整套关于一致性、可用性、运维复杂度、业务形态的取舍。如果你能先把业务偏好看清楚再回头技术选型其实很多纠结都会自己消散。缓存这件事真正重要的不是工具多强大而是你对它的行为语义有多熟悉。
阅读完成 · 觉得有帮助?
咨询建站