
最近团队做技术选型评审又有人问起都这么多年了为什么还在用 Memcached不是都说 Redis 功能更强吗我当时没有直接回答而是把线上跑了三年多的 Memcached 集群的监控数据调了出来——单节点 20 万 QPSCPU 使用率不到 40%内存命中率稳定在 93% 以上没有一次因为缓存层故障引发的线上事故。数据摆在那里讨论自然就变成了另一个问题在什么场景下Memcached 依然是比 Redis 更合适的选择。这篇文章不是 Memcached 的入门教程也不是要论证谁取代谁。我想把这几年的使用经验做一个系统梳理包括内存管理机制、和 Redis 的选型边界、部署参数调优、生产环境踩过的坑以及一套可以直接拿去用的监控和容量规划方法。无论你是在考虑引入缓存组件还是已经在用 Memcached 但总感觉哪里没调明白这篇文章应该都能给你一些参考。1. 为什么在Redis红透半边天的今天Memcached依然值得认真对待很多人对 Memcached 的印象还停留在老古董三个字上。实际上Memcached 从诞生到现在经历了多次重要迭代尤其是 1.5 版本之后引入的现代内存管理机制和 1.6 版本对元数据存储的优化让它的性能表现比很多人的认知要强得多。它不是被 Redis 淘汰的产物而是在特定场景下依然有独特优势的缓存组件。1.1 Memcached到底在解决什么问题Memcached 的核心定位非常纯粹高性能的分布式内存对象缓存系统。它诞生于 LiveJournal 时代当时面临的问题是数据库读写压力过大动态页面生成速度太慢。它的解决方案也极其直接——把热点数据放在内存里用 O(1) 复杂度的哈希查找替代数据库查询让大量读请求直接打在内存上不再穿透到数据库。这种纯粹带来了一系列连锁优势。因为功能简单它的代码路径非常短单次 get 操作的耗时可以低到微秒级别。因为它不需要考虑持久化、复制、数据结构的多样性内存利用率可以做到非常极致。我测试过同样环境下处理纯 KV 读请求Memcached 的吞吐量通常比 Redis 高 30% 到 50%响应时间也更稳定长尾延迟更少。它的适用场景也非常明确热点数据缓存、API 响应缓存、数据库查询结果缓存、分布式会话存储。这些都是数据结构不需要太复杂但访问量极大的场景。如果你只是需要把一些字符串、数字、序列化后的对象存起来读多写少而且能容忍极端情况下的数据丢失Memcached 就是那个最顺手的工具。1.2 多线程模型带来的直观收益Memcached 是真正的多线程模型默认配置下会启动多个工作线程处理网络请求每个线程独立处理自己的连接和请求队列。这个架构决定了它对多核 CPU 的利用效率远高于 Redis 的单线程事件循环模型。实际应用中这个差异非常明显。我的一个业务高峰期集群8 核虚拟机跑 Memcached单实例能抗住 20 万以上的读 QPSCPU 还有富余。而同样配置下 Redis 单实例跑到 10 万 QPS 左右 CPU 就已经接近极限了想要更高吞吐就得引入集群分片增加运维复杂度。多线程模型还带来了一个隐藏的好处慢请求不会阻塞其他请求。单个 key 的访问即使因为网络抖动变慢影响的只是当前线程处理的那一批请求其他线程照常工作。而单线程模型下一旦某个操作变慢整个实例的处理能力都会受影响。这就是为什么在高并发、大流量的场景下Memcached 的尾延迟表现往往更稳定。2. 读懂Memcached的内存管理才能真正的优化它Memcached 的内存管理机制是它性能出色的核心原因但也是很多人配置不当导致内存浪费的根源。它采用了一套叫做 Slab Allocation 的内存管理机制理解这套机制你才能真正理解 Memcached 为什么会表现出某些奇怪的行为比如内存用不满但命中率上不去或者刚启动时性能差、运行久了才稳定。2.1 slab分类与chunk分配机制Memcached 不会像普通程序那样为每个 key 单独向操作系统申请内存。它在启动时会把分配到的内存划分成若干个 slab class每个 slab class 负责管理固定大小的内存块也就是 chunk。比如 slab class 1 管理 96 字节的 chunkslab class 2 管理 120 字节的 chunk以此类推每个 slab class 内的 chunk 大小是固定的不同的 slab class 之间按增长因子递增。当你要存储一个 key 时Memcached 会根据 key 和 value 的总大小加上元数据开销计算出需要放进哪个 slab class然后在该 slab class 的可用 chunk 列表中取一个 chunk 来存。这个设计的好处是避免了频繁的内存申请和释放导致的外部碎片分配效率极高而且不需要频繁和操作系统交互。但坏处也很明显内部碎片浪费。如果你的 value 实际大小是 200 字节而 Memcached 分配给你的 chunk 是 240 字节那 40 字节就浪费了。如果 value 是 100 字节放不进 96 字节的 chunk就会被分配到 120 字节的 chunk 去浪费 20 字节。大量小 value 存进来时这种浪费会积少成多。所以我通常会根据业务 value 的典型大小分布调整增长因子。Memcached 启动参数里的-f就是用来控制这个增长因子的默认是 1.25如果你的 value 大小分布比较均匀可以适当调大减少 chunk 种类过多造成的浪费。2.2 LRU算法在Memcached里和你想的不一样Memcached 的内存淘汰机制是 LRU但它的实现和我们教科书上看到的经典 LRU 不同。经典 LRU 维护一个全局的访问链表每次访问都会移动节点位置。Memcached 的 LRU 是分 slab class 进行的每个 slab class 维护自己的 LRU 队列而且它是分段 LRU分成了 HOT、WARM、COLD 几个段。简要来说新写入的数据进入 HOT 段被访问足够频繁的数据会晋升到 WARM 段而长时间未被访问的数据最终进入 COLD 段。内存不足时Memcached 优先从 COLD 段的尾部淘汰数据。这种分段机制的好处是避免一次全量扫描 LRU 链表的开销同时降低了刚写入没多久的热数据因为一次性批量写入被挤出内存的概率。这就是为什么同样内存容量下Memcached 的命中率在长时间运行后反而会更稳定。2.3 内存分配的经典问题浪费与碎片即便有 slab 机制内存浪费依然存在。最常见的情况是开关-n参数设置不当。-n是每个 key 的最小分配空间默认是 48 字节。如果你的 key 很短value 也很小但把-n调得过大每一个 item 都会强制分配大于实际所需的内存造成大量浪费。另外要注意的是Memcached 的 LRU 淘汰不是内存满了之后才开始。它在内存使用率达到某个水位后会根据 COLD 段的情况提前做驱逐这个水位接近内存上限时新写入的 item 会挤压旧 item。如果你的机器内存比较大但分配给 Memcached 的内存过小命中率就会快速下降这时候加内存参数往往比优化代码更直接有效。3. Memcached和Redis选型一次把不同维度讲透选型不是看哪个工具名气大而是看哪个更适合你当前的业务场景。Memcached 和 Redis 确实重叠度很高但它们的核心定位有明显差异。我用一张表把关键维度列清楚然后逐个说明我的判断标准。3.1 功能维度对比对比项MemcachedRedis数据结构纯 KVvalue 只支持字符串String、Hash、List、Set、ZSet 等丰富类型持久化不支持RDB/AOF支持数据恢复多线程支持多核利用充分6.0 之前主线程单线程之后引入 IO 多线程但命令执行依然串行内存分配Slab Allocation内存利用率高Jemalloc也做了内存池优化集群客户端分片为主官方 Cluster 集群自动分片和故障转移复制不支持主从复制哨兵集群模式Lua 脚本不支持支持可做原子性复杂操作性能纯 KV 读场景吞吐更高复杂操作灵活但极端读场景略逊如果把功能丰富当成选型的唯一标准那 Redis 毫无疑问胜出。但缓存场景里功能丰富很多时候是用不到的。你的业务如果只是读多写少存字符串为用不上的功能付出 CPU 和内存成本并不划算。3.2 性能与资源占用我做过一组压测数据对比条件相同单机 8 核 16Gvalue 大小 200 字节读多写少比例 9:1。Memcached 的 QPS 峰值稳定在 20 万以上延迟 P99 在 1 毫秒左右Redis 单实例 QPS 峰值在 10 万上下P99 在 1.5 毫秒左右。内存占用方面存储同样数量的 key-valueMemcached 的元数据开销更小整体省 20% 到 30% 内存。但这不代表 Memcached 全面领先。如果你的业务需要事务、原子性计数加复杂数据结构操作Redis 的灵活性就是不可替代的。Memcached 提供的原子操作只有 incr/decr 这一种多个 key 之间的事务完全没有。把缓存当成数据库来用的时候Redis 才是正确的选择。3.3 我实际用过的选型判断标准我自己做选型时判断顺序基本是这样的如果只是需要给数据库或接口加一层纯 KV 缓存不要求持久化优先考虑 Memcached。它足够简单、稳定、高效而且部署和运维成本低很多。如果需要缓存的数据结构复杂比如要存哈希、列表、集合或者需要做分布式锁、发布订阅、排行榜直接用 Redis。如果对数据安全性有要求缓存重启后不能全部丢失那只能选 Redis 开启持久化。Memcached 完全不具备这个能力别在这一点上抱有幻想。如果团队规模小、没有专职运维优先选 Redis 官方 Cluster因为 Memcached 的分布式方案目前没有官方标准全靠客户端和中间层实现对团队的架构能力有一定要求。简单来说Memcached 是少即是多的典型代表。它砍掉一切非核心功能把所有资源都聚焦在快这件事上。而 Redis 是工具箱功能全但使用时需要有节制。4. 从部署到压测一套可以直接复用的实操命令与配置这一部分是我实际部署和调优 Memcached 时使用的完整流程。从安装到压测所有命令都验证过可以直接抄作业。4.1 安装与基础配置在 Ubuntu 上安装 Memcached 很简单apt-get update apt-get install -y memcached libmemcached-tools编译安装则更灵活。如果你需要调整编译参数或者想用最新版本建议走源码安装wget https://memcached.org/latest tar -xzf memcached-1.6.x.tar.gz cd memcached-1.6.x ./configure --prefix/usr/local/memcached make make install启动时的核心参数我会重点说明。生产环境我用的启动配置是/usr/local/memcached/bin/memcached \ -p 11211 \ -U 0 \ -u root \ -m 8192 \ -c 10240 \ -t 8 \ -B binary \ -I 4m \ -o modern,hash_algorithmfnv_64a每个参数的含义要理解清楚-p 11211监听 TCP 端口默认 11211-U 0关闭 UDP 端口。UDP 在放大攻击场景下是风险点强烈建议关闭-m 8192Memcached 可用内存单位是 MB。建议留出一部分系统内存给操作系统本身和文件缓存不要占满全部物理内存-c 10240最大并发连接数。连接数不够时客户端会报连接失败可以根据预估 QPS 和单连接复用情况适当调大-t 8工作线程数。不是越多越好一般和 CPU 核数一致即可超过核数没有意义-B binary使用二进制协议。文本协议调试方便生产环境用二进制协议性能和安全性更好-I 4m单个 item 的最大大小默认是 1MB。如果业务需要缓存大对象可以调大-o modern开启 1.5 版本后的现代内存管理特性-o hash_algorithmfnv_64a哈希算法默认就是 FNV这个参数只是显式强调4.2 从命令行到代码客户端十分钟跑通部署完成后先通过命令行验证服务是否正常。用telnet或者nc都可以做简单测试telnet 127.0.0.1 11211连接成功后输入stats如果返回了一大堆STAT开头的指标说明 Memcached 已经正常对外提供服务了。这里可以重点看几个指标uptime运行时间、curr_items当前存储的 key 数量、get_hits和get_misses命中与未命中次数、bytes当前数据占用的字节数。真正的业务接入比如从 Python 读取和写入缓存看下面这段代码就够用了import memcache # 连接 Memcached 集群 client memcache.Client([10.0.0.1:11211, 10.0.0.2:11211], debug0) # 写入缓存过期时间设置为 60 秒 client.set(user:1001, {name: 张三, level: 5}, time60) # 读取缓存 user client.get(user:1001)客户端库会按照 key 的哈希把数据分布到不同的 Memcached 节点上这就是客户端分片。这也是 Memcached 最经典的高可用扩展方式加机器改客户端配置无需重启服务端。4.3 压测的方法论压测 I 不会用 ab 这种通用工具去压 Memcached因为 Memcached 走的是自定义的二进制协议通用 HTTP 压测工具根本用不上。通常我使用memcached-tool它是 libmemcached-tools 的一部分memcached-tool 127.0.0.1:11211 stats这个命令会输出分 slab 的详细统计信息可以看到每一个 slab class 分配了多少 item、存储了多少字节、eviction 了多少次方便定位内存分配不均的问题。如果需要更高并发的压测我推荐memtier_benchmark它是 Redis Labs 开源的压力测试工具也支持 Memcached 的二进制协议。装好之后跑一条命令memtier_benchmark -s 127.0.0.1 -p 11211 -P memcache_binary -c 50 -t 10 -n 100000这条命令会开 50 个连接、10 个线程发 10 万条请求。压测结果能直接看到 QPS、平均延迟、P99 延迟等关键指标。压测时要注意观察服务端的 CPU 使用率确保压测瓶颈在服务端而不是客户端。如果客户端 CPU 先打满了换一台更空闲的机器或者减少客户端线程数再测。5. 生产环境踩坑实录缓存雪崩、热key与数据不一致选型和部署只是开始真正的挑战在线上。我在这部分梳理了几个真实的故障案例每一个都是我和团队在半夜被报警吵醒后才总结出来的教训。5.1 一次缓存雪崩的完整排查链路那次事故的背景是电商平台做促销活动凌晨 0 点开始。当天 23:40 左右我收到监控报警数据库的慢查询数量开始暴涨紧接着接口响应时间直线上升。刚开始以为是数据库出问题了排查后发现数据库 CPU 和连接数都还在正常范围但 QPS 却高得异常。继续排查发现缓存集群的命中率从平时稳定的 93% 掉到了 40% 以下。原因很快浮出水面运营配置的缓存过期时间全部设置在 0 点整活动开始时几千个热点 key 同时过期请求全部穿透到数据库导致数据库压力瞬间增大。后来处理分了三步。第一把所有活动相关的 key 过期时间设置为固定值加随机偏移比如 3600 秒加上 0 到 300 秒的随机数避免同时过期。第二对热点依赖的 key 做逻辑过期兜底数据即使到了过期时间也不物理删除而是由后台异步刷新。第三在数据库访问层加了简单的限流和熔断机制当缓存未命中且数据库压力超阈值时直接返回降级数据而不是无限等待。这套组合拳打完之后活动期间缓存命中率最差也在 85% 以上数据库没有出现过载。5.2 热key导致的内存淘汰问题还有一次是热 key 引发的连锁问题。某个业务 key 的访问量占到整个集群访问量的 40%它所在的 slab class 内存很快被打满LRU 持续驱逐该 class 下的其他 key。结果就是其他业务虽然整体内存没满但数据频繁被热 key 挤掉命中率大跌。解决方式很直接我会把高访问的热 key 分散成多个子 key每个子 key 存储一部分数据或者在 key 后加随机后缀分散到不同节点客户端读取时随机选择其中一个。比如原来读news_detail_1001拆成news_detail_1001_0到news_detail_1001_9十个 key分布在十个节点上热点自然就分摊了。这种方式看似笨拙但效果立竿见影。5.3 数据一致性缓存的更新策略Memcached 没有原生的缓存更新机制缓存里的数据更新完全依赖业务代码。常见的做法有两种先写数据库再删缓存或者先更缓存再写数据库。实操中我推荐先更数据库再删缓存的策略配合短过期时间兜底一致性风险最低。为什么不用先删缓存后更数据库因为存在并发窗口。如果两个请求同时操作一个删缓存一个写数据库很容易出现数据库里是旧数据、缓存里却是新数据的错乱。而先更数据库再删缓存的并发风险相对可控就算删缓存失败还有过期时间兜底。为了减少删缓存的失败概率我会启用自动重试机制删除失败后投递到延迟队列再利用清理任务确保缓存最终被清掉。6. 监控与容量规划别等内存用满才发现问题最后这部分说的是日常维护。Memcached 的监控不像 Redis 那样有 INFO 输出的丰富内置指令但它提供的stats系列命令足够全面关键是你得知道看哪些指标以及这些指标背后的含义。6.1 stats命令全解析在 Telnet 或者 Netcat 连接到 Memcached 后输入stats会返回所有基本统计信息。我重点关注以下指标指标作用健康值参考curr_items当前缓存的 key 数量随业务波动持续增长需要关注total_items累计写入的 key 数量用于计算写入速率get_hits / get_misses缓存命中与未命中次数命中率 hits/(hitsmisses)bytes当前数据占用内存字节数接近 maxbytes 时需要注意evictions因内存不足被淘汰的 key 数量正常应为 0 或极小值expired_unfetched / evicted_unfetched未被访问就已过期/被淘汰的数量过高说明缓存利用率低curr_connections当前连接数接近 maxconn 时排查连接泄漏cmd_get / cmd_set读写命令次数用于计算读写比stats items命令可以查看每个 slab 的详细情况能定位是哪个 slab class 即将耗尽。stats slabs查看每个 slab 的 chunk 使用情况判断是否需要调整增长因子。6.2 容量规划与预估容量预估有一个经验公式可以套用预估 key 数量乘以平均 value 大小再乘以 1.2 到 1.3 的元数据和碎片开销系数就能得到一个基础容量。假设你有 5000 万个 key平均 value 200 字节那基础容量是 5000 万 × 200 字节 10GB乘上开销系数后建议分配 13GB 到 14GB 内存。但这不是唯一维度。还得算 QPS 需要。如果单机预估 QPS 超过 15 万就该考虑切分节点或提前扩集群了。我用的是水位预警机制内存使用率超过 70% 时预警超过 85% 时扩容或者清理无效 key。监控层面CAdv 或者 Prometheus 的memcached_exporter都能方便地采集这些指标。用memcached_exporter配合 Grafana 仪表盘基本可以做到开箱即用的可视化监控。在长期运维 Memcached 的过程中我有一个很深的体会这个组件不是那种用上就完事的工具它的内存分配特征、淘汰策略、线程模型都与实际业务访问模式强相关。你可能需要花时间去观察stats里每一项数据的变化曲线去理解每个 slab class 的分配是否符合预期。但一旦你把它的脾气摸透了它会成为整个链路里最让你省心的那一环。至少对我来说每次排查线上问题从数据库、应用代码一路查过来只要确认 Memcached 集群的命中率正常、各 slab 水位稳定心里就踏实了一半。