
一、为什么要做工程化设计
前面几篇分别讲了认证、发布、计数、关系、点赞、Feed、搜索和 RAG。
这些模块看起来很多,但背后其实有一条统一主线:
text
哪些数据必须强一致?
哪些数据可以最终一致?
哪些读写需要高性能?
哪些异常必须可恢复?
如果所有东西都同步写 MySQL,系统实现最简单,但高并发下会出现:
text
热点行更新
接口延迟升高
数据库压力集中
缓存难以维护
搜索和 AI 索引阻塞主链路
所以项目中大量使用了 Redis、Kafka、Canal、Outbox、Elasticsearch 这些组件,把不同类型的数据放到合适的位置。
二、强一致与最终一致的边界
这个项目里,我认为最重要的不是"用了多少技术",而是划清一致性边界。
强一致数据包括:
text
用户账号
登录认证
知文主表
关注主事实
发布权限
删除状态
这些数据一旦错了,会直接影响安全和业务事实,所以必须以 MySQL 事务为准。
最终一致数据包括:
text
点赞计数
收藏计数
粉丝投影
关系列表缓存
Feed 缓存
搜索索引
RAG 向量索引
这些数据允许短时间延迟,但必须满足:
text
可重试
可重建
可回放
可兜底
这就是项目的整体取舍:
text
主事实强一致,派生视图最终一致。
三、Redis 在项目中的四种角色
Redis 在项目中不是只做缓存,而是承担了四类职责。
1. 会话安全
refresh token 白名单存 Redis:
text
auth:rt:{userId}:{tokenId}
access token 走 JWT 无状态校验,refresh token 通过 Redis 判断是否仍然有效。
这样既保证访问接口高性能,也保留了登出、改密、强制下线能力。
2. 高频状态
点赞、收藏使用分片位图:
text
bm:{metric}:{etype}:{eid}:{chunk}
通过 GETBIT / SETBIT 判断用户是否已经操作过。
这类状态很适合 Redis,因为它读写频率高,而且天然适合位图压缩。
3. 紧凑计数
计数快照使用定制化 SDS:
text
cnt:v1:{etype}:{eid}
ucnt:{userId}
SDS 本质上是 Redis 字符串,但内部按固定偏移保存多个 4 字节计数。
相比 Redis Hash,它更紧凑,适合大量内容计数。
4. 多级缓存
Feed、详情页、关系列表都会用 Redis 缓存。
比如:
text
feed:item:{id}
knowpost:detail:{id}:v{version}
uf:flws:{userId}
uf:fans:{userId}
这类缓存不是权威数据,只是加速读路径。
一旦异常,可以删掉重建。
四、Kafka 的作用:削峰、聚合和异步派生
Kafka 在项目中主要有三类使用场景。
第一类是点赞收藏计数聚合。
点赞事件不是每次都立刻写最终计数,而是先发送增量事件:
text
like +1
fav -1
消费者聚合后再批量折叠到 SDS。
这样可以把高频写变成批量写,降低 Redis 和后续存储压力。
第二类是关系系统的异步投影。
关注主事实写入 MySQL 后,下游通过 Kafka 更新:
text
follower 表
关系 ZSet 缓存
用户关注数 / 粉丝数
第三类是搜索索引更新。
知文发布、更新、删除后,通过 Outbox + Kafka 通知搜索消费者更新 ES。
所以 Kafka 在项目中的定位不是简单消息队列,而是:
text
把同步主链路上的派生更新拆出去。
五、Outbox:解决双写不一致
关系系统和搜索系统都使用了 Outbox 模式。
典型问题是:
text
业务表写成功
Kafka 消息发送失败
如果没有 Outbox,下游就永远不知道这次变化。
项目中的做法是,在业务事务中同时写:
text
业务主表
outbox 事件表
例如知文发布后写入事件:
java
String payload = objectMapper.writeValueAsString(
Map.of("entity", "knowpost", "op", "upsert", "id", id)
);
outboxMapper.insert(outId, "knowpost", id, "KnowPostPublished", payload);
然后由 Canal 订阅 MySQL binlog,把 Outbox 事件投递到 Kafka。
这样只要 MySQL 事务成功,事件就一定存在。
后续即使 Kafka、消费者、ES 短暂失败,也可以通过重试或回放追平。
六、计数系统的自愈能力
点赞和收藏系统没有把 SDS 当作唯一事实。
真正的事实是:
text
用户是否点赞 / 收藏的分片位图
SDS 只是计数快照。
所以当 SDS 缺失或长度异常时,可以基于位图重建:
text
扫描相关位图分片
↓
BITCOUNT 统计置位数量
↓
重新写入 SDS 计数快照
Kafka 也可以作为灾难回放兜底。
如果某段时间聚合结果异常,可以用独立消费者组从 earliest 位点重新消费事件,恢复计数状态。
这就是项目中的一个关键思想:
text
快照可以丢,事实要能重建。
七、缓存一致性:不追求绝对实时,但要避免污染
Feed 和详情页缓存设计中,有一个很重要的原则:
text
公共缓存不能保存用户态数据。
比如 Feed 中的内容标题、封面、作者、点赞数可以缓存。
但是:
text
liked
faved
不能写入公共缓存。
因为这是用户维度状态。
项目中的处理方式是:
text
先取公共 Feed 缓存
↓
再根据当前用户读取位图
↓
覆盖 liked / faved 字段
这样既能复用公共缓存,又不会出现用户状态污染。
缓存一致性上,项目采用的是组合策略:
| 问题 | 方案 |
|---|---|
| 缓存穿透 | 空值缓存 |
| 缓存击穿 | single-flight |
| 缓存雪崩 | TTL 随机抖动 |
| 热点 Key | hotkey 探测 + 动态延长 TTL |
| 数据变更 | 主动删除 + 短 TTL 兜底 |
这套方案不追求缓存和 DB 毫秒级完全一致,而是追求:
text
读性能高
错误窗口短
异常能恢复
用户态不污染
八、搜索索引与 RAG 索引都是派生数据
Elasticsearch 在项目中有两种用途:
text
普通搜索索引
RAG 向量索引
它们都不是权威数据源。
搜索索引来源于 MySQL + OSS + 计数系统。
RAG 向量索引来源于 OSS Markdown 正文。
所以它们都可以重建。
搜索索引的恢复方式是:
text
如果 ES 索引为空,启动时分页回灌 public + published 知文
RAG 索引的恢复方式是:
text
用户提问前 ensureIndexed
发布后预索引
正文变化时按 SHA256 / ETag 重建
这两个系统都体现了一个思想:
text
ES 是面向查询体验的派生视图,不是最终事实。
只要 MySQL 和 OSS 中的数据还在,ES 索引就能恢复。
九、RAG 的准确性控制
RAG 系统最怕的问题是模型胡编。
项目中从三层控制准确性。
第一层是索引范围控制:
text
只索引 published + public 的知文
草稿、私密内容不会进入向量库。
第二层是检索范围控制:
text
向量召回后按 metadata.postId 过滤
这样用户围绕某一篇知文提问时,只会使用这篇知文的切片作为上下文。
第三层是 Prompt 控制:
text
只能依据提供的知文上下文回答
无法确定的请说明不确定
所以 RAG 不是简单把问题丢给大模型,而是:
text
用检索给依据
用 metadata 控范围
用 Prompt 立边界
用 SSE 提升体验
十、项目中的可恢复设计
这个项目里很多设计都留了恢复路径。
| 场景 | 恢复方式 |
|---|---|
| refresh token 泄露 | 删除 Redis 白名单,令牌立即失效 |
| SDS 计数缺失 | 基于位图 BITCOUNT 重建 |
| Kafka 计数异常 | 使用独立消费者组灾难回放 |
| follower 投影延迟 | Outbox 事件重试后追平 |
| 关系缓存不一致 | 删除缓存或等待 TTL 过期重建 |
| Feed 缓存击穿 | single-flight 控制回源 |
| 搜索索引为空 | 启动时 MySQL 历史回灌 |
| RAG 索引过期 | SHA256 / ETag 判断后重建 |
这类设计很适合在项目总结或面试中讲,因为它体现的不是"功能跑通",而是"系统出问题后能不能恢复"。
十一、如果继续优化,可以从哪里入手
这个项目已经有比较完整的高并发和最终一致性设计,但仍然有继续优化空间。
比如:
- Kafka 计数事件可以增加全局 eventId,进一步增强消费端幂等;
- Outbox 消费失败可以接入重试队列、死信队列和告警;
- Redis 扫描类逻辑可以从
KEYS演进为活跃 Key 索引或分片 SCAN; - Feed 可以从 offset 分页升级为游标分页;
- 搜索索引可以增加定期全量校验任务;
- RAG 索引可以加入后台批处理队列,避免请求线程承担重建压力;
- 对核心链路增加压测指标,比如 P99 延迟、缓存命中率、Kafka 堆积恢复时间。
这些优化方向也说明,工程项目没有绝对完成,只有在当前阶段做出合理取舍。
十二、本篇总结
这一篇主要从工程设计角度总结项目。
如果只记住一句话,就是:
text
强一致数据走 MySQL 事务,派生数据走 Redis / Kafka / ES 最终一致,并通过重建、回放、短 TTL 和幂等策略保证可恢复。
项目里比较值得总结的几个设计点是:
text
JWT 双令牌:高性能访问 + 可撤销续期
OSS 渐进式发布:降低后端大文件传输压力
Redis 位图:高并发点赞判重
Redis SDS:紧凑计数快照
Kafka 聚合:削弱热点写压力
Outbox:解决业务表和消息发送的双写一致性
Feed 三级缓存:提升高频读性能
ES 搜索:相关性、业务权重和深分页
RAG 问答:围绕单篇知文做可控智能问答
这个项目最核心的价值,不是堆技术栈,而是把每个组件放在了适合的位置:
text
MySQL 负责正确
Redis 负责快
Kafka 负责缓冲和传播
OSS 负责大对象
ES 负责检索
AI 负责理解
这也是一个内容社区后端从"能用"走向"能扛、能恢复、能扩展"的关键。