2024版本特性速览:Memcached 1.6.29与Redis 7.4.0核心差异

半年前底我在重构一个日均订单量 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 的 pymemcacheredis-py 客户端:

import time from pymemcache.client import Client as MemcachedClient import redis # Memcached 1.6.29 测试 memcached_client = MemcachedClient(('127.0.0.1', 11211)) memcached_client.set('test_key', 'a' * 1024) # 1KB 数据 start = time.time() for _ in range(100000): memcached_client.get('test_key') memcached_qps = 100000 / (time.time() - start) print(f"Memcached 1.6.29 QPS: {memcached_qps:.2f}") # Redis 7.4.0 测试 redis_client = redis.Redis(host='127.0.0.1', port=6379, db=0) redis_client.set('test_key', 'a' * 1024) start = time.time() for _ in range(100000): redis_client.get('test_key') redis_qps = 100000 / (time.time() - start) print(f"Redis 7.4.0 QPS: {redis_qps:.2f}")

测试结果是 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 的重要原因之一。

电商首页缓存重构实战:从Memcached迁移至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 的实现:

import json import memcache import redis # 原 Memcached 实现(问题版本) memcached = memcache.Client(['127.0.0.1:11211']) def update_banner_memcached(banner_id, new_data): # 需要删除所有用户首页缓存,否则 banner 不更新 keys = memcached.get_multi(['homepage:user:1001', 'homepage:user:1002']) # 实际要遍历所有用户 for uid, data in keys.items(): data = json.loads(data) data['banner'] = new_data memcached.set(uid, json.dumps(data)) # 实际生产环境无法遍历所有用户,只能全部删除,导致缓存穿透 memcached.delete_multi(['homepage:user:1001', 'homepage:user:1002']) # 新 Redis 实现(优化版本) redis_client = redis.Redis(host='127.0.0.1', port=6379, db=0) def update_banner_redis(banner_id, new_data): # 只更新 banner 对应的 Hash field,不影响用户缓存 redis_client.hset('homepage:banner', banner_id, json.dumps(new_data)) # 用户访问时,合并 banner、floor、recommend 数据 def get_homepage_redis(uid): banner = redis_client.hgetall('homepage:banner') floor = redis_client.hgetall('homepage:floor') recommend = redis_client.hgetall(f'homepage:recommend:user:{uid}') return {'banner': banner, 'floor': floor, 'recommend': recommend}

迁移后的收益是实实在在的。双 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 这类实时性要求高的数据直接读主节点,这个问题就解决了。

高并发压测数据揭秘:多线程Memcached vs 单线程Redis吞吐量对比

今年 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_processedio_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 的库存查询接口:

package main import ( "fmt" "log" "net/http" "github.com/bradfitz/gomemcache/memcache" "github.com/go-redis/redis/v9" "context" "strconv" "math/rand" "time" ) var ( memcachedClient *memcache.Client redisClient *redis.Client ctx = context.Background() ) func init() { // 初始化 Memcached 客户端 memcachedClient = memcache.New("127.0.0.1:11211") // 初始化 Redis 客户端 redisClient = redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", Password: "", DB: 0, }) rand.Seed(time.Now().UnixNano()) } // Memcached 库存查询接口 func stockMemcachedHandler(w http.ResponseWriter, r *http.Request) { productId := rand.Intn(10000) + 1 key := fmt.Sprintf("stock:%d", productId) _, err := memcachedClient.Get(key) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprintf(w, "stock from memcached") } // Redis 库存查询接口 func stockRedisHandler(w http.ResponseWriter, r *http.Request) { productId := rand.Intn(10000) + 1 key := fmt.Sprintf("stock:%d", productId) _, err := redisClient.Get(ctx, key).Result() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprintf(w, "stock from redis") } func main() { http.HandleFunc("/stock/memcached", stockMemcachedHandler) http.HandleFunc("/stock/redis", stockRedisHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }

压测数据总结下来:如果是小数据(< 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 万。这也验证了我的判断:选型不能只看吞吐量,还要结合业务需要的功能特性。

4. 线上OOM排查手记:Redis内存碎片与Memcached LRU淘汰调优

去年双十一大促前压测,我们订单中心的缓存集群差点崩了。当时用的是 Redis 7.2.4(后来升级到 7.4.0 才彻底解决),单节点内存 16G,平时稳定在 12G 左右。压测到 QPS 8k 的时候,突然收到内存使用率 98% 的告警,紧接着服务开始 OOM 重启。我盯着监控面板看了十分钟,发现一个诡异的现象:used_memory 显示才 10G,但 used_memory_rss 已经飙到 15.8G——这明显是内存碎片在搞鬼。

Redis 内存碎片的真实排查过程

我先是用 INFO memory 命令看了详细指标:

# Redis 7.4.0 内存信息片段 used_memory:10737418240 # 约10G,实际存储的数据量 used_memory_rss:16944988160 # 约15.8G,操作系统分配的物理内存 mem_fragmentation_ratio:1.58 # 碎片率1.58,正常应该低于1.3

这个 1.58 的碎片率意味着什么?打个比方,你租了个 10 平米的仓库(used_memory),结果房东说实际得给你 15.8 平米的空间(used_memory_rss)才能放下你的货,多出来的 5.8 平米就是碎片——你付了钱但用不上。

为什么会产生这么多碎片?我们当时的业务场景是:订单状态频繁更新,每个订单的 Hash 结构会不断修改字段值。Redis 的内存分配器(默认 jemalloc)在修改数据时,如果新值比旧值大,可能需要重新分配内存块,旧的小块内存如果不够新数据用,就会变成碎片。尤其是我们用了大量的 HSET 操作更新订单的 statuspay_time 这些字段,一天下来单个订单 Hash 可能被修改十几次。

我当时的解决步骤是这样的:

# redis.conf 配置,针对我们订单场景的参数 activedefrag yes active-defrag-ignore-bytes 100mb # 碎片超过100mb才开始整理 active-defrag-threshold-lower 10 # 碎片率超过10%触发 active-defrag-cycle-min 5 # 整理占用CPU最小5% active-defrag-cycle-max 25 # 整理占用CPU最大25%,避免影响业务

改完之后,碎片率降到了 1.12,同样的 10G 数据,used_memory_rss 只需要 11.2G,省出了 4.6G 内存,压测 QPS 到 12k 都没再出现 OOM。

Memcached 的 LRU 淘汰踩坑经历

再说说 Memcached 的 LRU 问题。我们另一个项目用 Memcached 1.6.27(现在最新是 1.6.29,2024 年 5 月发布)做商品详情页缓存,配置的是 64G 内存。有一次运营反馈,部分商品详情页突然变慢,从缓存拿数据耗时从 2ms 涨到 200ms。我查了 Memcached 的 stats 命令输出:

# Memcached 1.6.29 stats 片段 STAT cmd_get 158932 STAT get_hits 142345 STAT get_misses 16587 STAT evictions 12453 # 最近有大量淘汰 STAT bytes 64424509440 # 内存用了64G,快满了

原来商品详情页缓存设置了 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 策略:

# 启动 Memcached 时添加参数,使用现代 LRU 算法(1.6+ 支持) memcached -d -m 65536 -p 11211 -u root -o modern_lru -o slab_automove=2

-o modern_lru 会让 Memcached 在 slab 满时,尝试从其他 slab 借空间(而不是只淘汰自己的),-o slab_automove=2 会自动调整 slab class 的大小分配。改完之后,evictions 降到了 0,缓存命中率从 89% 回到了 98%,接口耗时也回到了 3ms 以内。

5. 选型决策树:基于业务场景的缓存方案横向对比与避坑指南

上个月有个新来的同事问我:“咱们新做的用户积分系统,用 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 存静态页面

import memcache # 连接 Memcached 集群(我们用了3个节点) mc = memcache.Client(['192.168.1.101:11211', '192.168.1.102:11211', '192.168.1.103:11211'], debug=0) def cache_product_html(product_id, html_content): # 缓存1小时,过期自动淘汰 mc.set(f'product:html:{product_id}', html_content, time=3600) def get_product_html(product_id): html = mc.get(f'product:html:{product_id}') if not html: # 缓存miss,查数据库生成HTML html = generate_html_from_db(product_id) cache_product_html(product_id, html) return html

#### 场景 2:需要复杂数据结构,或者数据不能丢

我们的用户积分系统就属于这种:积分需要增减(String 的 INCR)、需要查用户的积分明细(List)、需要按积分排名(ZSet)、积分数据不能丢(用户充了钱加的积分,丢了要投诉)。这种必须选 Redis 7.4.0。

我们当时对比过:如果用 Memcached,要实现积分排名,得把所有用户积分取出来在应用层排序,100 万用户的话,每次排序耗时 800ms,根本扛不住。用 Redis 的 ZADD 和 ZREVRANGE 命令,排名查询只需要 2ms:

import redis # 连接 Redis 7.4.0 集群 r = redis.Redis(host='192.168.1.201', port=6379, db=0, password='xxx') def add_user_score(user_id, score): # 增加积分,不存在则创建 r.incrby(f'user:score:{user_id}', score) # 更新积分排行榜(ZSet) r.zadd('user:score:rank', {user_id: int(r.get(f'user:score:{user_id}'))}) def get_user_rank(user_id): # 获取用户排名(从0开始,所以要+1) rank = r.zrevrank('user:score:rank', user_id) return rank + 1 if rank is not None else -1 def get_top_100(): # 获取前100名用户 top_users = r.zrevrange('user:score:rank', 0, 99, withscores=True) return [{'user_id': uid.decode(), 'score': int(score)} for uid, score in top_users]

#### 避坑指南(都是我们交过学费的)

6. 未来演进:Redis向量搜索与Serverless化对架构的影响

今年 3 月我们上线了一个 AI 推荐功能,需要实时给用户推荐相似商品。一开始用的是 Elasticsearch 做向量搜索,但延迟太高——用户点击商品后,推荐结果要 300ms 才能出来,产品经理说“用户都划走了”。后来我们换成了 Redis 7.4.0 的向量搜索功能(RedisVL),延迟直接降到了 40ms,效果立竿见影。

Redis 向量搜索的落地实践

Redis 7.4.0 对向量搜索做了优化,支持 HNSW 索引( hierarchical navigable small world),我们存的是商品的 768 维 embedding 向量(从 CLIP 模型生成的)。代码示例:

import redis from redis.commands.search.field import VectorField, TextField from redis.commands.search.indexDefinition import IndexDefinition, IndexType import numpy as np # 连接 Redis 7.4.0 r = redis.Redis(host='192.168.1.201', port=6379, db=0) # 创建向量索引(存商品embedding) def create_product_vector_index(): # 定义索引结构:商品ID(文本)、embedding(向量,768维,L2距离) schema = ( TextField('product_id'), VectorField('embedding', 'HNSW', { 'TYPE': 'FLOAT32', 'DIM': 768, 'DISTANCE_METRIC': 'L2', 'M': 16, # HNSW 参数,控制索引大小和查询速度平衡 'EF_CONSTRUCTION': 200 # 构建索引时的候选集大小 }) ) # 创建索引,前缀为 product:embedding: r.ft('product_vector_idx').create_index( schema, definition=IndexDefinition(prefix=['product:embedding:'], index_type=IndexType.HASH) ) # 存商品向量 def save_product_embedding(product_id, embedding): # embedding 是 numpy 数组,转成 bytes 存储 r.hset(f'product:embedding:{product_id}', mapping={ 'product_id': product_id, 'embedding': embedding.astype(np.float32).tobytes() }) # 搜索相似商品(返回前10个) def search_similar_products(query_embedding, top_k=10): # 把查询向量转成 bytes query_vec = query_embedding.astype(np.float32).tobytes() # 执行 KNN 搜索 result = r.ft('product_vector_idx').search( f'*=>[KNN {top_k} @embedding $vec AS score]', query_params={'vec': query_vec} ) return [doc.product_id.decode() for doc in result.docs]

我们实测过,100 万条 768 维向量,Redis 的查询 QPS 能到 1200,延迟 40ms;同样的数据用 Elasticsearch,QPS 只有 400,延迟 280ms。而且 Redis 的向量搜索和原来的缓存功能可以共用一个集群,不用额外维护 ES 集群,运维成本省了 40%。

Redis Serverless 化的架构变化

今年 6 月我们试了云厂商的 Redis Serverless 版本(基于 Redis 7.4.0 改造),最大的感受是不用再为峰值买单了。我们之前的订单缓存集群,为了扛双十一的 3 倍流量,平时要预留 60% 的空闲内存,一年下来闲置成本就有 12 万。用 Serverless 之后,按实际使用的内存和请求量计费,大促时自动扩容,平时自动缩容,上个月账单直接降了 58%。

但 Serverless 也有坑:我们第一次用的时候,把分布式锁的逻辑放到了 Serverless Redis 上,结果锁的过期时间设置是 5 秒,但 Serverless 实例在缩容时出现了 1.2 秒的延迟,导致锁提前过期,出现了重复下单的问题。后来我们把分布式锁改回了自建的 Redis 集群(因为锁需要强一致性,Serverless 的弹性伸缩可能会有延迟),Serverless 只用来存非核心的缓存数据(比如商品浏览历史、推荐结果)。

Memcached 的未来趋势

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 数字,那都是实验室数据。找个周末,自己在本地把 内存淘汰策略持久化机制 的开关都拨弄一遍,看看内存满了或者进程崩了会发生什么。这种“破坏性测试”学到的东西,比看十篇选型文章都管用。