首页/数据/NoSQL 速查/读扩散与写扩散的取舍
选型与架构50 条

读扩散与写扩散的取舍

推送(写扩散)在写入时把数据分发到多处,读路径简单但写成本乘以扇出数;拉取(读扩散)在读取时聚合多处数据,写便宜但读延迟与查询成本高。社交时间线、关注流常见混合方案:活跃用户推送、长尾拉取。

NoSQL 速查数据更新于 2026-10-13

是什么

写扩散(fan-out on write)是发布时就把数据分发到所有需要它的地方,读的时候一次取回;读扩散(fan-out on read)是发布只写一处,读的时候再把多路数据合并起来。两者是同一份成本在读写两端的取舍:写扩散把代价压在发布路径上,读扩散把代价压在读取路径上,混合方案按活跃度或粉丝规模分流。

基本信息

写扩散发布时写向所有订阅方收件箱:读快;粉丝多时写放大明显,不活跃用户白占存储
读扩散读时合并关注对象的最新内容:写简单;关注多时聚合慢,缓存压力集中在读侧
混合按活跃度或粉丝阈值分流:活跃用户预先推送,长尾用户读时合并
典型组件收件箱用 Redis 列表或有序集合存 ID,正文单独存储,合并时再按 ID 取

典型使用场景

  • 社交时间线:粉丝量小的作者直接推给粉丝,粉丝量大的账号由读者自己拉取后合并
  • 搜索结果预拼:把多表数据提前拼成宽文档写入搜索引擎,牺牲写入换取检索速度
  • 报表预聚合:用物化视图或汇总表在写入或调度时算好指标,查询只做过滤
  • 通知分发:一条消息要送达多端多用户时,决定是落一次待拉取的收件箱,还是逐个写入
  • 边缘缓存预热:把源站内容提前推到各边缘节点,本质是把一次源站读换成 N 次分布式写

混合时间线的分流逻辑

混合时间线的分流逻辑
发布事件 -> 读取作者粉丝规模与活跃度
  |- 粉丝少且活跃:写给每个粉丝的收件箱(写扩散,例如阈值 500 以内)
  |- 粉丝多:只写作者自己的时间线,读者侧拉取(读扩散)
  |- 不活跃粉丝:不推送,减少无效写入与存储
读时间线 = 合并(收件箱最新 N 条, 关注的大账号最新 N 条) -> 按时间排序 -> 截断
缓存策略 = 只缓存活跃用户的合并结果,冷用户每次重算

两种扩散逐项对比

维度写扩散(推)读扩散(拉)
写入成本乘以订阅方规模,大号发一条就是海量写一条内容只写一次
读取成本一次查询即可,延迟低且稳定每次都要合并多路来源并排序截断
存储占用同一内容存多份,热门内容重复明显只存一份,另外依赖缓存扛读
修改与删除要回写所有副本,容易出现残留改一处即可,天然一致
可见性异步推送被积压时读到的内容滞后发布即生效,读时一定能看到
搜索与列式库里的同类取舍更容易被忽略:把宽表预拼好灌进搜索引擎、把聚合结果写进物化视图、把统计提前算进汇总表,本质都是用写入成本换读取成本,判断标准是读写比与数据变更频率。
规模变化会让方案失效:上线时粉丝数上限很低,纯写扩散很舒服;等出现百万粉丝账号,同一个设计就会在发布路径上产生尖峰。分流阈值应当落成配置项,而不是写死在代码里。

易错点

  • 给百万级粉丝的账号做全量写扩散:一次发布产生海量写入并拉长发布延迟,高峰期直接拖垮写入队列
  • 纯读扩散在关注数很大时每次都要聚合多路数据,读路径一旦超时,重试会让负载进一步放大形成恶性循环
  • 忽略删除与修改的同步代价:写扩散下修改一条历史内容要回写所有副本,缺少回写机制就会长期存在脏数据
  • 只在平均流量下评估:写扩散的痛点出现在头部账号的尖峰,用平均写入量估算资源必然不够

数据来源与参考

  • Twitter Engineering Blog: Timelines(概念参考)
  • MongoDB Manual: Materialized views
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌