Java 项目实战: 交友项目 - 小视频模块实战:访客记录、FastDFS 存储与 SpringCache 缓存

概述

本文补齐小视频模块:记录「谁看过我」的访客数据,用 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 要按业务单独配置;发布新视频后必须显式清缓存,否则列表看不到新内容。

把这三件套跑通之后,后续再遇到「大文件存储 + 元数据查询 + 读多写少列表」的场景,基本可以照搬这套组合。

下一站

结构清楚了,从第一个功能开始:先做「我的访客」的需求说明。

相关推荐
香蕉大香蕉(06敬畏市场)1 小时前
MySQL 单点详解(09)
java·数据库·mysql·面试·八股·大厂实习
吹什么轩2 小时前
constexpr 完全指南:从 const 的两种含义,到编译期计算的四次演进
java·前端·javascript
她的男孩2 小时前
租户 A 的验证码,却用了租户 B 的 AccessKey:Forge Admin 修跨租户短信串发
java·spring boot·saas
LabVIEW开发2 小时前
LabVIEW 局部变量与数据流:这道 CLAD 题为什么答案是 Undefined
java·开发语言·labview
文丑颜不良啊2 小时前
LeetCode1721~1740题目解析
java·leetcode
wangbing11252 小时前
监控网页项目常用英文缩写
java
麻瓜code2 小时前
双写一致性:为什么是“删缓存“,而不是“改缓存“
java·缓存·mybatis
小蒜学长2 小时前
基于SpringBoot+Vue的云南地区扶贫助农系统的设计与实现(代码+数据库+LW)
java·vue.js·spring boot·后端·乡村振兴
砚底藏山河2 小时前
量化回测可复现流水线CI化
java·python·ci/cd·金融