概述
本文补齐小视频模块:记录「谁看过我」的访客数据,用 FastDFS 存储视频文件,实现发布与列表接口,最后用 Spring Cache 给列表加上缓存。
纲要
- 前置回顾:上一阶段「附近的人」的两个核心产出
- 主线内容:按讲义顺序拆解本文结构
- 我的访客:需求与表结构、记录访客数据、首页「谁看过我」
- 小视频功能说明
- FastDFS:是什么、工作原理(上传 / 下载)、Docker 搭建、入门案例
- 发布小视频:表结构、接口实现全链路
- 小视频列表:步骤分析与实现、测试问题解决
- Spring Cache:概念、@Cacheable / @CacheEvict 入门、视频列表缓存、发布视频清空缓存
- 依赖关系:FastDFS 是视频存储的前提,访客记录复用 MongoDB,Spring Cache 优化列表查询
- 产出预告:本文结束时工程的类与配置变化
前置回顾
上一阶段围绕「附近的人」展开:
| 产出 | 一句话回顾 |
|---|---|
| 地理位置索引 | MongoDB 2dsphere 索引 + geoNear 查询,按距离排序返回附近用户 |
| 项目滑动配对 | 右滑喜欢写入 UserLike 表,双向喜欢即成为好友,配合 Redis 记录今日喜欢次数 |
这两样产出都会被直接消费:访客记录继续用 MongoDB 存文档,配对产生的好友关系则是小视频列表「可见范围」的隐含前提。
任务清单
| 板块 | 做什么 | 产出物 | 验收标准 |
|---|---|---|---|
| 我的访客 | 设计 visitors 表;发布动态等动作时写入访客记录;首页展示「谁看过我」 | Visitors 文档实体 + VisitorsApi + VisitorsVo | 首页接口返回最近看过我的用户及缘分值 |
| 小视频功能说明 | 明确需求:发布 15 秒以内的小视频,列表滑动浏览 | 需求梳理 | 能画出发布与浏览的交互流程 |
| FastDFS | 理解 Tracker / Storage 架构与上传下载原理,Docker 搭建环境,写 Java 入门案例 | docker-compose 起的 FastDFS 环境 + FastDFSTest 测试类 | 测试类完成文件上传并返回 group/path |
| 发布小视频 | 设计 video 表;前端传视频文件与缩略图,上传 FastDFS 后落 MongoDB | SmallVideoController + SmallVideosService + VideoApi | 发布后 MongoDB 中出现一条 video 文档 |
| 小视频列表 | 分页查询推荐视频列表,拼装作者信息 | VideoVo 组装逻辑 | 列表返回视频封面、点赞字段与用户信息 |
| SpringCache | 用 @Cacheable 缓存视频列表,发布视频后 @CacheEvict 清缓存 | RedisCacheConfig(videos 缓存 30 秒) | 二次请求列表命中缓存,发布新视频后缓存失效 |
我的访客:又吃一次 MongoDB 的老本
访客数据的写法在上一章已经练过:文档实体打 @Document 注解,Dubbo 服务提供者放 mf-dubbo-mongo,用 MongoTemplate 查询。表结构只有一张 visitors 表:```mermaid
erDiagram
visitors {
ObjectId id PK
Long userId "被访问的用户"
Long visitorUserId "访客"
Double score "缘分值"
Long date "访问时间"
}
写入口在业务动作里(比如查看他人资料时触发),查询口是首页「谁看过我」,按 userId 查最近访客,按缘分值倒序,再调 UserInfoApi 补全头像昵称。整体套路与「今日佳人」功能几乎一致,属于趁热打铁的复习题。## FastDFS:视频为什么不能放 OSS 之外的传统目录
视频文件动辄几十兆,放数据库或本地磁盘都撑不住。本章引入 FastDFS 分布式文件系统,两个角色分工明确:
| 角色 | 职责 | 类比 |
|---|---|---|
| Tracker | 调度:接收请求,返回可用的 Storage | 负载均衡器 |
| Storage | 存储:按 group 组织,实际存放文件并同步 | 存储节点 |
上传时客户端先问 Tracker 要 Storage,再直传 Storage;下载时凭 group 名 + 文件路径定位。Docker 环境用 docker-compose 起,资源占用较多,如果 Tracker 或 Storage 没正常起来,`docker-compose restart` 重启一次即可。入门案例用 Java 客户端上传文件,拿到 `group1/M00/xx/xx.png` 这样的路径,这个路径就是后面 video 表里存的值。
## 发布与列表:一条链路两个接口
发布小视频的完整链路:
```mermaid
flowchart LR
A[前端上传<br/>视频文件 + 缩略图] --> B[SmallVideoController]
B --> C[SmallVideosService]
C --> D[上传 FastDFS<br/>得到 videoUrl / picUrl]
D --> E[VideoApi @DubboReference]
E --> F[VideoApiImpl<br/>MongoTemplate 保存 Video 文档]
列表接口则反过来:MongoDB 分页查 video 文档 → 调 UserInfoApi 补作者信息 → 组装 VideoVo 返回。实现中会遇到测试问题(典型的是分页与拼装次序问题),讲义里专门有一节「问题分析与解决」,动手时别跳过。
SpringCache:给列表查询提速
列表数据读多写少,适合缓存。Spring Cache 是注解式的声明缓存,底层实现换成 Redis 只改配置不改代码:
java
@Cacheable(value = "videos", key = "'page-' + page + '-' + pageSize")
public PageResult findVideoByPage(Integer page, Integer pageSize) { ... }
@CacheEvict(value = "videos", allEntries = true)
public void saveVideo(Long userId, String videoUrl, String picUrl, String text) { ... }
工程里的 RedisCacheConfig 用 RedisCacheManagerBuilderCustomizer 按缓存名配置 TTL,videos 缓存 30 秒,列表数据允许短暂陈旧,换来的是缓存击穿风险大幅降低。发布视频走 @CacheEvict 清空缓存,保证用户能立刻看到自己刚发的视频。
产出预告
本章结束时,工程里新增 / 变更的关键文件:
tree
mf
├── mf-app-server
│ ├── src/main/java/com/mf/server
│ │ ├── config/RedisCacheConfig.java # videos 缓存 30 秒
│ │ ├── controller/SmallVideoController.java
│ │ └── service/SmallVideosService.java
│ └── src/test/java/com/wb/test/FastDFSTest.java # FastDFS 入门案例
├── mf-model
│ └── src/main/java/com/mf/model
│ ├── mongo/Video.java # 视频文档
│ ├── mongo/Visitors.java # 访客文档
│ └── vo/VideoVo.java / VisitorsVo.java # 视图对象
└── mf-dubbo
├── mf-dubbo-interface
│ └── api/VideoApi.java / VisitorsApi.java
└── mf-dubbo-mongo
└── api/VideoApiImpl.java / VisitorsApiImpl.java
配置文件侧新增 FastDFS 连接参数(tracker 地址)与 Spring Cache 对 Redis 的启用配置。
依赖关系
#mermaid-svg-beT7jOEZaDXPHyAX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-beT7jOEZaDXPHyAX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-beT7jOEZaDXPHyAX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-beT7jOEZaDXPHyAX .error-icon{fill:#552222;}#mermaid-svg-beT7jOEZaDXPHyAX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-beT7jOEZaDXPHyAX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-beT7jOEZaDXPHyAX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-beT7jOEZaDXPHyAX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-beT7jOEZaDXPHyAX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-beT7jOEZaDXPHyAX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-beT7jOEZaDXPHyAX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-beT7jOEZaDXPHyAX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-beT7jOEZaDXPHyAX .marker.cross{stroke:#333333;}#mermaid-svg-beT7jOEZaDXPHyAX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-beT7jOEZaDXPHyAX p{margin:0;}#mermaid-svg-beT7jOEZaDXPHyAX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-beT7jOEZaDXPHyAX .cluster-label text{fill:#333;}#mermaid-svg-beT7jOEZaDXPHyAX .cluster-label span{color:#333;}#mermaid-svg-beT7jOEZaDXPHyAX .cluster-label span p{background-color:transparent;}#mermaid-svg-beT7jOEZaDXPHyAX .label text,#mermaid-svg-beT7jOEZaDXPHyAX span{fill:#333;color:#333;}#mermaid-svg-beT7jOEZaDXPHyAX .node rect,#mermaid-svg-beT7jOEZaDXPHyAX .node circle,#mermaid-svg-beT7jOEZaDXPHyAX .node ellipse,#mermaid-svg-beT7jOEZaDXPHyAX .node polygon,#mermaid-svg-beT7jOEZaDXPHyAX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-beT7jOEZaDXPHyAX .rough-node .label text,#mermaid-svg-beT7jOEZaDXPHyAX .node .label text,#mermaid-svg-beT7jOEZaDXPHyAX .image-shape .label,#mermaid-svg-beT7jOEZaDXPHyAX .icon-shape .label{text-anchor:middle;}#mermaid-svg-beT7jOEZaDXPHyAX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-beT7jOEZaDXPHyAX .rough-node .label,#mermaid-svg-beT7jOEZaDXPHyAX .node .label,#mermaid-svg-beT7jOEZaDXPHyAX .image-shape .label,#mermaid-svg-beT7jOEZaDXPHyAX .icon-shape .label{text-align:center;}#mermaid-svg-beT7jOEZaDXPHyAX .node.clickable{cursor:pointer;}#mermaid-svg-beT7jOEZaDXPHyAX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-beT7jOEZaDXPHyAX .arrowheadPath{fill:#333333;}#mermaid-svg-beT7jOEZaDXPHyAX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-beT7jOEZaDXPHyAX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-beT7jOEZaDXPHyAX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-beT7jOEZaDXPHyAX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-beT7jOEZaDXPHyAX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-beT7jOEZaDXPHyAX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-beT7jOEZaDXPHyAX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-beT7jOEZaDXPHyAX .cluster text{fill:#333;}#mermaid-svg-beT7jOEZaDXPHyAX .cluster span{color:#333;}#mermaid-svg-beT7jOEZaDXPHyAX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-beT7jOEZaDXPHyAX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-beT7jOEZaDXPHyAX rect.text{fill:none;stroke-width:0;}#mermaid-svg-beT7jOEZaDXPHyAX .icon-shape,#mermaid-svg-beT7jOEZaDXPHyAX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-beT7jOEZaDXPHyAX .icon-shape p,#mermaid-svg-beT7jOEZaDXPHyAX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-beT7jOEZaDXPHyAX .icon-shape .label rect,#mermaid-svg-beT7jOEZaDXPHyAX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-beT7jOEZaDXPHyAX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-beT7jOEZaDXPHyAX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-beT7jOEZaDXPHyAX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} day07 附近的人
MongoDB 熟练度 + 好友关系
我的访客
Visitors 文档
FastDFS Docker 环境
发布小视频
小视频列表
SpringCache 缓存
Redis
首页访客卡片
缓存清空 @CacheEvict
三条技术线在这章交汇:MongoDB 承接访客与视频元数据,FastDFS 承接大文件本体,Redis + SpringCache 承接列表加速。后面章节还会继续复用这三件套。
误解澄清
| 常见误解 | 实际情况 |
|---|---|
| 视频文件存进 MongoDB | 只存 FastDFS 返回的 URL,文件本体在 Storage 上,库里是元数据 |
| Tracker 负责存文件 | Tracker 只调度返回 Storage 地址,实际存储与同步由 Storage 完成 |
| @Cacheable 默认永久缓存 | 默认跟随 CacheManager 配置,本项目对 videos 单独设了 30 秒 TTL |
| 发视频后缓存会自动更新 | 不会,需要显式 @CacheEvict 清空,否则列表看不到新视频 |
| 访客记录存 MySQL | 存 MongoDB 的 visitors 集合,与推荐数据同库不同集合 |
总结
本文围绕小视频模块,把三条技术线串成了一条完整链路:
- 访客记录:沿用 MongoDB 文档存储,一张 visitors 集合承载「谁看过我」,写入挂在业务动作上,查询按缘分值倒序并补全用户信息。
- 视频存储 :FastDFS 负责大文件本体,Tracker 调度、Storage 存储,客户端上传后拿到
group1/M00/xx/xx.png形式的路径,video 文档里只保存这个 URL。 - 列表加速 :Spring Cache 以注解方式给列表查询加缓存,
@Cacheable读缓存、@CacheEvict写后清缓存,底层换成 Redis 只改配置不改代码。
几个容易踩的坑值得记住:视频文件不要塞进 MongoDB,库里只放元数据;Tracker 不存文件,只负责返回可用的 Storage;@Cacheable 不是永久缓存,TTL 要按业务单独配置;发布新视频后必须显式清缓存,否则列表看不到新内容。
把这三件套跑通之后,后续再遇到「大文件存储 + 元数据查询 + 读多写少列表」的场景,基本可以照搬这套组合。
下一站
结构清楚了,从第一个功能开始:先做「我的访客」的需求说明。