
买卖双方都不傻亿级流量面前缓存方案一定不是“用 Redis 顶在前面”这么简单。Uber 这篇集成缓存Integrated Cache的分享是系统设计圈里少有的、把缓存直接塞进数据库存储引擎里的实战派解法。我读完第一反应是他们是真的被独立缓存层的运维成本打疼了才会下决心去改 MySQL 的存储引擎。这篇文章我会用自己的理解把整套方案拆开从问题背景、架构演进、核心实现到复刻思路全部讲清楚适合正在设计高并发读路径、或者被缓存一致性问题折磨的后端工程师。1. 读放大困境Uber 当初为什么要把缓存做进引擎1.1 调度系统的核心数据模型与读放大现象Uber 的核心业务是派单和调度整套系统围绕“司机当前位置、订单状态、车辆轨迹”这一批高实时性数据运转。司机的位置数据每秒都在变化乘客端、司机端、调度算法、预测引擎、支付风控都在同时读这一批数据这就形成了典型的读放大效应一条写入可能被几十个业务系统读取而且每个系统读的频率还不一样。这个场景和社交平台的时间线很像但有一个关键差异Uber 的数据有更强的时空属性位置会持续更新旧值很快失效新值必须马上可见。数据库层面这些数据最终落在 MySQL 集群里以 Profile 和 Trip 维度组织部分表本身就是 KV 结构比如profile_id → 最新位置。这类结构天然不适合复杂 JOIN却非常适合缓存。当时的痛点很直接主库纵向扩容到头了CPU 和磁盘 I/O 都撑不住读流量又不像写流量那样可以通过业务削峰来降低它完全由用户行为和调度算法驱动峰值来得又快又猛。如果按峰值读 QPS 去扩整个 MySQL 集群成本会高到没人敢签字。1.2 第一版方案影子副本和读副本为什么不够最先尝试的是常规的 MySQL 主从副本方案给主库挂 N 个只读副本读流量打到副本上主库专心处理写请求。这个方案的优点是可以快速上线、对业务透明几十行配置就能把读流量分走。但实际跑起来就发现不对劲。位置数据的特点是小而高频每条记录可能只有几百字节但每秒更新次数极高。把这类流量复制到多个副本上带来的问题是每个副本都要承担几乎和主库等量的写入重放磁盘 I/O 和 CPU 消耗一点没少。更难受的是主从复制有延迟调度算法读到旧位置可能导致派单到错误区域乘客端看到的车辆位置也会卡顿。副本方案本质上只是把“读”从主库挪到了别处并没有改变每个节点都要处理全量写路径的事实。Uber 的调度负载不是简单的一写多读更重要的是实时性要求极高读副本在多级复制链路上引入的延迟根本无法接受。到了这一步团队意识到不能只靠 MySQL 本身的扩展机制需要在读路径上引入一层专门扛热点的组件。1.3 中间过渡独立 Redis 缓存层的得与失随后很自然的方案是部署独立的 Redis 集群采用旁路缓存Cache-Aside模式应用先读缓存没命中再查数据库然后把结果写回缓存。这个方案上线后缓存命中率一度到了 95%MySQL 的压力确实下来了一大截。但这套架构埋了几个雷。第一个是运维复杂度翻倍Redis 要单独搭集群、单独监控内存、单独处理淘汰和分片等于在 MySQL 之外又养了一套和业务同样关键的基础设施。第二个是缓存和数据库之间的一致性很难保证。比如司机位置更新时是先更新数据库还是先更新缓存先更新缓存再写库写库失败会导致缓存里有脏数据先写库再删缓存中间有极短的时间窗口会读到旧值。对于 Uber 这种位置数据旧值可能导致调度偏差不是“多刷一次页面”那么轻松。第三个雷是缓存穿透和雪崩处理。某个热门区域的司机位置 key 过期瞬间大量请求同时回源到 MySQL如果正好赶上高并发窗口数据库又会被打垮。为了防穿透团队加了布隆过滤器、空值缓存、随机过期时间等手段但这些细节每一个都代表新的开发和运维成本。独立缓存层治标但没治本——它只是把压力从 MySQL 转移到一个同样需要精心细养的系统上。2. 集成缓存一场针对“网络开销”的手术2.1 为什么选择改 MySQL 而不是继续优化 Redis既然 Redis 方案能用为什么要动 MySQL 引擎这是整个方案里最值得琢磨的一步。公开分享里透露了几个关键考量其中最核心的是请求链路的物理开销。在旁路缓存架构里一次缓存命中的读取大概是这样的链路客户端进程通过网络访问 RedisRedis 把序列化好的数据返回客户端再反序列化成业务对象。这段链路包含两次网络往返、一次 TCP 或 UDP 处理、一次数据序列化/反序列化。对于 U 量级的单个请求单看一次不到零点几毫秒但如果算到每秒上亿次的读取这就是比 CPU 计算还贵的物理损耗。同时独立 Redis 只能做“外部缓存”它和 MySQL 之间隔着网络。MySQL 无法感知缓存的存在于是“缓存什么、何时失效”只能由业务方在应用层控制这导致大量的失效逻辑散落在代码里无法形成统一的策略。Uber 团队意识到如果把缓存直接做进 MySQL 存储引擎内部让请求在数据库进程内完成缓存查找就能把上亿次读取中的绝大部分网络开销省掉同时把一致性问题收缩到引擎内部解决。2.2 集成缓存的工作原理读与写路径的重新设计集成缓存的设计思路可以概括为一句话让 MySQL 的存储引擎本身具备内存索引用把它变成“内存优先、磁盘兜底”的架构。在具体实现中MySQL 的 InnoDB 引擎之上扩展了一个基于内存的键值缓存区Block Cache 和 Key 索引都驻留在内存里数据访问路径变成了下面这样。读请求进入引擎后先去内存缓存区里查目标 key。如果命中直接返回缓存数据完全跳过 B-Tree 索引扫描和磁盘 I/O。只有未命中时才继续走 InnoDB 的标准路径查缓冲池、读磁盘、然后拿到数据再决定要不要把这个 key 塞回内存缓存区。这个“缓存优先”的路径本质上和 Redis 的单线程事件循环非常像只不过它和表数据存在同一个进程里没有网络跳转。写路径更关键。常规旁路缓存里写请求要先更新数据库再删缓存中间有窗口期。而集成缓存因为和 InnoDB 在同一进程内所以可以在事务提交的瞬间直接同步更新或删除对应缓存条目让内存缓存和磁盘数据保持强一致。位置更新时新的 GPS 坐标在事务提交那一刻就替换掉缓存里的旧值所有后续读请求立刻读到最新值。这就绕开了旁路缓存架构中永远绕不过的一致性问题。2.3 1.5 亿次/秒读取请求是如何计算的很多人看到 1.5 亿 QPS 会下意识觉得是纸面数据但理解它的计算逻辑后会发现这套数字不是靠单机性能吹出来的而是靠“横向分片 高命中率 无网络热路径”三件事叠加出来的。Uber 的业务数据按城市和业务线拆成了多个逻辑集群每个集群覆盖一部分数据分片。这些分片分布在成百上千个 MySQL 节点上每个节点只负责自己那部分 key 空间的内存缓存和磁盘存储。如果总共有数千个节点平均每个节点只需要承担几万 QPS 的读流量对纯内存操作来说几万 QPS 完全在常规服务器能力范围内。再叠加上缓存命中率。公开分享中提到的缓存命中率高达 99%这意味着真实落到磁盘的读请求非常少。以 1.5 亿次读取为例如果命中率是 99.8%那么实际打到磁盘的只有 30 万次。磁盘只承接 0.2% 的流量自然不会成为瓶颈。整个架构的核心不是让单台机器变快而是让 99% 的读请求永远不接触磁盘同时把单机负载控制在硬件能轻松扛住的水平。提示这个 QPS 数字对绝大多数团队没有直接参考意义但它揭示了一个可复制的规律——把热点数据尽量压在内存里 把单机负载拆到足够低比优化单个查询快十倍更有工程价值。3. 核心细节解析缓存区设计、一致性与内存管理3.1 存储引擎内的缓存结构与淘汰策略集成缓存在引擎内部实现了一套类似 Redis 的哈希索引结构但数据粒度更贴近 MySQL 的行数据。缓存区按 key 做哈希分桶每个桶内挂多条缓存记录value 中存储的是已经序列化好的行数据或行子集。这里的序列化格式直接复用了 InnoDB 的内部行格式省去了业务对象的转换开销。内存淘汰策略使用近似 LRU。每条缓存记录上记录最近访问时间内存达到上限时从淘汰候选链表里回收最久没被访问的记录。工程上并没有做全局精确 LRU因为维护精确 LRU 的链表在并发访问下会变成性能瓶颈近似 LRU 已经足够产生很高的命中率。另一个细节是内存池的划分。缓存区不是简单的一块大内存而是被分成多个 Shard每个 Shard 有独立的锁和淘汰链表。这样做的好处是不同 Shard 之间的淘汰和插入操作互不干扰避免全局锁竞争同时可以将冷热表隔离热点表所在的 Shard 内存压力再大也不会把冷表数据挤走。这个设计的启发是缓存管理不能只考虑命中率平均值还要考虑热点集中度和局部性。3.2 一致性和并发控制的取舍一致性是集成缓存最有说服力的卖点但实现起来并不简单。位置数据这类场景读多写少但写入可能集中在同一批热门 key 上。比如一个订单密集区的司机位置 key 的更新频率可能达到每秒多次。集成缓存处理并发更新的一个关键机制是版本号比较。每个缓存条目上保存一个版本标识写入事务提交时新的数据带着更高的版本号进入缓存。如果在并发更新场景下出现缓存条目的版本号低于磁盘中已提交的最新版本引擎会拒绝使用旧缓存数据强制回源。这样即使缓存条目被意外替换也不会把过期数据返回给上层业务。这里有个典型取舍缓存更新是同步做还是异步做。同步更新能保证一致性但会让写路径多等待一个缓存写操作异步更新会减少写延迟但会有短暂的时间窗口读到旧值。Uber 的调度业务对位置实时性要求极高所以他们选择在事务提交路径上同步更新缓存。付出的代价是写延迟小幅上升换来的是所有读取都能立刻看到新值从业务角度看这笔账非常划算。3.3 内存容量规划与命中率监控集成缓存最核心的运维指标是命中率。如果命中率下降 1%回源到磁盘的读流量会直接升高一个数量级因此团队对命中率的监控是分钟级别的。内存容量也不是拍脑袋定的背后有一套计算公式。对于位置数据这类场景容量规划的核心是估算热数据集的规模。假设一个集群有 5000 万活跃 key每个 key 平均 value 大小是 2KB包含元数据那么只需要 100GB 内存就能完全容纳热数据。Uber 的关键数据在内存上是非常舍得投入的因为省掉的磁盘 I/O 带来的延迟下降和稳定性提升远大于内存本身的硬件成本。监控上会同时关注缓存命中率、淘汰速率和内存使用水位。淘汰速率是一个早期预警信号如果淘汰速率持续上升说明内存容量快要不够了可能需要扩容或者把冷 key 分区迁移出去。另一个隐蔽的坑是内存碎片特别是当缓存 value 大小差异巨大时长时间运行后碎片率会升高导致实际可用内存缩水这个需要通过定期重启或底层内存池整理来解决。3.4 与 Redis 方案的深度对比很多人在理解集成缓存时会把它简单等同于“MySQL 内置了 Redis”但两者的本质区别在于信任边界。Redis 方案中缓存和数据库是两个互不信任的独立系统你需要通过外部机制去同步它们集成缓存方案中缓存和磁盘数据是同一个存储引擎的两个视图一致性由引擎自己保证。从部署角度看Redis 方案需要额外维护一套集群包括 Sentinel 或 Cluster 的高可用管理、持久化、分片槽位迁移。集成缓存则跟着 MySQL 集群一起扩容缩容节点挂掉后数据从磁盘恢复缓存区重建即可不需要单独做数据复制。这个差异在故障场景下特别明显Redis 节点宕机缓存是冷的回源直接把数据库打挂MySQL 节点宕机由主从切换接管新的主节点上缓存虽然也是冷的但这个“冷”只在单分片范围内且能顺着主从复制逐步预热。从性能上限来看Redis 的 CPU 效率很高但它受限于网络入口带宽和单机内存容量。集成缓存则可以直接横向扩展到上千节点每个节点只管一部分分片。单节点性能不如 Redis 也没关系靠分片数量堆出总吞吐量。这就是 1.5 亿 QPS 和普通 Redis 集群之间的分水岭。4. 实操视角如果要在业务中复制集成缓存思路4.1 什么样的业务适合集成缓存不是所有业务都值得像 Uber 这样改数据库引擎。从 Uber 这套方案里可以提炼出四个适用条件。第一读放大量级要大最好做到写读比例 1:100 以上如果只有 1:5 的读写比外部缓存已经够用。第二数据访问模式要是 key-value 型通过主键或唯一索引直接读不需要跨表 JOIN 和复杂聚合。第三热数据总量要能被内存装下如果一个集群的热数据超过单机内存上限分片后要分别扩容成本会迅速上升。第四实时性要求极高旧数据会造成业务损失这类业务才会愿意为“写路径多等一次缓存更新”买单。反过来看财务报表、日志分析、数据仓库这类场景就不适合集成缓存。数据量巨大且访问模式偏扫描缓存收益极低读取频率不高内存投入完全不划算容忍分钟级延迟用外部缓存或干脆去掉缓存直接读库更合理。4.2 复刻路线应用内嵌缓存 数据变更订阅如果不想改 MySQL 引擎怎么用这套思路的降级版实现部分效果有一个模式非常值得尝试应用进程内嵌本地缓存如 Caffeine / Guava Cache配合数据库变更日志Binlog订阅来同步刷新缓存。具体步骤是应用在进程内维护一层热点缓存承载绝大部分读流量同时用 Canal 或 Debezium 订阅 MySQL 的 Binlog解析出数据变更事件通过消息队列推给应用服务服务收到变更事件后更新本地缓存或直接删除对应 key。如果除了本地缓存还要一个共享缓存层可以在这个模式下再叠加一层 Redis本地缓存扛绝大部分热点Redis 扛分布式访问数据库扛最终兜底。这套方案的优点是比改存储引擎简单得多适合业务快速验证缓存收益。但它也有明显的边界应用实例重启后本地缓存是空的需要预热多个应用实例之间缓存无法保证强一致只能靠精确的失效事件来逼近如果消息队列延迟高缓存数据可能短暂过期。所以它适合一致性要求没有 Uber 那么苛刻的业务典型场景如商品详情页、用户资料、配置数据。注意无论用哪种方式实现集成缓存都不要忽略缓存命中率监控。命中率是缓存系统的血压计低于 90% 时就要警惕回源流量是否压垮数据库而不是等到雪崩后才去看监控大盘。4.3 避坑指南集成缓存方案常见的五个问题问题一内存上限设得太高导致操作系统 Swap。缓存本意是减少延迟如果把机器的物理内存全部占满触发 Swap 反而会把延迟拉高几个数量级。建议内存使用率上限设为物理内存的 70% 左右预留出操作系统和线程栈的空间。问题二所有表混用一个缓存区。如果不同表的访问模式和实时性差异很大混用会导致淘汰策略失灵热表的数据被冷表数据挤掉。应该按表的业务优先级拆分缓存分区或独立缓存实例。问题三写密集场景下缓存更新放大。如果数据更新频率远超读取频率每次写入都同步更新缓存反而增加了写路径的负担。这种场景下应该采用失效策略只删除缓存条目不写入新值让下一次读取时再加载。问题四忽略缓存预热。一个节点重启后缓存是空的如果直接放入流量前几分钟会有一波巨大的回源压力。常规做法是提前用离线数据或历史访问日志做一次预热把热 key 先加载到内存里再放流量进入。问题五只关注平均命中率忽略热点分散。平均命中率 99% 不代表没有风险如果极少数核心 key 的命中率只有 50%它们依然会带来大量的磁盘 I/O。要按 key 维度做热点监控及时发现集中访问的 key。5. 从集成缓存看多级缓存架构的未来5.1 缓存与存储的信任边界真正的架构转变集成缓存带来的启示不是“把 Redis 并进 MySQL”这个具体动作而是改变了缓存与存储之间的信任关系。在传统架构中缓存和存储是两个平级组件应用层负责在它们之间做路由一致性边界模糊数据可信度依赖开发者的自律。在集成缓存架构中缓存成为了存储引擎的一个内部模块存储系统自己管理自己的缓存应用层不再介入一致性问题。这个思路的下一步自然延伸是内存数据库和持久化引擎的融合。SAP HANA、单表内存引擎、以及那些自带二级缓存的分布式数据库本质上都在走同一条路把内存和磁盘的差异从“外部架构问题”变成“内部存储问题”。Uber 的实践表明这条路是走得通的而且在高并发实时场景下收益极其明显。5.2 对应到日常后端设计的行动清单如果你现在要在一个新项目里规划高并发读架构可以按照下面的顺序来思考第一步画清楚核心数据的读写比例和实时性要求判断是否值得为缓存投入额外架构。第二步优先用应用内嵌缓存和 Binlog 订阅做初步方案验证命中率能做到多少。第三步如果业务规模快速增长命中率依然很高但单机内存扛不住再考虑把缓存下沉到存储节点用分片的方式横向扩展。第四步无论选哪种方案都提前把缓存监控、预热流程、容量规划模板做好而不是等线上出问题再救火。我个人在实际操作中的体会是Uber 这套方案最值钱的部分不是那 1.5 亿 QPS 的数字而是他们对“一致性”这一问题的态度——与其在外部用两套系统互相校准不如把信任边界收回到一个系统内部。在高并发场景下少一跳网络、少一次序列化、少一个外部依赖永远比任何花哨的智能算法都更可靠。如果你也想在未来架构里塞高速缓存层不妨先问自己一句这层缓存能不能离数据更近一步而不是再加一台偏远服务器。