在整理硬盘时,翻出了半年前的一个项目源码。那是当时为了切入微信广告赛道,打算用两周时间快速上线的一个短视频内容管理平台。虽然最后因为业务调整,项目还没正式推向市场就停掉了,但在那个极致追求“短平快”的环境下,我关于架构设计的一些纠结和坚持,现在回头看依然挺有意思。

一、 业务背景:高频重复与广告变现

这个项目的逻辑非常纯粹:通过爬虫抓取海量视频内容,分发给中老年用户,在刷视频的过程中插入激励视频广告。

针对这批用户,我们在调研中发现了一个特点:他们对“精准推荐”的要求不高,但对内容的“杀时间”爽感要求很高。只要视频刷得顺畅、不重复,他们就能停留很久。这个业务属性给了我们在技术实现上很大的“取舍”空间。

二、 解决分页漂移:Session List 的随机策略

在短视频流中,最头疼的是“分页漂移”。

如果用传统的 LIMIT offset, limit 分页,后台爬虫在实时塞入新内容时,用户滑到第二页可能就会看到第一页出现过的视频。要在短时间内搞一套推荐算法显然不现实,所以我实现了一套基于 Redis 的 Session List 缓存机制

  1. 随机采样:用户进入时,系统从近期的视频池中随机抽取 500 条视频 ID。
  2. 内存 Shuffle:这 500 条 ID 在后端通过 rand.Shuffle 再次打乱。
  3. Session 绑定:将这组有序的 ID 存入 Redis 并设置过期时间。后续的分页请求(Page 2, 3…)本质上只是在消费这个固定的 List。

这种做法很“原始”,但在那个赶进度的阶段,它用极低的成本解决了用户体感最核心的问题。

三、 架构上的“执念”:那个让我心累的依赖容器

在当时的开发环境下,领导的反馈很直接:“不要搞什么三层架构,太复杂了,简单直接写就行。”

这里提到的“简单方案”,本质上就是利用 Go 的全局变量和 init() 函数。必须承认,全局变量在 MVP 阶段有它极其诱人的一面:开发极快,不需要层层传递参数,任何地方 import 就能用;心智负担低,不用考虑依赖拓扑。

但我心里有一个底线:即使项目生命周期可能很短,也不能让它烂在手里。全局变量带来的隐式状态是系统腐烂的开始。因为状态是全局共享的,并行测试变得极难;由于 init() 执行顺序依赖于 import 顺序,一旦项目变大,你根本无法准确判断数据库、Redis 和配置中心到底谁先初始化完。

我希望通过某种机制,让系统在未来增加功能时依然能保持基本的模块解耦。于是,我选择在项目中引入了一套自研的依赖注入(DI)容器

现在回想起来,那段日子确实挺“拧巴”的。

1. 手动装配的苦恼
因为是自研容器,没用 wire 这种静态编译工具,我必须手动管理模块的注册。为了追求所谓的“模块自治”,我采用了按模块注册的方式:

1
2
3
4
5
6
7
8
9
// 模块 A 注册
container.Provide(func() *ModuleA {
return NewModuleA()
})

// 模块 B 注册,依赖模块 A
container.Provide(func() *ModuleB {
return NewModuleB(container.Get[*ModuleA]())
})

2. 隐藏的注册顺序地雷
由于我没有在容器里实现复杂的递归解析或拓扑排序,这种“模块即插件”的模式带来了一个隐性隐患:必须严格遵守注册顺序。如果模块 B 在模块 A 之前被注册,container.Get[*ModuleA]() 就会因为找不到实例而直接报错或返回 nil。

在那段时间里,我常常在深夜调试那些因为初始化顺序不对导致的 panic。那种“明明知道方向是对的,却因为环境和工具不成熟而不断修修补补”的疲惫感,我记忆犹新。虽然这种由于“方便”而产生的技术债在短期内看起来收益很高,但我更担心它会变成永久的泥潭。

最终我还是留下了容器。这种“手动有序注册”虽然笨,但它强迫我在代码层面去思考:谁才是真正的基础设施?谁依赖谁? 这种对依赖关系的显式声明,是防止代码演变成“大泥球”的最后一道防线。

四、 数据建模:共享 Content 层与 Aggregate Root 的尝试

在内容模型设计上,我面临一个选择:是把所有属性塞进一张表,还是做领域拆分?最终我采用了一种“核心主表 + 子模块扩展表”的结构。

  • Content(聚合根):存放 ID、标题、封面、分类等共有属性。
  • 子模块扩展表:分为 video(视频)、gallery(图集)、article(图文)。

在这种结构下,content 模块成为了一个共享层,它只提供 Repository(数据库操作),不提供 Service 和 Handler。而具体的 video 模块则拥有自己的独立业务逻辑,它会通过容器获取 content.Repo 来操作共享数据。

这种设计的优势在于,当你想要增加一个“短剧”业务时,它只需要在 video 模块下扩展一个子领域(Subdomain),而不需要改动核心的内容分发链路。这种对“领域边界”的划分,让我在后期处理多租户隔离和内容分发时,省去了大量的重复工作。

五、 反思:在丑陋中维持平衡

当时我也在想:既然公司追求“短平快”,这种坚持有意义吗?反正这些代码可能活不过几个月。

回头看,那次经历让我真正理解了架构师的修行:在系统里设计完美闭环并不难,难的是在混乱、不完美,甚至有些“恶心”的现实环境中,仍然守住系统里那一点点秩序。

我学会了接受“不完美的平衡”:

  • 我可以接受用随机抽样这种“投机”的方式解决分页,因为那是业务所需的。
  • 但我不能接受完全没有依赖管理的全局变量,因为那是工程师的底线。

这让我想到一句话:“知世俗,但不世俗。”

理解世俗的逻辑,比如业务要快、成本要低、生命周期短,是为了让项目先活下来;守住必要的技术底线,比如模块化、依赖清晰、数据模型合理,是为了让系统不在短期妥协里失控,也让自己作为工程师的专业性不被一点点磨掉。

六、 总结

回头再看这个项目,我其实并不满意。很多设计现在看来都显得粗糙,代码边界也不够清晰,有些地方还带着明显的试错痕迹。但它仍然有价值。那套 DI 容器思路、素材管理中的回滚逻辑,以及“核心能力 + 扩展能力”的模型意识,今天看仍有些粗糙,但它再次强化了我对工程边界的理解:代码要能跑,也要能承受后续变化。

如果现在再回到那个时刻,我大概会删掉不少东西,把设计做得更轻,也更贴近当时真正的问题。但我应该还是会保留那一点对秩序的敏感。毕竟很多项目走到最后,留下来的不一定是代码本身,反倒是你在混乱里判断边界、处理取舍、承认不完美的方式。