《互联网产品的服务端架构·内容、社交与实时互动》前言

卷二前言:说明本书的读者对象、13 章的组织方式、每章的统一结构、可运行 Go 实验工程的用法、代码分级与版本口径、命名与错误处理约定、表格的读法,以及三种阅读路径、如何跳过已熟悉的部分与如何验证自己真的读懂了;全部代码来自可运行的 Go 1.26 实验工程。

这一篇说明这本书怎么读、代码怎么跑、数字怎么理解。赶时间的读者可以只读「每章的结构」和「实验工程」两节,其余内容在遇到疑问时再回来查。

这本书假设你已经知道什么

本书假设读者具备以下基础:

  • 能读懂 Go 代码,知道 goroutine、channel、context 的基本用法。
  • 写过 HTTP 服务,理解路由、中间件、状态码。
  • 用过至少一种关系型数据库,知道索引、事务、分页。
  • 对缓存、消息队列有概念级认识,不要求有生产运维经验。

不要求读者做过内容社区或 IM。本书会从内容模型开始,一步步搭出这些能力。但也不打算从「什么是数据库」讲起,基础概念默认读者已经掌握。

如果你完全没有后端经验,建议先读卷一的账号、BFF、API 三章,再回到本卷。卷一的代码和本卷在同一个实验工程里,不需要重新搭环境。

13 章的组织方式

本卷 13 章按「内容本身 → 内容分发 → 关系与互动 → 实时通道 → 收口」排列:

  1. 内容模型:文本、图片、视频的元数据,内容状态机,冷热分层。
  2. 发布与审核:同步与异步的分界,机审、人审、复审,失败体验。
  3. Feed 模型:推、拉、混合,写扩散成本,多源合并。
  4. Feed 排序:时间、热度、亲和度,去重折叠,实验分桶。
  5. 社交关系:关注、好友、拉黑,可见性判定链,关系图查询。
  6. 计数与互动:点赞、收藏、计数一致性,频控。
  7. 评论:评论树、分页、折叠、通知触发边界。
  8. IM 消息模型:会话、序号、单聊群聊、存储与漫游。
  9. IM 投递:在线离线、多端、已读合并、去重。
  10. 推送与通知:渠道能力、模板、聚合、频控、降级。
  11. 搜索接入:索引同步、召回排序过滤、删除与墓碑。
  12. 推荐接入:服务边界、特征与候选、冷启动兜底。
  13. 收口:发布到通知的端到端链路、IM 与未读验证、质量指标。

每一章的三节之间有明确的递进关系:第一节定义问题与模型,第二节给出主要实现与取舍,第三节处理失败场景与边界。不建议只读第三节,因为失败场景依赖前两节的定义。

每章的结构

为了让读者能快速判断「这一章要不要细读」,每章都遵循相似的结构:

  1. 问题开场:这一章要解决的产品现象,通常是一个具体的用户抱怨或线上事故。
  2. 模型与判定:把现象拆成状态、关系、判定规则,给出最小数据模型。
  3. 主实现:一段可运行的 Go 代码,配套测试与运行输出。
  4. 取舍与替代方案:至少两个备选方案,说明各自在什么条件下更合适。
  5. 失败场景:一个具体的故障,说明它如何发生、用户看到什么、怎么定位。
  6. 小结与边界:这一章不解决什么,留给哪一章。

不是每一节都完整包含六部分,但「失败场景」和「取舍」是每节都有的。如果某一节只讲了「怎么做」而没讲「什么时候不该这么做」,那多半是我写漏了。

实验工程

本书的代码在 webapp-lab 实验工程里,module 名为 webapp,Go 版本 1.26.0。工程按章分包:

webapp-lab/
  go.mod
  internal/
    content/      # 第 1 章:内容模型
    moderate/     # 第 2 章:发布与审核
    feed/         # 第 3 章:Feed 模型
    rank/         # 第 4 章:Feed 排序
    social/       # 第 5 章:社交关系
    counter/      # 第 6 章:计数与互动
    comment/      # 第 7 章:评论
    im/           # 第 8 章:IM 消息模型
    delivery/     # 第 9 章:IM 投递
    push/         # 第 10 章:推送与通知
    search/       # 第 11 章:搜索接入
    reco/         # 第 12 章:推荐接入
    socialbase/   # 第 13 章:收口

每个包只依赖标准库,不引入第三方框架。这样做的原因不是排斥框架,而是让读者能看清逻辑本身:可见性判定不该被 ORM 的查询构造器遮住,去重窗口不该被消息队列的重试策略遮住。

运行某一章的测试:

go test ./internal/feed/ -count=1
go test ./internal/im/ ./internal/delivery/ -count=1

运行全卷测试:

go test ./... -count=1

如果只想看某一节的代码,正文里的代码块开头都有三行标记:

// file: internal/feed/hybrid.go
// tier: L1
// verify: go test ./internal/feed/

file 是文件路径,tier 是代码分级,verify 是验证命令。这三行不是注释装饰,而是让读者能把正文代码和工程文件对上。

代码分级

本书用四级标注代码的可运行程度:

级别含义依赖本卷情况
L1单包纯逻辑仅标准库第 1–12 章全部
L2进程内端到端httptest、内存适配器第 13 章
L3真依赖数据库、缓存、消息队列本卷不做
L4多节点、故障注入容器、网络工具本卷不做

L1 代码可以直接复制到一个空包里跑。L2 代码需要一个 HTTP handler,但仍在单进程内。L3 及以上需要真实环境,本书不承诺这些代码可以直接上生产。

这不是偷懒。L3 代码的正确性高度依赖具体中间件版本、配置和网络条件,写进书里很容易变成「在我机器上能跑」。与其给一份看起来完整但无法复现的实现,不如把 L1/L2 写扎实,把 L3 的取舍留到卷四讨论。

数字与版本口径

本书对数字的使用有三条规则:

  1. 实测就是实测。 标注「实测」的输出,都在本机真实执行过,命令和结果一起给出。
  2. 示例就是示例。 案例里的 DAU、粉丝数、消息量、存储量都是假设值,用于说明数量级关系,不代表任何真实产品。
  3. 预期就是预期。 没有跑过的结果,写「预期」「应当」,不写「实测」。

版本口径:

  • Go 1.26.0,module webapp。
  • 时间统一用秒或毫秒,存储统一用 GB(1 TB = 1024 GB),金额统一用元。
  • 所有阈值都是起点,不是结论。换产品、换规模、换用户结构,都要重新标定。

读者如果发现某个数字和自己环境差很多,先看这个数字是实测还是示例。如果是示例,它只用来表达比例关系;如果是实测,注意机器配置和并发条件,这些在对应章节里会写。

与站内其他内容的关系

站内已经有一些内容社区、Feed、推荐、IM 相关的文章。本书不复述它们的定义和清单,只在需要时用一段话交代结论,并给出链接。

本书新增的是三样东西:

  • 把能力放回「回声」的具体阶段,说明为什么现在做、不现在做会怎样。
  • 每一节都有可运行的 Go 代码和真实输出。
  • 每一节都有一个失败场景,说明做错了用户会看到什么。

如果你已经在站内读过 Feed 或 IM 的综述文章,可以跳过概念部分,直接看「判定」「取舍」「失败场景」三节。

怎么读比较快

三种读法,按目的选:

按主循环读。 第 1 → 2 → 3 → 4 → 6 → 7 → 10 → 13 章。这条线回答「一条内容从发布到被消费、被互动、被通知,中间发生了什么」。适合第一次接触内容社区的读者。

按故障读。 第 5 → 9 → 10 → 11 → 13 章。这条线回答「可见性、投递、推送、搜索删除这几类事故怎么定位」。适合正在处理线上问题的读者。

按章节读。 每章独立,包也独立。适合只想解决一个具体问题的读者。但建议至少先读第 1 章和第 5 章,因为后面的章节会反复引用内容状态与关系判定。

怎么判断自己读懂了

每章末尾都有「小结与边界」。读完一章,试着回答三个问题:

  1. 这一章的核心状态或数据,谁拥有写权限?谁只能读?
  2. 这条链路失败时,用户看到的是错误、空列表、旧数据,还是重复数据?
  3. 如果流量涨十倍,先坏的是哪一段?这一章有没有给出判据?

三个问题都能用自己的话回答,并且能指出代码里对应的函数,才算读懂。只记住「用了推模式」「加了缓存」不算。

环境准备

实验工程只依赖 Go 标准库,不需要数据库、缓存或消息队列。准备一个 Go 1.26.0 的环境即可:

go version
# go version go1.26.0 darwin/arm64

进入工程目录后,先跑一次全量测试,确认环境正常:

go test ./... -count=1

第一次运行会编译所有包,时间稍长;之后有构建缓存,单包测试通常在几百毫秒内完成。正文里给出的运行输出都来自本机 -count=1 的结果,避免缓存影响。

如果读者的 Go 版本低于 1.26,代码里的某些标准库用法可能不可用。本书尽量不用新版本特性,但版本口径以 1.26.0 为准。遇到编译错误时,先确认版本,再检查是否漏了 internal 包的导入路径。

代码阅读顺序

正文里的代码块通常只展示一个文件的核心部分,完整文件在实验工程里。建议的阅读顺序是:

  1. 先看类型定义。结构体字段决定了模型能表达什么,很多问题在读字段时就能发现。
  2. 再看入口函数。它是这一节的主判定或主流程,决定了代码的骨架。
  3. 最后看测试。测试用例是这一节的行为契约,比注释更准确。

如果时间有限,可以只读类型定义和测试。类型定义告诉你「系统认为世界上有哪些东西」,测试告诉你「这些东在什么情况下应该怎样」。两者合起来,比逐行读实现更高效。

代码块开头的 file 路径可以直接在工程里找到对应文件。正文代码与工程文件逐字节一致,如果发现不一致,以工程文件为准,并把它当作勘误。

命名与错误处理约定

为了让不同章节的代码读起来一致,本书遵循几条命名约定:

  • 涉及权限判定时,主语用 Owner,访问者用 Viewer,关系用 Relation。
  • 涉及消息时,会话用 Conversation,消息用 Message,序号用 Seq。
  • 涉及计数时,目标对象用 Target,计数键用 Key。
  • 错误统一用 error 返回,不 panic;测试里用 errors.Is 断言哨兵错误。
  • 时间统一用 int64 的 Unix 秒或毫秒,字段名带单位后缀,如 CreatedAt、TTLSeconds。

这些约定不是行业标准,只是为了让读者在跨章阅读时不用重新理解命名。读者在自己的项目里可以用别的约定,但建议保持「单位写进字段名」这一条,它能避免大量时间单位错误。

错误处理上,本书区分三类失败:

  • 参数错误:调用方传了不合法的输入,返回 error,不改变任何状态。
  • 业务拒绝:输入合法但当前状态不允许,返回带原因的 error,由上层决定如何展示。
  • 系统失败:依赖不可用、超时、数据不一致,返回 error,由上层决定重试或降级。

正文在讲失败场景时,会说明它属于哪一类。分类的意义在于:参数错误不该重试,业务拒绝不该告警,系统失败才需要重试和降级。

表格怎么读

本书的表格大致分三类,读法不同:

  • 对比表:列出多个方案在若干维度上的差异。不要只看哪一列「更好」,要看自己的约束落在哪一列。
  • 参数表:列出阈值、默认值、单位。这些值是起点,不是结论;换产品要重新标定。
  • 状态表:列出状态、允许的操作、触发条件。它是状态机的另一种写法,读的时候要检查「有没有状态没有出口」。

表格里的数字如果是示例,会在表前或表后注明。没有注明的阈值,通常是代码里的默认值,可以在实验工程里找到对应常量。

已知的取舍

写这本书时,我刻意做了一些取舍,提前说明,避免读者误会:

  • 不追求覆盖所有方案。 推拉模式只讲三种主流组合,不列举所有变体。
  • 不写中间件对比。 消息队列、搜索引擎、缓存的具体产品选型,各团队约束不同,书里只讲语义与边界。
  • 不写算法细节。 推荐只讲到候选合并与兜底,排序只讲到信号与实验,不展开模型训练。
  • 不写多机房。 多活、容灾、单元化留到卷四,因为那是规模化阶段的问题。

如果读者需要这些内容,站内有对应的专题文章,本书会在相关章节给出入口。

反馈与勘误

书中的代码都在实验工程里,读者可以直接改、直接跑。如果发现某段代码跑不通,或者某个结论与你的经验不符,欢迎指出。特别是「实测」类内容,如果你的环境和书里差很多,那很可能是我漏写了某个前提条件。

对于示例数字,不必纠结具体值,关注它表达的比例关系即可。对于阈值类建议,把它当作起点,用自己的数据重新标定。

致谢

感谢在写作过程中提供真实故障案例与评审意见的同事。书里的失败场景大多来自真实事故的抽象,有些细节做了改写,但故障模式没有编造。

也感谢卷一的读者,他们对「判定链」「失败语义」的反馈直接影响了本卷的写法。

如果这本书只能留下一个习惯,我希望是:在写任何一行与「可见」「可发」「可达」相关的代码之前,先把判定条件写成一句话,再写测试。代码可以重构,判定错了,重构只会让错误更隐蔽。

具体来说,写代码前先回答:

  • 这条数据的 owner 是谁,写权限在哪个服务?
  • 这个接口的 viewer 可能来自哪些关系,判定顺序是什么?
  • 这次失败应该返回错误、返回空,还是返回旧数据?

三个问题写下来通常不超过十行,但它们决定了后面几百行代码的形状。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查