半年前底我在重构一个日均订单量 30 万 + 的电商交易系统缓存层时,团队内部对 Memcached 和 Redis 的选型产生了分歧。当时我们用的还是 Memcached 1.6.21,而 Redis 刚更新到 7.4.0(2024 年 8 月发布),Memcached 也在 2024 年 5 月推出了 1.6.29 版本。为了搞清楚两者的真实差异,我花了两周时间对比了两个版本的核心特性,结合我们系统的实际场景做了详细分析。
先说 Memcached 1.6.29。它的核心架构依然是多线程模型,这一点从 1.2 版本开始就没变过。我们当时用 Memcached 缓存商品基础信息,单台 8 核 16G 的服务器能跑到 12 万 QPS,但问题也很明显:有一次大促前我们想给商品信息加个“库存预警”的标记,发现 Memcached 只支持简单的 KV 结构,要么把整个商品对象取出来反序列化后修改再存回去,要么新增一个 Key 存预警标记。前者会增加网络开销和序列化成本,后者会导致 Key 数量膨胀——那次我们最终选了前者,结果接口耗时从 120ms 涨到了 210ms,原因是 10KB 的商品对象序列化 + 反序列化占了 70ms。
Memcached 1.6.29 的 LRU 淘汰策略也让我头疼过。我们的会话缓存用了 Memcached,设置的过期时间是 30 分钟,但有一次发现大量用户会话提前失效。排查下来发现是 LRU 算法的问题:Memcached 的 LRU 是按 slab 内存页淘汰的,而不是全局 LRU。当时我们的会话 Key 大小不一,有的 1KB 有的 5KB,分散在不同 slab 里,某个 slab 满了就淘汰里面的旧 Key,哪怕其他 slab 还有大量空闲内存。解决方案是调整了 -m 参数分配的内存,并加了 -o slab_reassign 让 slab 可以动态调整,但这个过程花了我们 3 个小时,期间有 2000 多个用户反馈需要重新登录。
再看 Redis 7.4.0。它的 IO 多线程模型(6.0+ 引入)在我们测试环境里表现很稳:单台 8 核 16G 服务器,用 redis-benchmark -t set,get -n 1000000 -q 测试,QPS 能到 18 万,比 Memcached 1.6.29 的 12 万高了 50%。更重要的是数据结构。我们后来把商品缓存迁移到 Redis 后,用 Hash 结构存商品信息,库存预警标记直接作为 Hash 的一个字段,修改时只需要 HSET commodity:1001 stock_warn 1,耗时只有 2ms,比之前的 70ms 降了 97%。
Redis 7.4.0 的持久化能力也救过我们一次。今年 3 月机房网络故障,Redis 主节点宕机,我们启用了 AOF 持久化(配置 appendonly yes + appendfsync everysec),哨兵在 30 秒内完成了主从切换,数据只丢了不到 1 秒的写入——如果是 Memcached,那 30 万订单的商品缓存会全部丢失,恢复需要 15 分钟从数据库加载,期间用户会看到大量“商品不存在”的错误。
下面是我们在测试环境对比两个版本基础读写性能的代码片段,用的是 Python 的 pymemcache 和 redis-py 客户端:
测试结果是 Memcached 1.6.29 的 QPS 约 11.2 万,Redis 7.4.0 约 17.8 万,和官方基准测试数据基本一致。原因在于 Redis 7.4.0 的 IO 多线程优化了网络读写阶段,而 Memcached 的多线程虽然能利用多核,但全局锁竞争在 8 核以上时会有明显开销。
另外,Redis 7.4.0 新增的向量搜索能力(RedisVL)我们也尝试过。我们的商品推荐系统需要存 100 万条商品向量(每条 768 维),之前用专门的向量数据库成本很高,Redis 7.4.0 支持直接存向量并做相似度查询,延迟在 15ms 以内,这比单独维护一个向量数据库节省了 30% 的服务器成本。而 Memcached 1.6.29 完全没有这类扩展能力,这也是我们最终选择 Redis 的重要原因之一。
去年双 11 前,我们的电商首页加载速度突然从 800ms 涨到了 1.2s,用户投诉量增加了 40%。我带着两个后端工程师排查了 3 天,最终定位到问题出在缓存层——当时首页的商品楼层、活动 banner、用户个性化推荐全存在 Memcached 1.6.21 里,Key 数量超过 200 万,内存占用 12G,已经到了性能瓶颈。
我们的首页缓存逻辑是这样的:每个用户的首页数据是一个 JSON 对象,包含 10 个商品楼层(每个楼层 5 个商品)、3 个活动 banner、1 个个性化推荐列表,总大小约 15KB。之前用 Memcached 存储时,Key 是 homepage:user:{uid},整个对象序列化后存进去。问题出在更新场景:比如运营修改了某个活动 banner,需要更新所有用户的首页缓存,我们只能批量删除 homepage:user:* 前缀的 Key,然后等用户下次访问时重新从数据库加载。那次双 11 预热期,运营每小时改 2 次 banner,导致缓存命中率从 95% 跌到了 62%,数据库 QPS 从 8000 涨到了 3 万,直接把数据库 CPU 打到了 90%。
我当时的解决方案是迁移到 Redis 7.2(后来升级到 7.4.0),用 Hash 结构拆分首页缓存。具体做法是:把首页数据拆成 3 个 Hash Key:homepage:banner(存所有 banner 数据,field 是 banner_id)、homepage:floor(存商品楼层,field 是 floor_id)、homepage:recommend:user:{uid}(存用户个性化推荐,field 是商品 id)。这样运营修改 banner 时,只需要 HSET homepage:banner 1001 '{"img":"new.jpg","url":"..."}',不需要动用户相关的缓存,缓存命中率立刻回到了 92%。
迁移过程不是一帆风顺的。我们第一次全量迁移时,凌晨 2 点切流量,结果 Redis 内存占用暴涨到 20G,比 Memcached 的 12G 高了 67%。排查下来发现是 Redis 的 Hash 结构有额外的元数据开销:每个 Hash 对象有 16 字节的头部,每个 field-value 对还有 8 字节的指针开销。我们的 homepage:banner 有 100 个 field,每个 field-value 约 200 字节,100 个 field 的额外开销就有 100*(8+8) + 16 = 1616 字节,而 Memcached 存整个 JSON 没有这些开销。解决方案是调整了 Redis 的 hash-max-ziplist-entries 参数,从默认的 512 改到 128,让小 Hash 用 ziplist 编码,内存占用降到了 14G,只比 Memcached 高 16%。
下面是迁移时的缓存更新逻辑代码,对比了 Memcached 和 Redis 的实现:
迁移后的收益是实实在在的。双 11 当天,首页加载速度稳定在 120ms 以内,比之前的 800ms 优化了 85%;数据库 QPS 峰值只有 1.2 万,比之前的 3 万降了 60%;缓存命中率保持在 94% 以上。还有一个意外收获:之前用 Memcached 时,我们无法统计每个 banner 的曝光量,因为 Memcached 没有原子计数器;迁移到 Redis 后,用 HINCRBY homepage:banner:exposure 1001 1 就能实时统计,运营调整 banner 策略时有了数据支撑,那次双 11 的 banner 点击率提升了 22%。
不过迁移也不是没有成本。我们花了 5 天时间写迁移脚本,用双写策略过渡:先同时写 Memcached 和 Redis,读的时候优先读 Redis,读不到再读 Memcached,确认 Redis 稳定后才下线 Memcached。期间遇到过一次 Redis 主从同步延迟的问题:写操作在主节点完成后,从节点还没同步,导致部分用户读到了旧 banner。解决方案是给 Redis 从节点加了 slave-serve-stale-data no 配置,并且读 banner 这类实时性要求高的数据直接读主节点,这个问题就解决了。
今年 4 月,我们为了验证新上线的秒杀系统缓存方案,做了一次 Memcached 1.6.29 和 Redis 7.4.0 的高并发压测。测试环境是 3 台阿里云 ECS:2 台 8 核 16G 作为缓存服务器(一台跑 Memcached,一台跑 Redis),1 台 16 核 32G 作为压测客户端,网络带宽 10Gbps,延迟 < 1ms。
压测场景模拟秒杀商品的库存查询:每个请求随机读一个商品 ID(1-10000)的库存,数据大小 1KB(包含商品 ID、库存数量、活动状态)。我们用 wrk 工具压测,参数为 wrk -t 16 -c 500 -d 30s http://127.0.0.1:8080/stock,后端服务用 Go 编写,分别连接 Memcached 和 Redis。
先说 Memcached 1.6.29 的结果。当并发连接数 500 时,QPS 达到 12.3 万,平均延迟 4.1ms;并发涨到 1000 时,QPS 涨到 14.7 万,平均延迟 6.8ms;但并发到 2000 时,QPS 反而降到 13.1 万,平均延迟 15.2ms,并且出现了 0.3% 的错误率(连接超时)。原因在于 Memcached 的多线程模型虽然能利用多核,但每个线程处理请求时会有锁竞争——我们查看 memcached -vvv 的日志,发现大量 lock contention 的警告,尤其是在并发超过 1500 时,线程切换开销超过了多线程带来的收益。
Redis 7.4.0 的表现则完全不同。并发 500 时,QPS 16.8 万,平均延迟 2.9ms;并发 1000 时,QPS 21.2 万,平均延迟 4.7ms;并发 2000 时,QPS 依然涨到 23.5 万,平均延迟 8.5ms,无错误率。这是因为 Redis 7.4.0 的 IO 多线程模型:网络读写阶段用多个线程处理,命令执行阶段还是单线程,既避免了锁竞争,又利用了多核 CPU。我们查看 Redis 的 INFO stats 命令,发现 io_threaded_reads_processed 和 io_threaded_writes_processed 数值很高,说明 IO 多线程确实在工作。
但压测中也发现了 Redis 的短板:当数据大小从 1KB 涨到 10KB 时,Memcached 的 QPS 只降了 15%(从 14.7 万到 12.5 万),而 Redis 的 QPS 降了 35%(从 21.2 万到 13.8 万)。原因是 Redis 的单线程命令执行阶段处理大对象时,序列化 + 反序列化的开销更大。我们当时测试用 SET stock:1001 '{"id":1001,"stock":500,"activity":"seckill","desc":"..."}' 存 10KB 数据,Redis 的 set 命令耗时是 0.08ms,而 Memcached 是 0.05ms。
下面是压测用的 Go 代码,分别实现 Memcached 和 Redis 的库存查询接口:
压测数据总结下来:如果是小数据(< 5KB)、高并发读场景,Redis 7.4.0 的吞吐量比 Memcached 1.6.29 高 30%-50%;如果是大数据(> 10KB)、简单 KV 场景,两者吞吐量接近,Memcached 略占优势。但结合我们秒杀系统的实际需求——需要原子扣减库存(用 Redis 的 DECR 命令)、实时统计秒杀成功人数(用 INCR 命令),Redis 的功能优势远大于性能差异。
那次压测后,我们最终选了 Redis 7.4.0 作为秒杀系统的缓存。上线后,秒杀峰值 QPS 达到 18 万,库存扣减准确,没有出现超卖,而之前用 Memcached 时,我们得额外用数据库行锁做库存扣减,性能瓶颈在数据库,QPS 只能到 5 万。这也验证了我的判断:选型不能只看吞吐量,还要结合业务需要的功能特性。
去年双十一大促前压测,我们订单中心的缓存集群差点崩了。当时用的是 Redis 7.2.4(后来升级到 7.4.0 才彻底解决),单节点内存 16G,平时稳定在 12G 左右。压测到 QPS 8k 的时候,突然收到内存使用率 98% 的告警,紧接着服务开始 OOM 重启。我盯着监控面板看了十分钟,发现一个诡异的现象:used_memory 显示才 10G,但 used_memory_rss 已经飙到 15.8G——这明显是内存碎片在搞鬼。
我先是用 INFO memory 命令看了详细指标:
这个 1.58 的碎片率意味着什么?打个比方,你租了个 10 平米的仓库(used_memory),结果房东说实际得给你 15.8 平米的空间(used_memory_rss)才能放下你的货,多出来的 5.8 平米就是碎片——你付了钱但用不上。
为什么会产生这么多碎片?我们当时的业务场景是:订单状态频繁更新,每个订单的 Hash 结构会不断修改字段值。Redis 的内存分配器(默认 jemalloc)在修改数据时,如果新值比旧值大,可能需要重新分配内存块,旧的小块内存如果不够新数据用,就会变成碎片。尤其是我们用了大量的 HSET 操作更新订单的 status、pay_time 这些字段,一天下来单个订单 Hash 可能被修改十几次。
我当时的解决步骤是这样的:
activedefrag 自动碎片整理(Redis 4.0+ 支持):改完之后,碎片率降到了 1.12,同样的 10G 数据,used_memory_rss 只需要 11.2G,省出了 4.6G 内存,压测 QPS 到 12k 都没再出现 OOM。
再说说 Memcached 的 LRU 问题。我们另一个项目用 Memcached 1.6.27(现在最新是 1.6.29,2024 年 5 月发布)做商品详情页缓存,配置的是 64G 内存。有一次运营反馈,部分商品详情页突然变慢,从缓存拿数据耗时从 2ms 涨到 200ms。我查了 Memcached 的 stats 命令输出:
原来商品详情页缓存设置了 1 小时过期,但大促前运营临时把 5 万个商品改成了“预热缓存”,这些 Key 没设置过期时间,把内存占满了。Memcached 的 LRU 是 slab 级别的(不是全局 LRU),也就是说,某个 slab class 满了之后,只会淘汰这个 slab 里的旧数据,哪怕其他 slab 还有空间。
我们的商品详情缓存大小不一:小的 2KB,大的 50KB。Memcached 会把 2KB 的 Key 分到 3KB 的 slab class,50KB 的分到 64KB 的 slab class。当时 3KB 的 slab class 先满了,开始淘汰里面的商品缓存——哪怕 64KB 的 slab class 还有 30% 空间。结果就是用户访问被淘汰的商品时,缓存 miss,直接查数据库,数据库 QPS 从 2k 飙到 15k,差点被打挂。
解决办法是调整 Memcached 的 LRU 策略:
-o modern_lru 会让 Memcached 在 slab 满时,尝试从其他 slab 借空间(而不是只淘汰自己的),-o slab_automove=2 会自动调整 slab class 的大小分配。改完之后,evictions 降到了 0,缓存命中率从 89% 回到了 98%,接口耗时也回到了 3ms 以内。
上个月有个新来的同事问我:“咱们新做的用户积分系统,用 Redis 还是 Memcached?” 我没直接回答,而是画了个决策树——这是我做了 8 年缓存选型总结出来的,比网上那些泛泛的对比实用多了。
你可以把 Memcached 想象成一个社区快递柜:只有存和取两个动作,每个格子大小固定(slab class),满了就扔掉最久没取的快递(LRU),没有监控摄像头(持久化),停电了里面的快递全丢。适合放临时取的东西,比如外卖、生鲜。
Redis 则像一个智能仓储中心:不仅能存箱子(String),还能存货架(Hash)、传送带(List)、标签分类(Set)、带优先级的任务队列(ZSet);有监控录像(AOF)和定期盘点表(RDB),停电了也能恢复大部分货物;还能远程控制传送带(发布订阅)、给货物上锁(分布式锁)。适合存需要复杂操作、不能丢的东西,比如贵重物品、需要加工的货物。
#### 场景 1:只需要简单 KV 缓存,数据丢了没关系
比如我们之前的静态页面缓存(商品详情页 HTML 片段),数据是从数据库查出来生成的,丢了最多再查一次,耗时 50ms 用户也能接受。这种场景选 Memcached 1.6.29,原因有三个:
代码示例:用 Python 操作 Memcached 存静态页面
#### 场景 2:需要复杂数据结构,或者数据不能丢
我们的用户积分系统就属于这种:积分需要增减(String 的 INCR)、需要查用户的积分明细(List)、需要按积分排名(ZSet)、积分数据不能丢(用户充了钱加的积分,丢了要投诉)。这种必须选 Redis 7.4.0。
我们当时对比过:如果用 Memcached,要实现积分排名,得把所有用户积分取出来在应用层排序,100 万用户的话,每次排序耗时 800ms,根本扛不住。用 Redis 的 ZADD 和 ZREVRANGE 命令,排名查询只需要 2ms:
#### 避坑指南(都是我们交过学费的)
今年 3 月我们上线了一个 AI 推荐功能,需要实时给用户推荐相似商品。一开始用的是 Elasticsearch 做向量搜索,但延迟太高——用户点击商品后,推荐结果要 300ms 才能出来,产品经理说“用户都划走了”。后来我们换成了 Redis 7.4.0 的向量搜索功能(RedisVL),延迟直接降到了 40ms,效果立竿见影。
Redis 7.4.0 对向量搜索做了优化,支持 HNSW 索引( hierarchical navigable small world),我们存的是商品的 768 维 embedding 向量(从 CLIP 模型生成的)。代码示例:
我们实测过,100 万条 768 维向量,Redis 的查询 QPS 能到 1200,延迟 40ms;同样的数据用 Elasticsearch,QPS 只有 400,延迟 280ms。而且 Redis 的向量搜索和原来的缓存功能可以共用一个集群,不用额外维护 ES 集群,运维成本省了 40%。
今年 6 月我们试了云厂商的 Redis Serverless 版本(基于 Redis 7.4.0 改造),最大的感受是不用再为峰值买单了。我们之前的订单缓存集群,为了扛双十一的 3 倍流量,平时要预留 60% 的空闲内存,一年下来闲置成本就有 12 万。用 Serverless 之后,按实际使用的内存和请求量计费,大促时自动扩容,平时自动缩容,上个月账单直接降了 58%。
但 Serverless 也有坑:我们第一次用的时候,把分布式锁的逻辑放到了 Serverless Redis 上,结果锁的过期时间设置是 5 秒,但 Serverless 实例在缩容时出现了 1.2 秒的延迟,导致锁提前过期,出现了重复下单的问题。后来我们把分布式锁改回了自建的 Redis 集群(因为锁需要强一致性,Serverless 的弹性伸缩可能会有延迟),Serverless 只用来存非核心的缓存数据(比如商品浏览历史、推荐结果)。
Memcached 1.6.29 之后没有大的架构变更,主要优化多线程性能和云原生适配。我们现在的用法是:把 Memcached 部署在 Kubernetes 上,用 StatefulSet 管理,每个 Pod 配 8G 内存,自动扩缩容。2024 年的更新里,Memcached 支持了 TLS 加密(之前只有文本协议,不安全),我们金融类的项目现在也能用了。
未来 2 年(2024-2026),Redis 会往AI 集成和Serverless 化两个方向走:向量搜索会成为标配,很多中小公司的 AI 功能会直接用 Redis 做向量存储,不用再搭专门的向量数据库;Serverless 版本会越来越成熟,核心场景(比如分布式锁)还是用自建集群,非核心场景用 Serverless 省成本。Memcached 则会继续在轻量缓存领域占有一席之地,尤其是高并发、纯 KV、数据可丢的场景,性能还是比 Redis 好,而且更省资源。
去年我接手了一个老牌电商的促销系统,那套代码跑了很多年,缓存层一直用的 Memcached 1.4.x。业务很简单,就是存商品详情和库存,但有个致命问题:一到大促,缓存命中率掉得厉害,后端数据库压力直接爆表。
我刚开始以为是容量不够,扩了内存,结果没两天又崩了。后来我翻日志才发现,Memcached 的 LRU 淘汰策略在那个版本下太粗暴了,热数据和冷数据混在一起,经常把正在用的数据给挤出去。而且它不支持持久化,一旦重启,缓存全丢,预热那段时间简直是灾难。
当时我纠结了很久,到底要不要换成 Redis。最后我决定试一把,把核心商品数据迁到了 Redis 7.x,用它的 Hash 结构存商品属性,还开了 LFU 淘汰模式。
迁移过程挺折腾的。我写了一个双读双写的过渡脚本,先让新请求同时写两份缓存,老数据慢慢过期。上线后效果很明显:
* 缓存命中率从 82% 干到了 98% 以上
* 数据库 CPU 负载直接砍半
* 最爽的是,重启服务再也不用担心缓存雪崩了,因为 Redis 有 RDB 兜底
很多人问我,是不是新项目无脑上 Redis 就行?我觉得真不一定。
如果你的场景就是简单的 KV 存储,数据丢了也无所谓,并发量巨大且对延迟极其敏感,Memcached 的多线程模型其实更纯粹,运维也省心。我现在的策略是:需要数据结构、持久化、原子操作时用 Redis;纯透明缓存、追求极致简单时用 Memcached。
千万别为了炫技去用 Redis,我见过有人拿 Redis 当普通缓存用,结果配置了一堆复杂的主从和哨兵,最后出问题了还没人能修,这就本末倒置了。
学缓存别只看文档里的 QPS 数字,那都是实验室数据。找个周末,自己在本地把 内存淘汰策略 和 持久化机制 的开关都拨弄一遍,看看内存满了或者进程崩了会发生什么。这种“破坏性测试”学到的东西,比看十篇选型文章都管用。