ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redis与Memcached全面对比:从底层原理到选型实战

Redis与Memcached全面对比:从底层原理到选型实战 先说明一句这题我经常在技术群和面试场景里被问到干脆把两年的实践体会整理成一篇从底层原理到选型经验都过一遍给正在纠结缓存选型的同学做个参考。1. 从出身看定位一个是纯内存搬运工一个是数据结构服务器1.1 Memcached的诞生当年只为扛住数据库压力这一件事Memcached 最早是 2003 年为 LiveJournal 开发的当时社区网站流量一上来数据库瞬间就被打满了开发者的诉求非常朴素把热数据从数据库里搬出来放到内存里用简单的键值对方式读写。所以它的设计目标从一开始就极度单一——能塞就塞读得快别管别的。这就决定了它骨子里的三个特性第一只有 KV 结构value 就是一串字节第二纯内存运行没有持久化机制第三服务端只负责读写不关心节点状态节点挂了应用层自己想办法。这不是缺陷这是它的设计边界。1.2 Redis的诞生把数据结构本身当核心卖点Redis 是 2009 年由 Salvatore Sanfilippo网名 antirez开发的他最初做实时统计分析发现单纯 KV 不够用比如需要排序、需要队列、需要集合去重所以他直接实现了 String、Hash、List、Set、ZSet 等数据结构并起名 Redis REmoteDIctionaryService远程字典服务。名字其实已经点明了它不只是一个缓存更像是一个跑在内存里的数据结构服务器。后来又从存储扩展到 Lua 脚本、发布订阅、流、地理位置等逐渐长成了一个全家桶。但注意——它依然鼓励你把它当缓存用只是它比 Memcached 多了太多可玩的空间。1.3 一句话理解核心差异用生活化类比Memcached 像一个随存随取的储物柜你把东西放进去、取出来柜子本身不记任何额外的账Redis 像一个带分类标签的收纳房你不仅能把东西存进去还能按类别操作、排序、统计、组合甚至能安排哪些东西先扔。想清楚这一层后面对比才能不跑偏Memcached 解决热点查找这一个问题Redis 解决数据结构操作 缓存 更多附加能力这一揽子问题。2. 数据形态的碾压能存什么决定了你能做什么2.1 Memcached的value限制1MB这个门槛很多人不知道Memcached 存储的最基本单位就是键值对key 是一个字符串value 是一个字节串。因为内部实现把 value 保存在 slab 划分的固定长度 chunk 里所以单个 value 被限制在1MB以内。实际使用中如果你往里面塞一张大图或者一个超大的对象字符串会直接返回错误。这意味着它只适合存小体积热点数据用户 session、验证码、热点文章标题列表、计数器。想做复杂查询没有想存结构化数据你得在应用层先序列化成一个 JSON 字符串或二进制整体写进去再用的时候整体读出来自己解析。2.2 Redis的数据结构超市五种核心类型分别能做什么Redis 的存在让缓存还能这么写成为可能。以下是五种基础类型的实战用途我按使用频率排序String最简单的 KV除了常规缓存还能用 INCR/DECR 做计数器、用 SET NX EX 做分布式锁、用 SETEX 做限流窗口。Hash适合存对象。一个用户信息不再是user_1001: {name:xxx}这种序列化整体写入而是直接HSET user:1001 name zhangsan age 30改一个字段就 HSET 一个字段不需要读整个对象再写回这在更新热点字段的场景下能少很多序列化开销。List底层是链表LPUSH BRPOP 组合起来就是一套极简消息队列也可以用 LRANGE 做分页列表。Set天生支持去重SADD/SISMEMBER 可以做“用户是否参与过活动”这种判断SINTER 可以做共同好友。ZSet有序集合每个元素带一个 score按分数排序。排行榜、延时队列score 存时间戳、滑动窗口计数都能基于它实现。后面还有 Bitmaps用户签到位图、HyperLogLog基数统计比如 UV 去重、Geo地理位置、Stream更完整的消息队列每一个都是 Memcached 里不存在的概念。2.3 同样的用户信息缓存需求两种写法的对比假设要缓存一个用户对象id1001namezhangsanage30。Memcached 的写法你在应用层要先把对象序列化import json, memcache mc memcache.Client([127.0.0.1:11211]) user {id: 1001, name: zhangsan, age: 30} mc.set(user:1001, json.dumps(user)) # 改年龄时要整体读出来再写回去 user json.loads(mc.get(user:1001)) user[age] 31 mc.set(user:1001, json.dumps(user))Redis 用 Hash 的写法import redis r redis.Redis(host127.0.0.1, port6379) r.hset(user:1001, mapping{id: 1001, name: zhangsan, age: 30}) # 只想改年龄 r.hset(user:1001, age, 31)区别不只是几行代码的事。在网络传输层面Memcached 每次都是整个对象往返Redis 只需要传输发生变化的字段在并发场景下多个线程同时改不同字段时Memcached 的读-改-写三步很容易产生相互覆盖而 Redis 的 HSET 是单字段原子操作。这个差异我实测感受很深缓存对象越大、更新越频繁两种方案的差距越明显。3. 宕机之后见真章数据要不要活下来3.1 Memcached的立场我是缓存你凭什么要求我不丢Memcached 没有任何持久化机制进程一退出内存里的数据全部清空。这不是疏忽是明确的设计取舍。缓存的意义在于让热数据离 CPU 近一些如果每次写入都要同步落盘那性能就退回到数据库的层级了。所以用它的时候业务上必须能接受重启丢数据——session 全失、计数器清零、热点缓存重建这些后果团队提前都得想清楚。3.2 Redis的持久化三件套RDB、AOF和混合持久化Redis 的持久化能力让它从纯缓存跃升为一个可以被认真对待的存储组件。主要机制有两个RDB快照按配置时间间隔比如 900 秒内 1 次变更、300 秒内 10 次变更、60 秒内 10000 次变更把内存全量数据 dump 到 .rdb 文件。它用 fork 子进程 写时复制Copy On Write实现主进程不用阻塞式地把所有数据写到磁盘子进程自己慢慢写。优点是恢复快、文件紧凑缺点是两次快照之间的数据会丢。AOF追加日志把每次写命令追加到 .aof 文件类似 MySQL 的 binlog。通过配置 appendfsync 决定刷盘策略always每条命令都 fsync最安全也最慢、everysec每秒刷一次默认最多丢一秒数据、no交给操作系统决定。AOF 的优点是数据丢失窗口小缺点是文件体积会膨胀恢复速度也比 RDB 慢。Redis 4.0 之后有了混合持久化在 AOF 文件里把某一时刻的 RDB 快照作为 base后续增量命令用 AOF 格式追加重启时先加载快照部分再重放增量部分。Redis 7.0 又对 AOF 做了多部分重构把基础快照和增量日志拆成多个文件分开管理避免了大文件重写时的复杂操作。我建议在生产环境直接开aof-use-rdb-preamble yes兼顾恢复速度和数据安全。3.3 持久化不是免费的fork阻塞、AOF膨胀、以及取舍经验很多教程会把持久化讲得很美好但实际运行中它是有代价的fork 阻塞RDB 触发 bgsave 时主进程要 fork 子进程。如果内存实例很大比如 10GBfork 一瞬间的耗时可能达到几百毫秒甚至秒级这段时间内 Redis 对外是完全阻塞的。有种观点说bgsave 不阻塞严格说是不对fork 阶段一定短暂阻塞只是时间通常很短。想减小这个窗口可以调小内存占用、提前在低峰期触发。AOF 文件膨胀长期运行不重写AOF 文件可能从几 GB 长到几十 GB。Redis 有自动重写机制bgrewriteaof但重写也需要 fork同样有阻塞风险。磁盘IO争抢在生产服务器上Redis 和数据库共用磁盘时持久化产生的 IO 压力可能反过来拖慢主业务。有条件还是单独挂盘。我在实际运维中的做法是如果 Redis 只当纯缓存关掉 AOF靠主从节点做数据冗余就够了如果 Redis 存了不能丢的业务数据比如订单状态、限流标记就开 AOF everysec RDB 兜底同时监控 fork 耗时指标。4. 单线程 vs 多线程别再被多线程性能更好带偏4.1 为什么 Redis 6.0 之前坚持单线程也这么快很多人一听到单线程就觉得性能差这是个误区。Redis 的所有命令执行阶段是一个单线程的 Reactor 模型核心在网络事件处理上用了IO 多路复用epoll 等机制单线程可以同时监听成千上万个客户端连接哪个连接有请求就处理哪个。这个模式下几乎没有线程上下文切换和锁竞争的开销内存操作本身又是微秒级所以瓶颈根本不在 CPU 上而在网络 IO 和内存带宽。还有一个被很多人忽视的红利单线程执行命令让所有操作天然具有原子性。两个客户端同时执行 INCR 不会互相穿插不需要像 Memcached 那样靠 CAS 令牌去处理并发更新问题。4.2 Memcached的多线程模型线程池和锁开销Memcached 在主线程 accept 连接后会把连接分配给一组 worker 线程每个线程处理各自的连接事件。从吞吐角度多线程确实在多核环境里更容易吃满 CPU但代价是复杂度明显上升共享的统计信息、LRU 队列、哈希表都要加锁锁竞争在高并发下会抵消一部分多线程收益。实操里衡量下来两者在典型缓存场景下都能跑出每秒十万级的 QPS具体谁高谁低取决于 value 大小、读写比例、网络延迟。网上很多压测数据差异很大就是因为测试环境不同。我的经验是小 value几十字节、纯读写场景Memcached 偶尔能领先一点大 value、复杂数据结构、批量操作场景Redis 的灵活性和效率明显更好。4.3 Redis 6.0 之后的演变IO线程化与命令执行单线程Redis 6.0 做了很大的变化把网络 IO 读写accept、解析、把响应写回客户端改成多线程执行默认 4 个 IO 线程而命令的实际执行依然在主线程。这样既能利用多核缓解大 value、慢客户端场景下的 IO 吞吐压力又不会破坏单线程执行命令的原子性。Redis 7.x 进一步优化了 IO 线程配置的细节让大流量场景下吞吐又涨了一截。4.4 单线程真正的软肋慢命令会拖垮全站Redis 单线程最需要警惕的是——一条慢命令会把整个实例的响应时间拉高一个量级。最常见的坑有三个用KEYS *匹配大量键直接遍历全库几千万 key 的时候会卡住好几秒删除一个大 Key比如几百万个成员的 SetDEL 会同步阻塞。把复杂的 Lua 脚本或 O(n) 命令放在热路径上。我刚接手维护的时候踩过第一个坑凌晨执行一个运维脚本用了 KEYS结果线上报警直接爆炸。后来我全部改用SCAN家族命令分批遍历删除大 Key 用UNLINK异步释放内存生产环境还开启了lazyfree-lazy-user-del yes让 DEL 在底层异步执行。如果你正在用 Redis 6.0 以上版本一定把这些基础配置吃透避免单线程背锅。5. 内存满了怎么办淘汰策略的细节和缓存治理5.1 Memcached的Slab分配与近似LRUMemcached 的内存管理是 slab 机制把内存按不同大小的 chunk 分成若干 slab class每个 class 内再分配固定大小的 page。优点是减少内存碎片、分配效率高缺点是如果数据大小分布不均衡可能某个 class 内存耗尽而别的地方还有空闲。它的淘汰策略是 LRU称为近似 LRU——不是严格按最后访问时间逐个比较而是 1.5.0 后引入了分段 LRUSegmented LRU来降低锁竞争最热数据放在 HOT 段冷数据逐级往下淘汰。理解这个机制你才会明白为什么 Memcached 里大 value 一旦超过 slab 限制会直接 OOM而不是挤一挤再放。5.2 Redis的八种淘汰策略选错等于白干活Redis 在maxmemory设置后由maxmemory-policy控制满了之后怎么处理。八种策略大致分三类主动拒绝noeviction写不进去直接报错适合把 Redis 当数据库用的场景。基于过期键volatile-lru、volatile-lfu、volatile-random、volatile-ttl只淘汰那些设置了过期时间的 key。全键空间allkeys-lru、allkeys-lfu、allkeys-random所有 key 一视同仁按策略淘汰。最容易踩的坑团队把策略配成了volatile-lru但当内存写满时如果大量 key 根本没设置过期时间Redis 会找不到可淘汰对象最终退化为 noeviction 行为新写入全部报错。这种内存降不下来的问题排查起来很迷惑。后来我把纯缓存场景统一改成allkeys-lru因为缓存本来就是冷数据该死给谁留寿数都行如果业务上明确有冷热之分再用 LFURedis 4.0 引入记录访问频次比 LRU 更抗突发流量干扰。5.3 缓存穿透、击穿、雪崩治理经验分享同流程的运维与开发问题还有另三个高频词穿透、击穿、雪崩。虽然 Memcached 场景也遇到但 Redis 生态里治理手段更顺手穿透查询一个不存在的 key每次都打到数据库。治本方案是布隆过滤器拦截Redis 4.0 后有官方模块 RedisBloom简单方案是缓存空值并设置短过期时间比如 60 秒。击穿一个热点 key 突然失效大量请求同时打到数据库。关键是只让一个人去重建缓存其他请求等结果——可以用分布式锁也可以用一个进程内互斥标记singleflight 思路。雪崩大量 key 设置了同一过期时间同时失效。业界标准做法是过期时间加随机抖动比如SETEX key random(300,600)另外热点 key 可以设置“永不过期后台定期更新”。6. 高可用和分布式一个自带整套方案一个全靠客户端6.1 Redis的主从复制、Sentinel哨兵和Cluster集群Redis 的高可用是体系化设计的一层比一层强主从复制Replication主节点异步把写命令同步到从节点从节点可以承担读流量主节点挂了需要人工切换。Sentinel 哨兵模式在复制基础上加一组监控进程主节点挂了哨兵会自动执行故障转移把某个从节点提升为主节点并通知客户端重定向。它适合数据量在单机能装下、但要求自动切换的场景。Redis Cluster 集群模式从 3.0 开始支持把数据按CRC16(key) % 16384分配到 16384 个哈希槽槽分布到不同节点上每个节点再配主从。平时群里的问题redis哨兵模式和集群模式的区别其实就是哨兵模式解决的是高可用主从切换集群模式解决的是数据分片容量水平扩展集群内部也可以带主从和故障迁移。搜索热点里还有 Docker 安装 Redis 主从、KubeSphere 搭建 Redis 集群——现在容器化环境下跟着官方镜像跑主从和 Cluster 其实很容易关键是网络、持久化目录和配置文件挂载后面我可以单独开一篇讲。6.2 Memcached没有集群客户端一致性哈希是唯一出路Memcached 的定位决定了它服务端没有集群、没有主从、没有故障转移。节点多起来时靠的是客户端一致性哈希利用 CRC/hash 算法把 key 映射到一个 2^32 的环上节点也分布在环上一个 key 沿环顺时针找到第一个节点保存。增加或摘除节点时只影响该节点附近的部分 key而不是全局重新映射。为了避免节点太少时数据倾斜一致性哈希通常会引入虚拟节点把一个真实节点拆成上百个虚拟节点均匀散在环上。比如业界常见的ketama算法就是这类实现。应用层还需要自己处理节点故障配置列表里摘掉坏节点重连机制自己维护数据重建自己触发。我在老项目里看到过基于 Libmemcached 的 PHP 客户端服务端 5 台挂了 1 台业务层自动把故障节点移除、请求分布到其余 4 台这已经是 Memcached 官方能踩到的最好方案了。6.3 什么场景该用什么高可用方案如果你的团队在纠结这一层我的判断标准是这样数据量能装进单机通常几个 GB 到几十 GB要求自动故障切换 → 选 Redis 哨兵模式数据量会超出单机需要水平扩容 → 用 Redis Cluster只是纯缓存对丢失不敏感团队不想维护哨兵和集群 → Memcached 客户端一致性哈希反而更简单但如果你已经遇到缓存节点挂了数据库崩了的连锁反应说明缓存并不是可丢的那么你应该考虑的是 Redis 哨兵或 Cluster而不是 Memcached。7. 杀手锏功能与工具链Redis 比 Memcached 多出来的一截7.1 分布式锁SET NX EX 和 Redisson看门狗的坑分布式锁几乎是 Redis 面试和实战的必考点。最基础的写法SET lock:order:1001 1 NX PX 30000这条命令的意思是只有 key 不存在时才设置成功NX并设置 30 秒过期PX。获取锁成功的人才能执行业务执行完用 Lua 脚本保证检查持有者身份再删除是原子的。拿到锁的进程如果业务执行超过 30 秒锁就被自动释放了会锁失效。用 Redisson 客户端的看门狗机制可以解决默认 30 秒生命周期后台线程每 10 秒续期一次业务没结束锁就不会过期。但基于主从的 Redis 分布式锁有一个隐患主节点写入了锁还没同步到从节点主节点挂了哨兵把从节点提升为主节点新的主节点没有这把锁别的客户端就能再次获取成功两个客户端同时持锁。Redis 作者 Antirez 为此提出了 Redlock 算法但业界也有人说它在极端时钟漂移下并不严格安全。所以我在实践中的态度是普通分布式系统用 SETNX 看门狗足够对安全性要求极高的场景比如资金相关的幂等控制请直接引入 etcd 或 ZooKeeper不要拿 Redis 系硬扛。7.2 Lua脚本、事务、Pub/Sub与Stream除了锁还有很多 Memcached 完全不具备的能力Lua 脚本把一组操作封装成脚本发送到 Redis 原子执行不用连续多次网络往返。事务MULTI/EXEC批量执行并保证中间不被其他客户端命令插入注意 Redis 事务不支持回滚。Pub/Sub轻量级消息广播适合聊天室和实时通知。StreamRedis 5.0 加入的完整消息队列支持消费者组、ACK、pending 待处理列表可以替代部分简单 MQ 场景。7.3 日常工具与客户端选择操作 Redis 的可视化工具里大部分人在用的Another Redis Desktop ManagerGitHub 上的开源项目界面简单、跨平台以及老牌的Redis Desktop Manager。运维视角更推荐命令行 redis-cli --stat实时看监控。客户端选型上Java 生态现在是 Lettuce 为主Spring Data Redis 默认需要选主从切换感知/集群拓扑感知时 Lettuce 更顺滑老项目还在用 Jedis 的话注意它的连接池参数调优。Memcached 在这些方面确实几乎裸奔它只有 get/set/delete/append/cas 几个命令客户端做的也就是连接池和散列。工具生态和排障知识库都少很多。8. 最终选型思路从今天往后答案已经不言自明8.1 完整对比表下面这个表是我反复和团队对齐的信息页直接拿去可以参考维度RedisMemcached内存可设置过期时间多种淘汰策略纯内存Slab 分配近似 LRU持久化RDB / AOF / 混合持久化无重启数据全丢数据类型String / Hash / List / Set / ZSet / Bitmap / HyperLogLog / Geo / Stream仅字符串最大 1MB原子操作单线程命令原子Lua 脚本支持仅 CAS / append 等简单原子分布式主从复制、哨兵、Cluster 集群无服务端集群客户端一致性哈希扩展能力Lua、事务、Pub/Sub、Stream、BloomFilter 模块无适用角色缓存 数据库 消息中间件 工具集纯缓存典型QPS单实例十万级单实例十万级8.2 六类场景的选型建议纯 KV 的小对象缓存团队已经用 Memcached 跑得很稳定 → 没必要迁移继续用。需要读写 List / ZSet / Hash或者要排行榜、去重、限流、布隆 → 直接 Redis。缓存数据能容忍重启丢失 → Memcached 够用但 Redis 也不差主要看团队租户成本。缓存数据不能丢session、业务状态、限流配额 → Redis开 AOF。数据量超过单机内存水平扩展势在必行 → Redis Cluster。新项目、新团队、从零搭建 → 别犹豫Redis。它没有额外引入什么你能承受的复杂度但给了你多的自由度。8.3 面试怎么答简洁版对比话术如果面试官问到这个对比我会把核心差异压缩成四句话一是数据结构层面Memcached 只有字符串Redis 有完整的类型体系二是数据安全性层面Memcached 没有持久化Redis 有 RDB/AOF/混合方案三是高可用层面Memcached 需要客户端自己维护一致性哈希Redis 自带主从、哨兵、Cluster四是定位层面Memcached 纯粹是缓存Redis 是存储组件、消息队列、工具集的综合体。最后补一句Memcached 的设计简单稳定在极小化缓存场景仍有价值但新架构我更倾向 Redis。最后分享一个个人体会我在最初接手中间件这块时曾被Memcached 性能比 Redis 好的说法带偏过结果在被需要数据结构能力的业务上写了很多笨重的序列化代码。后来才想明白性能瓶颈极少出现在缓存访问这一层的算力上反而是在序列化、网络往返、代码复杂度里。现在选型时我会先问这里面需要什么数据形态、丢了能不能接受、要不要自动故障转移这三个问题一过答案基本就浮出水面了。
返回列表