最近团队做技术选型评审,又有人问起:都这么多年了,为什么还在用 Memcached?不是都说 Redis 功能更强吗?我当时没有直接回答,而是把线上跑了三年多的 Memcached 集群的监控数据调了出来——单节点 20 万 QPS,CPU 使用率不到 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 万以上的读 QPS,CPU 还有富余。而同样配置下 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 字节的 chunk,slab 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 功能维度对比
| 对比项 | Memcached | Redis |
|---|---|---|
| 数据结构 | 纯 KV,value 只支持字符串 | String、Hash、List、Set、ZSet 等丰富类型 |
| 持久化 | 不支持 | RDB/AOF,支持数据恢复 |
| 多线程 | 支持,多核利用充分 | 6.0 之前主线程单线程,之后引入 IO 多线程但命令执行依然串行 |
| 内存分配 | Slab Allocation,内存利用率高 | Jemalloc,也做了内存池优化 |
| 集群 | 客户端分片为主 | 官方 Cluster 集群,自动分片和故障转移 |
| 复制 | 不支持 | 主从复制,哨兵,集群模式 |
| Lua 脚本 | 不支持 | 支持,可做原子性复杂操作 |
| 性能 | 纯 KV 读场景吞吐更高 | 复杂操作灵活但极端读场景略逊 |
如果把"功能丰富"当成选型的唯一标准,那 Redis 毫无疑问胜出。但缓存场景里,"功能丰富"很多时候是用不到的。你的业务如果只是"读多写少,存字符串",为用不上的功能付出 CPU 和内存成本,并不划算。
3.2 性能与资源占用
我做过一组压测数据对比,条件相同:单机 8 核 16G,value 大小 200 字节,读多写少比例 9:1。Memcached 的 QPS 峰值稳定在 20 万以上,延迟 P99 在 1 毫秒左右;Redis 单实例 QPS 峰值在 10 万上下,P99 在 1.5 毫秒左右。内存占用方面,存储同样数量的 key-value,Memcached 的元数据开销更小,整体省 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_algorithm=fnv_64a每个参数的含义要理解清楚:
-p 11211:监听 TCP 端口,默认 11211-U 0:关闭 UDP 端口。UDP 在放大攻击场景下是风险点,强烈建议关闭-m 8192:Memcached 可用内存,单位是 MB。建议留出一部分系统内存给操作系统本身和文件缓存,不要占满全部物理内存-c 10240:最大并发连接数。连接数不够时客户端会报连接失败,可以根据预估 QPS 和单连接复用情况适当调大-t 8:工作线程数。不是越多越好,一般和 CPU 核数一致即可,超过核数没有意义-B binary:使用二进制协议。文本协议调试方便,生产环境用二进制协议性能和安全性更好-I 4m:单个 item 的最大大小,默认是 1MB。如果业务需要缓存大对象,可以调大-o modern:开启 1.5 版本后的现代内存管理特性-o hash_algorithm=fnv_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'], debug=0) # 写入缓存,过期时间设置为 60 秒 client.set('user:1001', {'name': '张三', 'level': 5}, time=60) # 读取缓存 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/(hits+misses) |
| 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 水位稳定,心里就踏实了一半。