卷二接着卷一往下写。「回声」已经有了账号、资料、BFF、入口、缓存、异步和发布能力,接下来要回答一个更难的问题:当产品的主循环变成「有人发内容、有人看内容、有人回应」时,服务端的形状要怎么变。
这一卷不从「如何设计一个 Feed 系统」开始,而是从「内容在什么状态下允许被谁看到」开始。因为内容社区的大部分事故,不是排序不够好,而是内容在不该出现的地方出现了:审核中的内容被搜到、拉黑的人还能评论、删除的帖子还在推荐位、未读数在多端之间对不上。
本卷要解决的问题
读完卷一,你已经有了一套基础能力:账号、会话、资料可见性、BFF 聚合、网关、缓存、异步消息、发布灰度、观测与容量。这些能力是通用的,换个产品也能用。
卷二的问题不一样:它们只在「内容 + 关系 + 实时互动」这个组合里出现。
- 内容有生命周期。草稿、审核、发布、删除不是四个布尔开关,而是一条有权限、有时限、有回滚的状态机。
- 内容有可见范围。公开、好友可见、仅自己可见,再叠加拉黑、屏蔽、群成员,最终由谁在什么场景下能看到什么,是一条判定链而不是一个字段。
- 关系有方向。关注是单向,好友是双向,拉黑是单向且优先级最高;三者混在一张表里,迟早会写出「删除好友后还能看到」的 bug。
- 互动有放大。一次点赞会触发计数、通知、Feed 重排;一条大 V 内容会触发几十万次写扩散;一个群消息会触发多端投递与未读变更。
- 实时有代价。长连接、在线状态、多端同步、离线补偿,每一项都在放大故障半径。
所以本卷的判断标准不是「用了什么中间件」,而是三件事:
- 这条内容在什么状态下、对谁、在哪个入口可见?
- 这次互动失败时,用户看到什么?会不会重复、丢失、顺序错乱?
- 这个实时能力故障时,影响一个用户、一个群,还是整个首页?
全书结构:13 章分四段
卷二 13 章可以分成四段,每段解决一类问题。
| 段 | 章节 | 解决的问题 | 收口产物 |
|---|---|---|---|
| 内容本身 | 第 1–2 章 | 内容怎么建模、怎么审核、怎么存 | 内容状态机、审核流水线、冷热分层策略 |
| 内容分发 | 第 3–4 章 | 内容怎么到达用户,顺序怎么定 | Feed 推拉模型、排序信号、实验回收口径 |
| 关系与互动 | 第 5–7 章 | 谁能看到、谁能回应、回应如何计数与通知 | 关系判定链、计数一致性方案、评论树与折叠规则 |
| 实时通道 | 第 8–12 章 | 消息怎么存、怎么投、怎么推送、怎么被搜到与推荐 | IM 消息模型、投递与去重、推送降级、搜索同步、推荐兜底 |
| 收口 | 第 13 章 | 把前面串成一条端到端链路 | 发布→Feed→互动→通知的可运行用例 |
四段之间不是严格串行。第 5 章的关系判定会被第 3 章的 Feed、第 7 章的评论、第 9 章的投递反复引用;第 2 章的审核状态会被第 11 章的搜索删除复用。阅读时可以按段跳,但每一段内部的顺序建议保留。
章节依赖图
下面这张图只画「读懂后一章需要先读哪一章」,不画代码依赖。
第 1 章 内容模型
├── 第 2 章 发布与审核
│ └── 第 11 章 搜索接入(删除与墓碑)
├── 第 3 章 Feed 模型
│ └── 第 4 章 Feed 排序
├── 第 5 章 社交关系
│ ├── 第 7 章 评论(谁能评论、谁被折叠)
│ └── 第 9 章 投递(群成员与拉黑)
├── 第 6 章 计数与互动
│ └── 第 7 章 评论(计数与通知)
├── 第 8 章 IM 消息模型
│ └── 第 9 章 IM 投递
├── 第 10 章 推送与通知
└── 第 12 章 推荐接入
第 13 章 收口依赖以上全部
如果只想解决一个具体问题,可以按下面的最短路径跳读:
- 内容发不出去、审核积压:第 2 章 → 第 1 章状态机 → 第 13 章发布链路。
- 首页刷不出新内容:第 3 章 → 第 4 章 → 第 12 章兜底。
- 用户说「拉黑了还能看到」:第 5 章 → 第 3 章读路径 → 第 11 章搜索过滤。
- 消息重复、未读数不对:第 8 章 → 第 9 章 → 第 13 章端到端用例。
三条阅读路径
路径一:按产品主循环读。 第 1 章建模 → 第 2 章发布审核 → 第 3、4 章分发 → 第 6、7 章互动 → 第 10 章通知 → 第 13 章收口。这条路径回答「一条内容从产生到被消费,中间经过了什么」。
路径二:按故障类型读。 第 5 章可见性 → 第 9 章投递与去重 → 第 10 章推送降级 → 第 11 章删除一致性 → 第 13 章质量指标。这条路径回答「内容社区最常见的四类事故怎么定位」。
路径三:按代码读。 每章的 Go 包都是独立的 L1 包,可以只跑自己关心的那一章。第 13 章有进程内端到端用例,适合在读完第 1–12 章后一次性验证。
贯穿案例:回声的第二阶段
卷一里的「回声」还是一个「能注册、能改资料、能发一条动态」的最小产品。卷二里它长出了内容社区的主循环:
- 用户可以发文本、图片、视频,内容要经过机审与人审。
- 用户可以关注、加好友、拉黑、屏蔽。
- 首页有关注流和推荐流,关注流按时间与热度混合,推荐流有兜底。
- 内容下面有评论、点赞、收藏,互动会触发通知。
- 用户之间可以单聊、群聊,消息要多端同步、离线补偿、未读计数。
- 内容要能被搜到,删除后要尽快从搜索结果消失。
这些能力不是同时上线的。本卷按「先让内容可见、再让内容可分发、再让互动可靠、最后让实时通道稳定」的顺序展开,每一章都对应一个可以独立上线的阶段。
十三章速览
第 1 章 内容模型
把文本、图片、视频的元数据拆成审核、检索、转码、计费都要用的字段;把草稿、审核、发布、删除写成状态机;再讨论内容存储的冷热分层依据为什么是访问而不是创建时间。
产出:元数据校验、状态迁移函数、冷热分层判定。
第 2 章 发布与审核
把发布链路拆成同步与异步两段,明确哪些步骤必须在用户等待时间内完成;机审给置信度,人审处理灰区,复审由抽样与申诉触发;最后处理审核失败的用户体验与申诉时限。
产出:发布流水线、审核决策函数、申诉状态机。
第 3 章 Feed 模型
拉模式把成本放在读时,推模式把成本放在写时,混合模式按作者分层。重点不是选哪个,而是算出「一次发布扩散多少次、一次刷新合并多少个源」。
产出:多源合并、推拉选择、混合时间线。
第 4 章 Feed 排序
时间、热度、亲和度三个信号各有偏差;去重、折叠、已读会影响用户对「内容消失」的感知;实验要稳分桶、先定指标、看长期留存而不是只看点击。
产出:排序函数、去重折叠规则、分桶与效果计算。
第 5 章 社交关系
关注是单向、好友是双向、拉黑是最高优先级;可见性判定是一条链,不是几个布尔字段;关系链的三种查询要有不同的存储与分页策略。
产出:关系校验、可见性判定、关系图查询。
第 6 章 计数与互动
点赞是关系,计数是物化结果;精确计数、近似计数、对账重建是三档不同成本;频控键要覆盖用户、目标、IP、设备多个维度。
产出:计数累加、漂移检测、滑动窗口频控。
第 7 章 评论
评论是树还是两层,决定了分页与「查看更多」的实现;折叠不是删除,要能展开与申诉;通知触发要有边界,避免一个人连点二十个赞。
产出:评论树构建、折叠规则、通知聚合判定。
第 8 章 IM 消息模型
会话 ID 怎么生成、序号是会话内单调还是全局单调、客户端重发怎么处理;单聊、群聊、系统消息的权限差异;消息存储按会话分片、按时间分区,漫游不能拖垮在线路径。
产出:消息校验、会话规则、保留与漫游分页。
第 9 章 IM 投递
在线投递是尽力而为,离线补偿才是可靠性来源;多端在线要全部投递;已读要合并多端;消息去重不能只靠客户端 ID。
产出:在线离线路由、未读计算、去重窗口。
第 10 章 推送与通知
到达率不是「发出去了」;四个渠道的包体、深链、图片能力不同;通知要聚合与频控;推送失败要降级到站内信或短信,而不是静默丢弃。
产出:渠道能力表、模板渲染、降级链。
第 11 章 搜索接入
索引是另一种读模型,不是数据库副本;召回、排序、过滤三阶段职责不能混;删除要有墓碑,搜索一致性窗口要写进产品预期。
产出:索引增量应用、查询匹配、滞后与重试。
第 12 章 推荐接入
推荐服务只回答「给谁推什么」,业务服务负责「能不能推」;特征分在线离线,候选分多路召回;冷启动要有明确的兜底链。
产出:候选合并、特征补全、兜底选择。
第 13 章 收口
把发布、Feed、互动、通知串成一条端到端链路;把 IM 与未读做成可验证的多端用例;给出 Feed 新鲜度、消息到达率、未读准确率三条质量指标。
产出:链路校验、端到端测试、质量指标汇总。
代码分级与本卷落点
本卷延续卷一的四级划分:
| 级别 | 判定 | 本卷落点 |
|---|---|---|
| L1 | 单包、纯逻辑、不依赖外部进程 | 第 1–12 章的全部示例 |
| L2 | 进程内端到端 | 第 13 章的 HTTP 用例 |
| L3 | 真依赖(数据库、缓存、消息队列) | 本卷不做,留给卷四的规模化讨论 |
| L4 | 多节点、故障注入、网络分区 | 本卷不做 |
每个代码块开头都有三行头:
// file: internal/<包名>/<文件>.go
// tier: L1
// verify: go test ./internal/<包名>/
代码块与实验工程逐字节一致。读者可以只跑某一章:
go test ./internal/feed/
go test ./internal/im/ ./internal/delivery/
数字与版本口径
- 语言与运行时:Go 1.26.0,module
webapp。 - 文中所有「实测」输出都在本机真实执行;没有执行过的数字一律写「示例」或「预期」。
- 示例里的 DAU、粉丝数、消息量、存储量都是案例假设值,不是任何真实产品的数据。
- 时间单位统一用秒或毫秒,存储单位统一用 GB(1 TB = 1024 GB),金额单位统一用元。
- 排序、计数、去重里的阈值都是起点,不是结论;换产品要重新标定。
与站内已有内容的关系
站内 miniblog 专题写过 Feed、评论、通知、搜索的产品实现,design 专题写过内容社区的系统设计,algorithm-interview 写过推荐与排序的基础题。本卷不重复这些文章的定义与清单,只在需要时用一段话交代结论,并链接到原文。
本卷新增的是:把内容社区的能力放回「回声」的具体阶段,给出可运行的 Go 实现、失败场景与取舍依据。同一个题目,如果站内已有文章讲「是什么」,本卷讲「在这个产品里什么时候做、做错会怎样」。
本卷不覆盖的内容
- 不写推荐算法的模型细节,只写推荐服务与业务服务的边界、候选合并与兜底。
- 不写搜索检索引擎的内部结构,只写索引同步、查询过滤与删除一致性。
- 不写 IM 协议的编解码与传输层优化,只写消息模型、投递语义与多端状态。
- 不写内容安全的具体模型与策略,只写审核流水线、置信度阈值与申诉流程。
- 不写多机房、多活与容灾,这部分留到卷四。
术语约定
- 内容:用户发布的文本、图片、视频及其元数据的集合。
- Feed:用户打开首页看到的内容列表,本卷区分关注流与推荐流。
- 收件箱:推模式下为每个用户预先写好的时间线。
- 写扩散:一次发布向多个收件箱写入的 fanout 过程。
- 可见性:在给定 owner、viewer、关系与内容属性的条件下,是否允许读取。
- 已读序号:用户在某个会话里读到的最大消息序号。
- 墓碑:删除后保留的标记记录,用于让索引与缓存知道「这条已经删了」。
- 兜底链:主路径不可用时按顺序尝试的备选来源。
如何判断自己读懂了
读完一章,试着回答下面三个问题;答不上来就回到对应小节。
- 这一章的核心状态或数据,谁拥有写权限?谁只能读?
- 这条链路失败时,用户看到的是错误、空列表、旧数据,还是重复数据?
- 如果流量涨十倍,先坏的是哪一段?这一章有没有给出判据?
如果三题都能用自己的话回答,并且能指出代码里对应的函数,这一章就算读懂了。
常见误区
- 把可见性当成前端问题。 前端隐藏字段不等于接口不返回;判定必须在服务端读路径上。
- 把 Feed 当成一个列表接口。 Feed 是「读模型 + 写扩散 + 排序 + 过滤」的组合,任何一段缺失都会在生产上暴露。
- 把消息去重当成客户端责任。 重发、网关重试、消费重试都会产生重复,服务端必须有自己的去重窗口。
- 把未读数当成真相。 未读数是派生值,真相是每个会话的已读序号与最新序号。
- 把搜索索引当成实时库。 索引天生有滞后,产品预期要写清「删除后多久消失」。
- 把推送成功当成用户看到。 到达率、展示率、点击率是三件事,渠道失败要能降级。
小结
卷二的主线可以压缩成一句话:先把「谁能看到什么」判定清楚,再谈分发、互动与实时。
内容模型与审核决定内容能不能出现;Feed 与排序决定内容出现在哪里;关系与互动决定谁能回应、回应如何计数;IM、推送、搜索、推荐决定这些能力在实时通道里如何稳定运行。第 13 章把这条链路串起来,给出可运行的验证与三条质量指标。
下一步
下一卷进入交易与增长:商品与库存、订单与支付、优惠券与营销、会员与积分、秒杀与限流、增长与实验、开放平台、国际化、后台与运营。那一卷的约束从「内容能不能被看到」变成「钱和库存不能算错」,对一致性的要求会明显提高。
如果你是从卷一读过来的,建议先跑一遍卷二的第 13 章端到端用例,再进入卷三;如果你是从某个具体问题跳读过来的,可以直接从对应章节开始。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。