【知识获取与分享社区项目 | 项目日记第 23 天】项目梳理下篇:高并发与最终一致性复盘:Redis、Kafka、Outbox、ES 与 RAG 如何协同

一、为什么要做工程化设计

前面几篇分别讲了认证、发布、计数、关系、点赞、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 判断后重建

这类设计很适合在项目总结或面试中讲,因为它体现的不是"功能跑通",而是"系统出问题后能不能恢复"。


十一、如果继续优化,可以从哪里入手

这个项目已经有比较完整的高并发和最终一致性设计,但仍然有继续优化空间。

比如:

  1. Kafka 计数事件可以增加全局 eventId,进一步增强消费端幂等;
  2. Outbox 消费失败可以接入重试队列、死信队列和告警;
  3. Redis 扫描类逻辑可以从 KEYS 演进为活跃 Key 索引或分片 SCAN;
  4. Feed 可以从 offset 分页升级为游标分页;
  5. 搜索索引可以增加定期全量校验任务;
  6. RAG 索引可以加入后台批处理队列,避免请求线程承担重建压力;
  7. 对核心链路增加压测指标,比如 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 负责理解

这也是一个内容社区后端从"能用"走向"能扛、能恢复、能扩展"的关键。

相关推荐
折哥的程序人生 · 物流技术专研18 小时前
Java面试85题图解版 · 特别篇:2026后端高频面试题复盘(算法底层逻辑+高并发架构设计全解析,附Java实战代码)
java·网络·数据库·算法·面试
AOwhisky18 小时前
Redis 学习笔记(第三期):持久化与主从复制
运维·数据库·redis·笔记·学习·云计算
一条泥憨鱼18 小时前
【Redis】数据类型和常用命令
java·数据库·redis·后端·缓存
云烟成雨TD18 小时前
Spring AI Alibaba 1.x 系列【78】沙箱(Sandbox)
java·人工智能·spring
程序员二叉18 小时前
【Java】 异常高频面试题精讲 | 易错点+对比总结
java·开发语言·面试
周航宇JoeZhou19 小时前
JB3-9-SpringAI(二)
java·ai·agent·多智能体·调度·智能体·观察
好家伙VCC19 小时前
Web Components主题热切换方案揭秘
java·前端
慕木沐19 小时前
Google ADK Java 1.0版本 核心机制与实战 Demo
java·开发语言·python
Tbisnic19 小时前
AI大模型学习第十一天:技术选型、安全防护与金融实战
python·学习·ai·大模型·提示词工程
大白要努力!19 小时前
MySQL 8.0 + Navicat 完整操作指南
数据库·mysql