【知识获取与分享社区项目 | 项目日记第 24 天】终章总结:从认证、发布、计数、Feed、搜索到 RAG:完整复盘一个知识社区后端系统

一、项目整体介绍

知识获取与分享社区是一个知识内容社区后端项目,整体可以理解为一个面向知识创作者的内容社区。

用户可以在平台中完成:

text 复制代码
注册登录
  ↓
发布 Markdown / 图文知文
  ↓
点赞、收藏、关注作者
  ↓
通过 Feed、搜索、详情页消费内容
  ↓
使用 AI 摘要和单篇知文 RAG 问答提升阅读效率

所以这个项目不是简单的增删改查,而是围绕内容社区中的几个核心问题展开:

  1. 用户身份如何安全管理;
  2. 大文件和 Markdown 正文如何发布;
  3. 点赞、收藏、关注这些高频行为如何抗并发;
  4. Feed 流如何保证读取性能;
  5. 搜索如何做到相关性、排序和深分页稳定;
  6. AI 问答如何围绕单篇知文准确回答;
  7. 派生数据如何保证最终一致和可恢复。

整体技术栈如下:

text 复制代码
Java 21
Spring Boot 3.2.4
Spring Security
Spring AI
MyBatis
MySQL
Redis
Kafka
Canal
Caffeine
Elasticsearch
阿里云 OSS
DeepSeek

从存储分工上看,可以这样理解:

技术 在项目中的作用
MySQL 保存用户、知文、关注关系、Outbox 等权威数据
Redis 验证码、刷新令牌白名单、位图、SDS 计数、缓存
Kafka 计数聚合、关系投影、搜索索引异步更新
Canal 订阅 MySQL Outbox binlog 并转发事件
OSS 保存 Markdown 正文、图片、视频等大对象
Elasticsearch 内容搜索、前缀联想、RAG 向量库后端
Spring AI + DeepSeek AI 摘要生成与单篇知文 RAG 问答

这个项目最核心的一句话是:

text 复制代码
MySQL 管业务事实,Redis 管高频状态,Kafka 管异步事件,OSS 管大对象,ES 管搜索检索,AI 管内容理解。

二、项目模块总览

整个后端可以拆成几个核心模块:

模块 核心职责
认证系统 注册、登录、验证码、JWT 双令牌、刷新令牌白名单
发布系统 草稿、OSS 预签名直传、内容确认、元数据更新、正式发布
计数系统 点赞数、收藏数、关注数、粉丝数、发文数等紧凑计数
点赞系统 分片位图判重、Kafka 异步写、计数聚合与重建
用户关系系统 关注取关、following 主表、follower 投影、Outbox 事件
Feed 流 Caffeine + Redis 页面缓存 + Redis 片段缓存
搜索系统 ES 全文检索、标签过滤、function_score、search_after、联想建议
AI 问答系统 DeepSeek 摘要、RAG 索引、向量召回、Prompt 构造、SSE 流式输出

这些模块之间不是孤立的。

比如发布一篇知文后,会影响:

text 复制代码
用户发文数
Feed 缓存
搜索索引
RAG 预索引
详情页缓存

点赞一篇知文后,会影响:

text 复制代码
点赞位图
点赞计数
Feed 中的计数展示
用户 liked 状态

关注一个用户后,会影响:

text 复制代码
following 主表
follower 投影表
关注数 / 粉丝数
关系列表缓存

所以项目的重点不只是"实现接口",而是要处理好数据在不同系统之间的流转关系。


三、认证系统:JWT 双令牌兼顾性能和安全

认证系统采用的是 JWT 双令牌模式:

text 复制代码
access token:15 分钟
refresh token:7 天

access token 用于普通业务接口访问,走 JWT 无状态校验。

refresh token 用于续期,但它不是完全无状态的,而是会写入 Redis 白名单。

整体流程如下:

text 复制代码
用户登录
  ↓
签发 access token + refresh token
  ↓
refresh token 的 jti 写入 Redis 白名单
  ↓
access token 过期后使用 refresh token 换新令牌
  ↓
刷新成功后撤销旧 refresh token
  ↓
登出或重置密码时删除 Redis 白名单

这样设计的好处是:

text 复制代码
access token 保持高性能
refresh token 保留可撤销能力

如果只使用一个长期 JWT,一旦泄露,在过期之前很难主动让它失效。

而双令牌方案可以做到:

  1. 普通接口访问时不查库;
  2. 登录态续期可控;
  3. 登出可以立即撤销 refresh token;
  4. 重置密码后可以让旧 refresh token 失效;
  5. access token 时间短,即使泄露风险窗口也较小。

所以认证系统的核心取舍是:

text 复制代码
业务访问尽量无状态,续期令牌保留轻状态。

四、发布系统:渐进式发布与 OSS 预签名直传

发布系统没有把所有内容一次性提交给后端,而是拆成了多个阶段:

text 复制代码
创建草稿
  ↓
申请 OSS 预签名 URL
  ↓
前端直传 Markdown / 图片 / 视频
  ↓
确认内容上传结果
  ↓
更新标题、标签、摘要、可见性等元数据
  ↓
正式发布

这就是项目中的渐进式发布流程。

它解决了几个问题。

第一,大文件不经过后端。

图片、视频、Markdown 正文都直接上传到 OSS,后端只负责生成预签名 URL 和保存对象元数据,避免后端承担大文件传输压力。

第二,发布过程可恢复。

如果用户上传图片后中断,草稿仍然存在;如果元数据没填完,也不会影响已上传内容。

第三,数据库不存大正文。

MySQL 中只保存:

text 复制代码
content_url
content_object_key
content_etag
content_size
content_sha256

正文内容本身存储在 OSS。

这样可以降低 MySQL 行大小,避免内容表变得过重。

第四,发布动作最后执行。

只有调用正式发布接口后,知文状态才会从 draft 变成 published,然后进入 Feed、搜索、RAG 等消费链路。

发布系统的整体价值是:

text 复制代码
把大文件上传、内容编辑、元数据更新、正式发布拆成多个可重试步骤。

五、计数系统:位图做事实,SDS 做快照

点赞、收藏、关注、粉丝、发文数等都属于计数场景。

如果每一次点赞都同步更新 MySQL:

sql 复制代码
UPDATE know_posts SET like_count = like_count + 1 WHERE id = ?

热门内容很容易形成热点行。

所以项目中采用了 Redis 位图 + Kafka 聚合 + Redis SDS 快照的设计。

整体可以概括为:

text 复制代码
位图保存用户行为事实
Kafka 传递增量事件
聚合桶合并短时间 delta
SDS 保存低成本计数快照

以点赞为例,流程是:

text 复制代码
用户点赞
  ↓
定位位图分片
  ↓
Lua 执行 GETBIT + SETBIT
  ↓
只有状态真的变化才发送 Kafka 事件
  ↓
消费者写入 Redis 聚合桶
  ↓
后台定时折叠到 SDS 计数

这里最关键的是位图。

它天然适合保存:

text 复制代码
某个用户是否点赞了某篇内容
某个用户是否收藏了某篇内容

重复点赞时,位图状态不会变化,因此不会重复产生计数事件。

读取时,项目把公共计数和用户态状态拆开:

text 复制代码
点赞数 / 收藏数:读取 SDS
我是否点赞 / 收藏:读取位图

这样做还有一个好处:Feed 缓存可以只缓存公共内容,不缓存当前用户的 liked / faved 状态,避免用户态污染。


六、点赞系统:异步写、写聚合与灾难回放

点赞系统可以看作计数系统中最高频的一条链路。

它的主要设计是:

text 复制代码
分片位图做幂等
Kafka 做异步削峰
Redis 聚合桶做写合并
SDS 做计数快照
位图和 Kafka 做重建兜底

用户点赞时,真正的强事实不是计数值,而是:

text 复制代码
这个用户是否点过赞

这个事实存在位图里。

计数只是由这些事实推导出来的快照。

所以当 SDS 缺失或损坏时,可以基于位图重建:

text 复制代码
扫描该内容的位图分片
  ↓
BITCOUNT 统计所有置位数量
  ↓
重新写入 SDS

如果发生更大的异常,也可以通过 Kafka 灾难回放来恢复计数。

这体现了一个很重要的工程思想:

text 复制代码
快照可以丢,事实必须可重建。

点赞系统不是追求每一次点击都同步强一致,而是牺牲一点点实时性,换来高并发写入能力和可恢复能力。


七、用户关系系统:following 主表 + follower 投影表

关注系统采用的是一主多从模型。

其中:

text 复制代码
following 表:主事实表,表示我关注了谁
follower 表:投影表,表示谁关注了我
用户计数 SDS:关注数、粉丝数
Redis ZSet:关注列表、粉丝列表缓存

关注发生时,不会同步更新所有数据源,而是在同一个事务中写入:

text 复制代码
following 主表
outbox 事件表

之后通过:

text 复制代码
Canal 订阅 Outbox binlog
  ↓
Kafka 分发事件
  ↓
消费者异步更新 follower 表、Redis 列表、用户计数

为什么需要 Outbox?

因为如果业务代码先写 MySQL,再发 Kafka,可能出现:

text 复制代码
MySQL 写成功
Kafka 发送失败

这样下游永远不知道这次关注事件。

Outbox 模式把业务事实和待发送事件放到同一个数据库事务里:

text 复制代码
事务成功,业务事实存在,事件也存在
事务失败,业务事实和事件都不存在

后续即使 Kafka 或消费者短暂失败,也可以通过 Canal / Kafka 重试追平。

所以关系系统的核心思想是:

text 复制代码
主事实强一致,投影数据最终一致。

八、Feed 流:三级缓存与用户态隔离

Feed 是内容社区最核心的读入口。

项目中 Feed 采用三级缓存:

text 复制代码
L1:Caffeine 本地缓存
L2:Redis 页面缓存 / ID 列表缓存
L3:Redis item 片段缓存

缓存设计中最重要的一条原则是:

text 复制代码
公共缓存不能保存用户态状态。

比如下面这些字段适合缓存:

text 复制代码
标题
摘要
封面
标签
作者信息
点赞数
收藏数

但下面这些字段不能放进公共缓存:

text 复制代码
当前用户是否点赞
当前用户是否收藏

因为这是和用户绑定的状态。

如果把 A 用户的 liked 状态写进公共缓存,B 用户读取时就会看到错误结果。

所以 Feed 返回时会做一层覆盖:

text 复制代码
读取公共 Feed 缓存
  ↓
批量读取计数
  ↓
根据当前用户读取位图
  ↓
覆盖 liked / faved

另外,Feed 系统还做了几类缓存保护。

问题 方案
缓存穿透 空值缓存
缓存击穿 single-flight
缓存雪崩 TTL 随机抖动
热点 Key hotkey 探测 + 动态延长 TTL
数据变更 主动删除 + 短 TTL 兜底

single-flight 的作用是:

text 复制代码
同一页缓存失效时,只允许一个线程回源 DB,其他线程等待结果。

这样可以避免同一个热点页面在缓存过期时被大量请求同时打到数据库。


九、搜索系统:ES 相关性排序与联想建议

搜索系统基于 Elasticsearch 实现,核心能力包括:

text 复制代码
关键词检索
标签过滤
BM25 相关性
function_score 业务加权
highlight 高亮摘要
search_after 游标分页
completion suggester 前缀联想

搜索不是简单的 LIKE

项目中使用 multi_match 搜索标题和正文,并且给标题更高权重:

text 复制代码
title^3
body

因为标题命中通常比正文偶然命中更能说明内容相关。

排序方面,通过 function_score 融合业务权重:

text 复制代码
最终排序 = 文本相关性 + 点赞数权重 + 浏览数权重

为了避免深分页问题,项目没有使用 from + size,而是使用 search_after

排序字段大致是:

text 复制代码
_score
publish_time
like_count
view_count
content_id

最后的 content_id 用于兜底,保证排序稳定。

索引更新也没有阻塞发布主链路,而是通过 Outbox 异步完成:

text 复制代码
知文发布 / 更新 / 删除
  ↓
写 Outbox
  ↓
Canal 订阅 binlog
  ↓
Kafka 分发
  ↓
搜索消费者 upsert / soft delete ES 文档

搜索系统体现的是:

text 复制代码
MySQL 是权威数据源,ES 是面向搜索体验的派生视图。

十、AI 系统:摘要生成与单篇知文 RAG 问答

AI 部分分成两个能力。

第一个是文章摘要生成。

用户输入正文后,后端调用 DeepSeek 生成不超过 50 字的中文摘要,再做格式清理、截断和规范化,避免模型输出失控。

第二个是 RAG 知识问答。

RAG 的完整流程是:

text 复制代码
用户围绕单篇知文提问
  ↓
检查该知文是否已索引
  ↓
从 OSS 拉取 Markdown 正文
  ↓
按 Markdown 标题和长度切片
  ↓
写入 Elasticsearch 向量库
  ↓
向量召回相关片段
  ↓
按 postId 过滤当前知文上下文
  ↓
构造 Prompt
  ↓
调用 DeepSeek 流式生成
  ↓
SSE 返回前端逐步渲染

RAG 系统中有几个关键设计。

第一,只索引公开已发布内容:

text 复制代码
published + public

第二,每个向量切片 metadata 中写入 postId,查询时按 postId 过滤,避免其他文章内容污染上下文。

第三,使用 SHA256 / ETag 判断正文是否变化。

如果没有变化,就跳过重建;如果变化,就先删除旧切片,再写入新切片。

第四,发布成功后触发预索引,减少用户第一次提问时的等待时间。

第五,接口使用 SSE 流式返回,让用户可以像聊天一样逐步看到答案。

所以 RAG 不是简单调用大模型,而是:

text 复制代码
检索提供依据
metadata 控制范围
Prompt 限制边界
流式输出提升体验

十一、强一致与最终一致的边界

这个项目中最值得总结的一点,是对一致性边界的划分。

强一致数据包括:

text 复制代码
用户账号
登录认证
知文主表
关注主事实
发布权限
删除状态

这些数据直接影响安全和业务事实,必须以 MySQL 事务为准。

最终一致数据包括:

text 复制代码
点赞计数
收藏计数
粉丝投影
关系列表缓存
Feed 缓存
搜索索引
RAG 向量索引

这些数据允许短时间延迟,但必须做到:

text 复制代码
可重试
可重建
可回放
可兜底

项目整体取舍就是:

text 复制代码
主事实强一致,派生视图最终一致。

这也是整个项目最核心的工程设计思想。


十二、Redis、Kafka、Outbox、ES 在项目中的协同

如果把各组件协同关系画出来,大致是:

text 复制代码
MySQL
  ├── 保存用户、知文、关注、Outbox 等权威事实
  ↓
Canal
  ├── 订阅 Outbox binlog
  ↓
Kafka
  ├── 分发关系事件、搜索索引事件、计数事件
  ↓
Redis
  ├── 保存位图、SDS 计数、缓存、令牌白名单
  ↓
Elasticsearch
  ├── 构建搜索索引和 RAG 向量索引
  ↓
DeepSeek
  └── 基于上下文生成摘要和问答结果

每个组件都有明确职责:

组件 主要职责
MySQL 保证业务事实正确
Redis 承担高频读写和低延迟缓存
Kafka 削峰、异步、事件传播
Canal 把数据库事件桥接到消息系统
Elasticsearch 提供搜索和向量检索能力
OSS 存储正文、图片、视频等大对象
DeepSeek 提供摘要和问答生成能力

这个项目不是为了用技术而用技术,而是不同组件服务于不同类型的数据。


十三、项目中的可恢复设计

一个系统能不能上线,不只看正常流程能不能跑,还要看异常时能不能恢复。

项目中很多设计都保留了恢复路径。

异常场景 恢复方式
refresh token 泄露 删除 Redis 白名单,使其立即失效
SDS 计数缺失 基于位图 BITCOUNT 重建
Kafka 计数异常 使用独立消费者组灾难回放
follower 投影延迟 Outbox 事件重试后追平
关系缓存不一致 删除缓存或等待 TTL 过期重建
Feed 缓存击穿 single-flight 控制回源
搜索索引为空 启动时从 MySQL 分页回灌
RAG 索引过期 根据 SHA256 / ETag 判断后重建

这里有一个非常重要的思想:

text 复制代码
派生数据可以坏,但必须能从主事实中恢复。

比如:

  • 搜索索引坏了,可以从 MySQL 和 OSS 重建;
  • RAG 索引坏了,可以从 OSS Markdown 重建;
  • SDS 计数坏了,可以从位图重建;
  • 粉丝投影慢了,可以通过 Outbox 事件追平。

这比单纯追求"永远不出错"更现实。


十四、项目中的几个核心亮点

1. JWT 双令牌认证

text 复制代码
短 access token 提供高性能访问
长 refresh token 通过 Redis 白名单实现可撤销续期

这比单一长 JWT 更安全,也比分布式 Session 更轻。

2. OSS 渐进式发布

text 复制代码
草稿状态机 + 预签名直传 + 内容确认 + 元数据更新 + 正式发布

大文件不压后端,发布流程可恢复。

3. 分片位图点赞判重

text 复制代码
GETBIT / SETBIT 天然支持幂等和状态判断

只在状态变化时产生 Kafka 事件,避免重复计数。

4. Redis SDS 紧凑计数

text 复制代码
定长字节布局保存多个计数字段

相比 Hash 更节省内存,也适合高频读取。

5. Kafka 写聚合

text 复制代码
高频 delta 先进入聚合桶,再批量折叠到 SDS

削弱热点写压力。

6. Outbox 事件驱动

text 复制代码
业务事实和待发送事件写入同一个 MySQL 事务

解决业务表和消息发送的双写不一致问题。

7. Feed 三级缓存

text 复制代码
Caffeine + Redis 页面缓存 + Redis item 片段缓存

配合 single-flight、TTL 抖动、hotkey 探测,提升高频读稳定性。

8. ES 搜索系统

text 复制代码
multi_match + function_score + search_after + completion suggester

同时兼顾相关性、业务权重、深分页稳定和联想体验。

9. RAG 知识问答

text 复制代码
索引检查 → 向量检索 → Prompt 构造 → DeepSeek 流式生成

围绕单篇知文做智能问答,避免大模型脱离上下文发挥。


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

这个项目已经有比较完整的设计,但仍然有继续优化空间。

  1. Kafka 计数事件增加全局 eventId,进一步增强消费端幂等;
  2. Outbox 消费失败接入重试队列、死信队列和告警;
  3. Redis 扫描类逻辑从 KEYS 演进为活跃 Key 索引或分片 SCAN;
  4. Feed 从 offset 分页升级为游标分页;
  5. 搜索索引增加定期全量校验任务;
  6. RAG 索引重建加入后台任务队列,减少请求线程压力;
  7. 增加压测指标,如 P99 延迟、缓存命中率、Kafka 堆积恢复时间;
  8. 对核心链路增加集成测试,覆盖发布、搜索、Feed、计数和 RAG 闭环。

这些优化方向说明,工程项目没有绝对完成,只有在当前阶段做出合理取舍。


十六、最终总结

项目从业务上看,是一个知识内容社区。

从技术上看,它是一套围绕高并发读写、缓存一致性、消息可靠性、搜索体验和 AI 问答能力构建的后端系统。

如果用一条主线串起来,就是:

text 复制代码
认证系统保证用户身份
发布系统产生内容主事实
OSS 保存正文和图片等大对象
Redis 承担令牌、位图、计数和缓存
Kafka + Canal 传播异步事件
Feed 和搜索负责内容分发
RAG 问答负责内容理解

整个项目最核心的设计思想是:

text 复制代码
强一致数据走 MySQL 事务,派生数据走 Redis / Kafka / ES 最终一致,并通过重建、回放、短 TTL 和幂等策略保证可恢复。

相比普通 CRUD 项目,它的提升在于:

  1. 认证不是简单登录,而是 JWT 双令牌会话管理;
  2. 发布不是普通表单提交,而是 OSS 渐进式发布;
  3. 点赞不是直接改库,而是位图 + Kafka + SDS;
  4. 关系不是同步多写,而是 Outbox 事件驱动;
  5. Feed 不是直接查库,而是三级缓存和用户态隔离;
  6. 搜索不是 LIKE,而是 ES 相关性排序和游标分页;
  7. AI 不是简单接口调用,而是围绕单篇知文的 RAG 问答。

最后用一句话收尾:

text 复制代码
这个项目真正训练的不是某一个框架 API,而是如何在真实业务中划分主事实、派生视图、高频状态和可恢复链路。

这也是知识获取与分享社区从"功能能跑"走向"系统能扛、数据能恢复、体验能持续优化"的关键。

相关推荐
Jabes.yang1 小时前
Java面试实录:AIGC场景下的Stream、微服务、Redis、Kafka与安全实战
java·spring boot·redis·微服务·面试·kafka·aigc
lwf0061641 小时前
实战:用 Java 模拟登录阿里云控制台,爬取没有 OpenAPI 的数据
java·阿里云
骄马之死1 小时前
Redis 核心知识点总结
数据库·redis·缓存
张哈大1 小时前
MCP:重塑AI工具调用的统一标准,告别重复造轮子的时代
人工智能·python·ai·prompt
极光代码工作室1 小时前
基于深度学习的智能图像识别平台
python·深度学习·机器学习·ai·系统设计
basketball6161 小时前
Redis基础:6. 哨兵模式
数据库·redis·bootstrap
程序员二叉1 小时前
【Java】 面试核心合集:BigDecimal、缓存池、多态、反射全解析
java·缓存·面试
一条咸鱼_SaltyFish1 小时前
Agent 工程化避坑指南——从实践看常见反模式
ai·agent·ai编程·memory·obsidian·harness·llm-wiki
小小编程路1 小时前
MySQL9.0|融合向量的新一代关系数据库安装配置教程
mysql