Skip to content

25 项目线二:业务实战主线与 minifeed 蓝图 ​

详细程度:项目 · 约 30 分钟 材料:W02-1 到 W21-1 全部周次(W01-1 缺失,对应 Go 基础语法)、WX-sms · 旧长版:无 前置:手册 04 到 20 节至少读过一遍 说明:本节是设计蓝图,不依赖仓库里已有实现是否完整;写到具体文件或函数时都真实存在于 minifeed/

先看一个问题:这个系统该从哪一行代码开始写起 ​

资料里的 21 周课程是同一个业务项目(内容社区,仓库名 webook)的完整成长史:从单体的注册登录起步,加缓存,加消息队列,上可观测,拆微服务,做数据迁移,再接搜索、Feed、IM。21 周听完,最值钱的不是任何一周的具体代码,而是一个系统长大的顺序:什么时候加缓存、什么时候拆服务、什么时候动数据库,每一步都对应着一个真实的痛点,不痛不加。

还原一下当时的场景。你入职一个新团队,老板给你一个空仓库:"做个内容社区,先能注册登录发帖,后面会火。"你面对的不是技术选型清单,是一串顺序问题:第一刀切在哪(先写哪一层)?什么时候才值得引入 Redis?帖子从 10 万涨到 1000 万,数据库要动什么?用户量过百万,要不要拆微服务?每一步都有人催你"早点上",也都有人警告你"别过度设计"。这一节按这条主线走一遍,每一步给出"痛点是什么、为什么这时候引入",然后你自己在 minifeed/ 上把最小的一刀做出来。

先把全景铺开。技术栈按课程出现顺序:Gin、GORM 与 MySQL、Redis(含 Lua 脚本)、Kafka、Prometheus 与 Grafana、OpenTelemetry 与 Zipkin、etcd、gRPC 与 Protobuf、Elasticsearch、Canal、K8s;装配用 wire,配置用 viper,日志用 zap。主线分层是 Handler → Service → Repository → (DAO + Cache)。〔W02-2 p14-17〕〔W21-1 p25〕 这条分层里最实在的一条设计结论:Repository 负责"一切与存储有关的事",包括缓存一致性、预加载、存储层降级;缓存与否、怎么缓存是技术问题,不是业务逻辑,所以放 Repository 不放 Service。〔W21-1 p25〕这与 10 节的分层原则是同一句话的两种说法。

一、21 周与手册各节的对应 ​

周做什么手册节
W02Gin 入门、用户注册登录;GORM 与三层结构10、11、12
W03 上Session 与 JWT;注册登录的限流保护11(认证小节)、16
W03 下K8s 部署入门23
W04登录性能与缓存;短信验证码登录;wire 依赖注入;单元与集成测试;第三方服务调用治理(限流、failover)13、10、14、16
W05微信扫码登录(OAuth2);长短 token;配置模块;日志模块11(OAuth2 与长短 token)、10、15
W06发帖子(全程 TDD);状态管理与 MongoDB 存储;查询与缓存14(TDD 小节)、10(状态管理)、13
W07阅读、点赞、收藏;Kafka 改造13
W08Prometheus;OpenTelemetry15
W09热榜(小顶堆加定时计算);分布式任务调度13
W10微服务前期准备(DDD、gRPC、Protobuf);拆分与微服务化19、11
W11数据迁移(双写与校验)17
W12 到 W13服务注册与发现;负载均衡20
W14熔断、限流、降级16
W15支付与打赏18、13
W16评论;用户关系(关注与计数)12(树形评论)、13、21
W17 到 W18搜索与标签;ELK;Canal21、15、17
W19Feed 流与压测21
W20IM 与 OpenIM21
W21总结:设计原则、设计模式、DDD、缓存策略、微服务04、10、13、19

读法建议:不要按周刷。先读 10 节拿到分层骨架,再按这张表找"这一周在解决什么问题",需要细节时才进对应节。11 HTTP 与 gRPC 是服务间通信的底座,20 注册发现与负载均衡在拆分后立刻需要;这两节的细节不进周次表,随用随查。

二、五条演进规则,每条讲清为什么 ​

规则一:重构先补测试。 覆盖率 80% 以上再动手,集成测试比单元测试更重要,因为它真的打到数据库和 Redis。为什么这么强调?因为后续每一条规则(拆服务、双写迁移、换缓存)都是"改在跑的系统",重构必然引入 bug,要问的不是"会不会出 bug",是"出了怎么办"。课程的做法:单元测试保逻辑分支,集成测试保"SQL 真的能跑、缓存真的会失效";前者快、随手写,后者慢、要起容器,但只有它能在重构前替你证明现状是好的。〔W10-2 p12-13〕14 节的测试金字塔在这里是施工规范不是知识。

规则二:拆微服务,先挑最不重要的模块。 直觉是拆"用户"这种核心,反了。资料的标准是三个条件按序筛:业务影响力小(崩了无所谓)、依赖最少(拆它不牵动别人)、QPS 低(拆的过程压测好做)。按这套筛子,用户和帖子不能选(核心依赖),短信不能选(登录要用),热榜不能选(QPS 高),最后选了点赞收藏。〔W10-2 p12〕拆分本身有八步固定动作:选模块、补测试、拆代码、技术选型、改为微服务对外、本地与微服务双调、流量逐步切到 100%、删除本地调用。其中第六步是整套流程的安全阀:两套调用都在,开关控流,微服务出问题立刻把流量切回本地调用,随时可回滚。〔W10-2 p7〕19 节有这八步的逐步展开,包括第六步为什么要留一个中间态。

规则三:不停机迁移数据,靠双写加反复校验。 四个阶段:读源、双写以源为准、双写以目标为准、只读写目标。难点不是搬运,是"迁移期间数据一直在变":存量搬完了,增量还在来;等增量追平,之前的存量又被改了。所以方案里没有"校验一次"这个动作,只有"增量校验持续跑、不一致就修复"这个循环,监控保证双写失败只是偶发,靠校验修复兜底。〔W11-1 p9-10〕增量从哪来?定时任务全量比对、binlog 的 CDC(Canal 伪装成 MySQL 从库收 binlog 再转发)各有适用场景,一个准实时一个实时,17 节展开。18 节的最终一致性是它的理论底座:双写方案默认"最终一致,靠校验收敛",先接受这一点,方案才立得住。

规则四:一切不在你控制范围内的调用都要容错。 第三方短信、支付、别的团队的接口,都是失败域:你不知道它什么时候挂,也不知道挂了会拖你多久。做法有固定顺序:先限流(防自己被打死),再用装饰器无侵入地叠加超时与重试(不改原有实现,调用方无感知),最后 failover(换一个备用通道)。〔W04-6 p3-5〕〔W04-6 p10〕〔W04-6 p21〕16 节把这三种阀门的位置讲全了:服务端放限流熔断保护自己,客户端放超时重试 failover 保护自己不被下游拖死。顺序不是随意的:不先限流,重试会放大流量;不先有超时,failover 永远轮不到执行。

规则五:没有强制同步就用异步。 Feed 明面上"实时性要求高",实际要求是秒级甚至十秒级;消息队列带来解耦、削峰和更高的可用性,代价是延迟与最终一致。〔W19-1 p35〕判断标准只有一条:业务上有"必须立刻看到"吗?点赞计数、Feed 刷新、榜单更新都没有;登录密码校验有。21 节把这个判断展开成了推拉结合的具体设计。异步化的另一半是幂等:队列会重复投递,每条消费逻辑都要先回答"做两遍会怎样",这是 13 节的主题。

什么时候才值得引入:一张对照表 ​

课程的每一周都由上一周的痛点逼出来。把"痛点、引入、判断信号"对齐看,比按周刷快得多:

痛点引入什么判断信号
登录接口每次都查库,数据库 CPU 打满缓存(W04,13 节)同一查询 QPS 高且数据变化不频繁;命中率能算出来
点赞数直接写库,读接口被顺手的写拖垮计数进内存加批量落库(W07,13 节)读 QPS 远大于写,写丢失可容忍窗口
出问题只能翻服务器日志指标加结构化日志加链路(W08,15 节)排障时间开始超过开发时间
单体部署互相影响,发布要整体停机拆微服务(W10,19 节)模块间发布节奏冲突,或资源画像差异大
换库或分表需求出现双写迁移(W11,17 节)存量数据不可停机搬运
实例数超过 2,配置文件写死 IP注册发现加负载均衡(W12,20 节)手工维护实例列表开始出错
下游抖一下全站雪崩熔断限流降级(W14,16 节)一个节点的故障沿调用链放大过

这张表也是 minifeed 阶段表的逻辑:每个阶段对应一个"痛点、引入"对,先有痛再动手。

现有骨架给的分阶段路线 ​

阶段做什么验收标准(go test -race 自动判定)对应手册节
1 单体分层user:注册(auth.Hash() 盐哈希)、登录(auth.SignToken() HMAC token);handler 收口在 minifeed/server.gohandler 层测状态码约定(400 参数错、409 占用、201 成功);存储层双实现跑同一组测试10、11、12
2 发帖post:PostStore.Create() 与按时间倒序的 List(),内存与 SQLite 双实现双实现过同一套测试(post 包已示范);并发写入 -race 干净12、14
3 计数counter:点赞进内存、定时批量落库(MemoryCounter 的 flush 回调)TestConcurrentLike 并发不丢计数;落库失败不阻塞点赞主路径13
4 热榜rank:定时刷新 + 单飞(Refresh 里 mutex 合并并发触发)+ 刷新失败沿用旧榜TestRefreshTTL 过期后重算;TestConcurrentRefresh 只算一次13
5 关注与 Feed(你来写)follow 中间表(唯一索引 follower 在前);推拉结合:取粉丝列表多取一个,超过阈值只写发件箱重复关注结果不变(幂等);大 V 发帖只产生 1 次写;合并排序分页稳定21、12
6 可观测slog 带请求 ID(对照 dnsproxi-go/logger.go 的 ctxHandler);自写计数器与直方图同一请求所有日志同一 ID;标签只有有限枚举15
7 稳定性登录限流;对"短信"这类外部依赖做超时、重试、熔断超阈值返回 429;外部依赖挂掉快速失败而不是堆积 goroutine16
8 拆分与迁移把 post 拆独立进程(先内部接口再 HTTP);SQLite 到另一库的双写迁移演练开关切换流量可回滚;双写校验发现不一致能修复17、19

现有骨架的代码里藏着几处可以对照课文的注解:server.go 的 rankSource 是热榜"分数来自计数、标题来自帖子表"的 Repository 组装(W09-1);handleLike 只动内存不夹 DB 写,是"读请求不夹写"的案例(W07-2);rank.Refresh() 的单飞与降级是"定时计算加可用性设计"的最小版(W09-1)。读它们时对照 13 节的相关小节,比裸读课文快。

前四个阶段抄什么:从骨架代码反推设计 ​

阶段 1 到 4 的实现已经在 minifeed/ 里,做它们的意义是"读懂一版正确答案"。每个阶段有一个值得停下来想一分钟的决策:

阶段 1(用户与登录)。密码为什么存盐哈希而不是加密?因为验证只需要比对,不需要还原,哈希把"拖库后直接得到明文"变成"拖库后还要暴力破解"。token 为什么是自签 HMAC 而不是 Session?因为 API 服务多实例部署时 Session 需要共享存储,token 只需要每个实例持有同一个密钥。对应课文:auth.Hash() 与 auth.SignToken() 的实现加注释里的 W02-1。

阶段 2(发帖)。为什么 PostStore 要内存和 SQLite 两个实现?因为"帖子怎么存"是可替换的存储细节,Handler 和 Service 都不该知道用的是哪个;双实现同测还白送一个好处:逻辑错误在内存实现上更容易暴露。对应课文:12 节的存储层抽象,post/post_test.go 里 TestMemoryList 与 TestSQLiteList 跑同一组断言。

阶段 3(计数)。为什么点赞先进内存、定时批量落库?1000 QPS 的点赞如果每次都 UPDATE,数据库光写这一件事就要扛 1000 QPS;批量合并后可能只要每秒 10 次。代价是崩溃丢最近一个窗口的计数,这是可接受的取舍:点赞数不需要精确。对应课文:13 节计数部分,counter 包的 flush 回调。

阶段 4(热榜)。为什么榜单是定时算而不是实时算?实时算意味着每次读都全表扫描加全局排序,千万级数据下磁盘和带宽都扛不住;定时算把计算成本摊平成"每 N 秒一次",读的时候只碰缓存。两个配套件:单飞(并发触发只算一次)和降级(刷新失败沿用旧榜,首页不能白屏)。对应课文:13 节热榜部分,rank.Refresh() 的实现。

读这四个阶段时带着一个问题走:每个决策把什么换成了什么?性能换一致性(阶段 3)、实时性换可用性(阶段 4)、开发速度换可测试性(阶段 2)。说不出换出去的是什么,说明还没读懂这个决策。

阶段 5 的设计走一遍:Feed 的决策点 ​

阶段 5 是唯一没有现成实现的,值得把决策链完整走一遍。问题:帖子创建成功(handleCreatePost)之后,粉丝怎么看到这条帖子?

方案 A 全推(写扩散):发帖时给每个粉丝的收件箱写一条。读快(收件箱就是结果),写随粉丝数线性涨:2000 粉丝的作者发一次帖写 2000 次。方案 B 全拉(读扩散):只写自己的发件箱,粉丝读时合并所有关注对象的发件箱再排序。写恒定 1 次,读随关注数涨。21 节给过这两个缺陷的数值版:2000 粉推,写 2000 次;300 万粉拉,读时合并两路。

生产答案是推拉结合:普通作者走推,大 V 走拉,阈值按压测数据定,再叠加活跃用户过滤(不活跃粉丝连收件箱都不用推)。这个"阈值判断"落到接口上就是一个小技巧:Followers 查询的 limit 参数多取一个,返回数超过 limit 就说明粉丝数超过阈值,走拉。判定成本 O(1),不需要维护精确计数。读路径的合并顺序、分页稳定性(同一页不能重复或漏),21 节的内存版实现可以直接抄过来改造。

两个附带的决策。一是唯一索引列顺序:follow 表的查询主形态是"查我关注了谁"和"查谁关注了我",两个方向都要快,唯一索引 follower 在前,WHERE follower = ? 才能命中;写反了就是全表扫。〔W16-2 p11〕二是幂等:重复关注不能报错也不能产生两行,FollowRepo 的 Follow 用唯一索引天然幂等,重复调用返回成功,这是 13 节"唯一索引去重"的直接应用。

你要写的两个接口 ​

关注关系与 Feed 的接口骨架(进程内事件总线先用 channel,换 Kafka 只动这一处):

go
type FollowRepo interface {
	Follow(ctx context.Context, follower, followee int64) error   // 幂等:重复调用不报错
	Unfollow(ctx context.Context, follower, followee int64) error
	Followers(ctx context.Context, followee int64, limit int, afterID int64) ([]int64, error)
	Followees(ctx context.Context, follower int64) ([]int64, error)
}

type FeedStore interface {
	PushInbox(ctx context.Context, uid int64, p Post) error     // 写扩散:粉丝收件箱
	PushOutbox(ctx context.Context, author int64, p Post) error // 读扩散:大 V 发件箱
	Inbox(ctx context.Context, uid int64, limit int) ([]Post, error)
	Outbox(ctx context.Context, author int64, limit int) ([]Post, error)
}

type Bus interface {
	Publish(ctx context.Context, topic string, v any) error
	Subscribe(topic string, h func(ctx context.Context, v any) error)
}

OnPostCreated 的决策逻辑是 21 节推拉结合的直译:Followers 多取一个,取满了说明粉丝数超过阈值,只 PushOutbox 一次;否则逐个 PushInbox。为什么多取一个?因为你需要在 O(1) 内回答"粉丝数是否超过阈值",而准确计数要么维护计数器要么读全表,都太重;"取 limit+1 看有没有多的"是列表场景的标准技巧。

做的时候要守的纪律 ​

  • 先写接口再写实现,包之间只依赖接口,server.go 是唯一知道具体类型的地方(10 节的手写 DI)。
  • 阶段 8 之前不要拆进程;拆的时候先补集成测试(规则一)。
  • 每个阶段结束项目都可运行、测试全绿;上一阶段不绿就不开下一阶段。

阶段 8 的双写演练怎么设计 ​

阶段 8 的迁移部分最容易做成"背课文"。给它一个可验证的最小形态:现有 user 表的存储从 SQLite 换成另一个 SQLite 文件(模拟换库),用 17 节的四阶段走一遍。写一个 DualStore,实现和 PostStore 一样的接口:内部持有源和目标两个存储,写路径按阶段开关决定写哪边、以哪边为准;读路径按阶段开关决定读哪边。再写一个校验循环:每 N 秒比对两边同一批 ID 的字段值,不一致就打日志并修复。四个阶段切换靠一个枚举字段,测试里显式推进阶段并断言每个阶段的读写行为。这个练习做完,17 节的"双写、校验、修复、切换"就不再是名词,你还会遇到真实的坑:双写失败一边怎么办(记录后继续,靠校验修复)、自增 ID 怎么对齐(显式指定主键)、校验本身打满数据库怎么办(限速分批)。

小结 ​

  1. 21 周课程的价值是"系统长大的顺序":分层、缓存、消息队列、可观测、拆服务、迁移、搜索、Feed、IM,每一步都由前一步的痛点逼出来;对照表比周次表好用。
  2. 分层主线 Handler、Service、Repository、DAO 加 Cache;缓存属于存储范畴,放 Repository 不放 Service。〔W21-1 p25〕
  3. 五条演进规则:重构先补测试;拆服务先挑最不重要的模块并保留回滚开关;不停机迁移靠双写加持续增量校验;外部调用一律容错(限流、装饰器、failover 有固定顺序);没有强制同步就用异步。
  4. minifeed 是主线的练习场:已有五个包的起步骨架,你要补的是关注与 Feed,按八个阶段推进,每个阶段用 go test -race 验收。
  5. 推拉结合的判定技巧:列表多取一个,O(1) 判断是否超过阈值;阈值只能来自压测数据,拍脑袋的阈值都是猜。

小结备查:每个阶段最容易迷路的地方 ​

阶段最容易迷路的地方拉回来的办法
1 单体分层Handler 直接写 SQL,Service 知道缓存存在构造函数只收接口;缓存参数不出现在 Service
2 发帖状态机散落成 if-else状态迁移集中一个函数,非法迁移返回错误
3 计数落库失败把点赞主路径堵死flush 回调只记日志,计数在内存继续
4 热榜刷新失败返回错误拖垮首页沿用旧榜,记一条 WARN
5 Feed阈值拍脑袋压测三档数据量再定;先只做推,阈值后补
6 可观测标签里塞用户 ID标签枚举写进代码注释,review 时对照
7 稳定性重试没有上限每层最多 3 次,预算用 ctx 传递
8 拆分迁移一口气回滚不了开关先行,第六步的中间态必须真实存在

常见坑 ​

  1. 一开始就拆微服务,没有测试保护,回滚没有开关。
  2. 把缓存逻辑写进 Service:业务代码被存储细节污染,换缓存要改业务。
  3. 关注关系的唯一索引列顺序写反:WHERE follower = ? 全表扫。〔W16-2 p11〕
  4. 大 V 全部写扩散:一次发文瞬时百万写。〔W19-1 p18〕
  5. 迁移只校验一次:迁移期间数据还在变,必须增量校验加修复循环。
  6. 异步化没有边界:把"必须同步看到结果"的路径(登录密码校验)也扔进队列。
  7. Followers 每次全量拉取不分页:粉丝百万时一次查询就是几百 MB,列表接口一律带 limit 加游标。
  8. 热榜候选集取全表:先按业务缩小范围(最近 N 天、有效状态),再进堆和排序。

判断题与答案 ​

先自己答,再看后面。

  1. 资料为什么选"点赞收藏"作为第一个拆出去的模块,而不是"用户"?
  2. 双写迁移的第二阶段和第三阶段,区别是什么?为什么不直接从双写跳到只写目标?
  3. minifeed 的 FeedStore 为什么要同时有 Inbox 和 Outbox 两套?

答案

  1. 三个筛选条件:业务影响力小(崩了无所谓)、最独立(依赖少)、QPS 低。用户是核心依赖,点赞收藏三者全占。〔W10-2 p12〕
  2. 第二阶段读源表、先写源表(以源为准),第三阶段读目标表、先写目标表(以目标为准)。直接跳意味着"读哪边"的语义在同一个部署里模糊,出问题无法定位以哪边为准;分两个阶段让读源切换成一个明确的、可回滚的开关。
  3. 推拉结合要求两种路径并存:普通作者走 Inbox(写扩散,读时直接拿),大 V 走 Outbox(读扩散,粉丝读时合并拉取)。只留一套,要么写放大要么读放大,见 21 节。

在项目里 ​

dnsproxi-go 与 minifeed 互补:24 节展示"一个小系统的内部机制"(缓存、逐出、成帧、背压),本节展示"一个业务系统的外部演进"(分层、异步、治理、迁移)。两边的交集是缓存:dnsproxi 的缓存自己管理生命周期,minifeed 的计数器用 flush 回调延迟落库,一个是进程内缓存,一个是"缓存当作主存储、DB 当持久化"的写回模式,两种取舍都值得写进你自己的笔记。

想深入:正文之外的干货 ​

W21-1 课程总结:把 21 周收进 11 页的判断题 ​

  • 面向接口的定义判据不是"现在有几个实现",是"预期维护期内会不会有新实现":单实现但预期换(比如短信要接多家服务商)就定义接口;单实现且预期稳定就用具体类型;反过来,要依赖实现细节(比如依赖缓存预加载这个特性)时才用具体实现。接口是为预期中的变化留的口子,不是仪式。〔W21-1 p3-4〕
  • 依赖注入两条铁律:A 用到 B,B 就是 A 的字段;B 是字段,就一定在构造 A 时从外部传入。minifeed 的 server 结构体和 openStore() 就是这两条的直译,server.go 是全项目唯一知道具体类型的地方。〔W21-1 p5〕
  • 单一职责落到接口上就是"方法寥寥几个且同类";CQRS 是它的读写特例:读写方法分散到不同接口,最大好处是分别治理,出问题可以只降级写接口、限流时读阈值高写阈值低。搜索服务的 SearchService 与 SyncService 拆分是课程范例。〔W21-1 p6-7〕
  • 开闭原则的落地形态是装饰器:给 sms 加可观测性不改旧代码,包一个新实现。反过来,"加一个 if-else 分支"往往就是违背它的信号。〔W21-1 p8〕
  • 超前设计但不超前实现,度量单位是迭代:接下来两三个迭代内的变更,设计时留口子;更远的变更,纳入代价低才考虑、代价高就放弃。全盘考虑的结果是复杂度爆炸,人力扛不住。〔W21-1 p9-10〕
  • 异步优先是规模逼出来的思维转换:系统一大,稳定性、可用性、性能问题突出,就该"不到逼不得已不用同步",能用 Kafka 完成的不要用同步 gRPC 或 HTTP,因为消息队列好治理、性能好。〔W21-1 p11〕
  • 装饰器不断叠加就是洋葱模式:越靠内核越核心,初始化 sms 时在最基础实现外包可观测、容错、鉴权,大部分中间件设计核心都是一个洋葱。课程在可观测性、容错、failover、ratelimit、metric、otel 等目录里大规模用了这套模式。〔W21-1 p13-14〕
  • 构造参数多了怎么选:有点复杂但不特别复杂的用 Option(函数式实现,WithXXX 返回函数,New 里逐个应用),特别复杂的用 Builder(链式调用返回 Builder 本身)。gRPC 的 Dialoption 是接口式 Option 的范例。〔W21-1 p16-17〕
  • gRPC 装饰器的向后兼容靠组合:嵌入 UnimplementedSmsServiceServer,只装饰必要的方法,protobuf 之后加新方法照样编译通过。〔W21-1 p19〕Gin 的 middleware 是责任链的一环:当前环节处理完调 ctx.Next() 执行下一环,defer 里统一统计。〔W21-1 p20〕
  • 缓存一致性选"先更新 DB 再删缓存"要想清楚它的真实属性:缓解不是解决。极端交叠下(一个读请求拿旧值回写缓存的速度慢于另一个请求写库删 key)缓存里会留下旧值,只是概率小,因为读库回写通常比写库删 key 快。要求强一致就得分布式锁或版本号。〔W21-1 p28〕
  • 分页接口很难缓存,可用的一招是只缓存第一页:第一页访问频率远高于后面所有页,收益立现;还能叠加业务预加载,把第一页前几条的详情也提前放进缓存。〔W21-1 p29-30〕
  • 更常用的是应用启动预加载:toB 场景标签服务在启动时把缓存灌满。预加载本地缓存是每个节点各自加载一遍;预加载 Redis 只需一个节点做一次。〔W21-1 p31〕
  • 性能苛刻的场景上三级结构,本地缓存、Redis、DB,读序本地到 Redis 到 DB,写序 DB 到本地到 Redis。先写本地后写 Redis 不是教条:本地操作几乎不可能失败,本地写成功 Redis 失败只是短暂不一致,读侧有本地兜底。〔W21-1 p32〕
  • 本地缓存的默认答案是别用:命中率低、占内存。只有两个场景值得:三级结构,或者当 Redis 的故障备份(平时走 Redis 到 DB,Redis 崩溃临时走本地到 DB,恢复后切回)。〔W21-1 p33〕
  • 注册发现面试必强调容错:客户端尽可能用本地缓存的节点数据,发现节点不可用先挪走,之后还要考虑挪回来。负载均衡的本质是找到预期最快返回的节点,静态算法是基础,关键在动态调权重(步长、上下限),再和熔断限流降级整合成复杂策略。〔W21-1 p36-37〕
  • 熔断和降级一句话分清:熔断是触发后直接拒绝所有请求,保护自己;降级是尽可能给个响应,预配默认值或走快慢路径里的快路径,保住体验。〔W21-1 p38-39〕
  • 限流算法固定窗口、滑动窗口、令牌桶、漏桶四选;"拒绝"有三种去向:转异步后续处理、返回特定错误、转发别的节点。限流熔断降级还能和缓存结合:触发时只查缓存,Redis 命中就返回,没数据才查库或直接降级。〔W21-1 p40-41〕
  • 治理是组合拳:触发后客户端直接换节点,还能把该节点权重降到极低让后续请求不再选它;客户端治理的核心是识别第三方有没有问题(超时、响应时间、错误响应、错误码四个判据,服务端自己看硬件资源和性能数据,还能通过响应元数据或定期上报把指标传出去)。判定出问题后的三板斧:同步转异步、换节点、返回默认值。〔W21-1 p42-44〕
  • 要不要微服务的原话值得记住:不是为了 KPI,只有走投无路才考虑。微服务对团队和运维要求都高,准备不充分贸然推行容易翻车。这条是规则二的元规则。〔W21-1 p46〕
  • 最佳实践七分客观三分主观,遇到先问三句:客观的还是主观的(缩进风格是主观的)、适不适配公司现状(大厂高并发实践未必适合你)、什么时候提出的(十年前的实践受制于当年性能)。归结为不盲从。〔W21-1 p47〕
  • TDD 的分界:工具库和基础中间件以单元测试为主,业务开发以集成测试为主;坚持用,除非连写测试的时间都没有。规则一说的"集成测试更重要"就是这条分界在业务项目里的应用。〔W21-1 p53〕

WX-sms 短信服务:把容错凑成一个完整业务 ​

  • 短信平台两头对外:对内收业务方请求(同步调用,或业务方直接发 MQ 异步),对外直连第三方服务商(腾讯、阿里);配套管理后台做查询、补发、单发群发定时发、营销短信和鉴权限额(比如某业务一个月 10 万条)。〔WX-sms p1-2〕
  • 可靠投递的根是入库先行:请求先落库,立刻尝试发送,失败交给定时扫描未成功记录的补偿任务补发。反过来直接调第三方,失败即丢,崩溃后无从追讨。代价是可能重复发送加部分非实时,都可接受。〔WX-sms p2-3〕
  • 重复请求靠去重加幂等回放:请求带 biz_type+biz_id 建唯一索引,重复请求来了查同键之前是否发送成功,成功返成功、失败返失败。去重键必须用业务语义,不能用手机号:同一个人两条不同业务的短信都要发。〔WX-sms p3〕
  • 灵活重试是按业务方配的:是否跨进程重试、间隔、次数各自不同;最终失败的消息丢进 MQ 让业务方订阅,再配告警(邮件、聊天机器人)告知终态。〔WX-sms p3-4〕
  • 平台自保护两件套:降级是高负载时同步转异步,给业务方"已受理"响应,业务方查接口或听消息拿结果;限流保护平台自己的入口,被限流的请求也转异步而不是直接报错。直接报错等于把压力变事故。〔WX-sms p4〕
  • 服务商容错三档递进:客户端限流(防触发服务商限流,触发了自动切换)、轮询 failover(超时或异常响应就切下一个)、健康判定 failover(按超时和异常判定不健康,临时移出,间隔试探,成功恢复、失败延长不健康时间,避免永久拉黑)。单用轮询会把流量反复打到坏节点上。〔WX-sms p4〕
  • 回查机制是事务消息的翻版:业务方先发"准备短信"不真发,业务成功提示后才发给用户,失败提示就删除,超时无提示就回查(回查 HTTP/RPC 接口,或丢 MQ 让业务方自查后回执)。它与事务消息、分布式事务联动是分布式架构的亮点,但实现复杂、要业务方配合改造,提一句即可别主动展开。〔WX-sms p4-5〕

第二轮补齐:散落在周次里的五处干货 ​

W02-2 三层落地的最后一环 ​

  • 调用链上每一层"薄"得有名堂:UserService.Signup 只透传 repo.Create,UserRepository.Create 只做 domain 到 dao 的类型转换再调 dao.Insert,真正的数据库操作集中在 db.WithContext(ctx).Create(&u)。层层薄透传不是浪费,是让"业务逻辑、存储抽象、SQL 细节"各自只有一个变化理由;哪层开始写业务,分层的钱就花在哪层。〔W02-2 p18〕

W10-2 方案设计一页纸与执行纪律 ​

  • 拆分方案先定边界:课程从 DDD 的“一领域一服务”出发,再用团队能掌握的业务细节范围校正粒度。边界要能被维护它的人理解。〔W10-2 p4〕
  • 路线二选一:先拆一个模块,能提前验证完整流程,但拆出去后难以停下;全部模块按模块化、依赖化、服务化推进,各阶段都能停下,问题却会集中到最后暴露。课程选先拆一个模块。〔W10-2 p6〕
  • 迁移方案写清八步与回滚中间态:补测试、拆代码、选框架之后,保留本地调用与微服务调用两条路径,用开关逐步切流;出问题就切回本地,稳定后才删旧调用。19 节展开整套步骤。〔W10-2 p7-8〕
  • 选模块先易后难:依次看业务影响力、依赖数量和 QPS,课程最终选了点赞收藏模块,具体理由见本节规则二。〔W10-2 p12〕
  • 模块化执行的两条纪律:充分利用 IDE 的重构功能挪目录,而不是手工剪切粘贴(挪错引用编译器立刻报);任何一个关键步骤之后都跑测试验证效果,测试绿了才走下一步。20 页这步只做"代码挪进独立目录、层结构原样保留",InitTables 挪移、消费者复制、wire 双份重生成是它后面的三颗雷,19 节已展开。〔W10-2 p20〕

W11-1 迁移这周的动机页 ​

  • 数据分离的动机:代码拆完后仍共享同一个逻辑与物理数据库,数据还没有随服务边界独立。课程因此继续拆分数据;数据迁移也是多数开发者会遇到的工作,值得在项目中练习。17 节展开迁移流程。〔W11-1 p4〕

W19-1 推拉结合的两个补注 ​

  • 综合方案的评价页给了两条 25 节设计没写透的边界:写侧的写放大还在,只是"能限制住写扩散的数据不多"(只有大 V 走拉之后的普通作者量);读侧的聚合排序还在,发件箱一旦分库分表就叠加跨库跨表查询。课程版本只做推拉结合,不做活跃用户判定,优先保障活跃用户体验,非活跃用户读得慢一点是接受的代价。〔W19-1 p20〕
  • 同步接口的形态值得抄:业务方到 Feed 的数据同步走 Kafka 异步,FeedEvent 只有 Type 加 Metadata map 两个字段,宽泛到毫无限制,消费者自己调 Service 方法。约束越少,新业务接入越不用改 Feed 平台,代价是 key 约定只能靠线下流程管。〔W19-1 p27〕

W21-1 Builder 与 Option 的分工原话 ​

  • Builder 用于构造复杂或预期多变的对象,配合链式调用,每个方法返回 Builder 本身直到构建完成;课程在 middleware 和插件构建里大量用它,MiddlewareBuilder(AllowReqBody、AllowRespBody 默认关闭)是范例。与 Option 的分工口诀:有点复杂用 Option,特别复杂用 Builder。minifeed 阶段不需要,等到配置项超过四五个再想起这条。〔W21-1 p15〕〔W21-1 p16-17〕

省略未吸收:W21-1 的 K40 到 K44(推行规范、要不要重构、面向简历编程等职场方法论条目,非技术知识);K29/K30 的注册步骤与静态算法细节、K31 到 K33 的算法清单展开在 16、20 节正文,这里只收结论。WX-sms 的 K3(面试话术的组织策略)与 K8(先列技术点等追问的表达技巧)属于面试表达而非设计知识。W10-2 p27 的职场视角(稳定系统也值得重构)与 W19-1 的 Handler 缺省兜底、点赞四 key 约定等条目属 19、21 节正文范围。W10-2 p2、W11-1 p2 均为标题页,正文引注改用实际承载内容的页面。

想深入去哪查 ​

  • 原材料:按第一节周次表,每一周对应 Wxx-x 单元;总览看 W21-1;短信服务面试话术见 WX-sms。
  • minifeed 相关的课文注解:minifeed/server.go(装配与路由)、minifeed/rank/rank.go(单飞与降级)、minifeed/counter/counter.go(内存计数加批量落库)。
  • 旧长版:无整章对应;分层与 DI 的展开见 10 节的 notes/p5-deepsearch.md。