简介:基于Java(SpringBoot)+MySQL的短视频网站完整项目源码,面向正在学习Spring Boot整合开发或需要快速搭建视频类Web应用的后端开发者。项目涵盖信息中心、用户中心、视频管理与后台管理四大模块,包括用户关注、私信、好友动态、视频推荐、实时弹幕、点赞点评及审核上架等完整业务闭环,代码结构清晰,适合课程设计、毕业设计或项目实训参考。压缩包共222个文件,以101个Java源码文件为主,另含25个JS脚本、16个CSS样式、15个XML配置、13个HTML页面以及SQL数据库脚本等,整体包体仅7.34MB,便于下载与本地部署。项目集成Flowplayer与FFmpeg处理视频播放与转码,并运用MyBatis、Thymeleaf等技术,能够帮助开发者理解短视频系统的前后端交互与后台管理流程。已有616人学习下载,适用于具备一定Java基础、希望深入掌握SpringBoot全家桶实战的读者。
1. SpringBoot+MySQL 开发短视频网站的选型边界
短视频网站从技术栈上看并不复杂,SpringBoot 负责对外暴露接口和业务编排,MySQL 保存用户、作品、互动数据,视频文件本身通常放本地磁盘或对象存储。真正容易被低估的是表数据设计和查询方式:一个视频详情、关注列表、评论流,背后都是关系型数据的组合查询。对于中小型项目、毕设或企业内部系统,这套组合能覆盖绝大多数需求;反过来,如果一开始就上微服务和分布式缓存,往往把问题复杂化。下面按最常见的做法,从一个可跑的 SpringBoot + MySQL 工程出发,把建表、接口、分页查询、部署配置和踩坑都过一遍。读完后你能独立搭出短视频网站的后端主干。
2. MySQL 表结构与 SpringBoot 实体映射的落地
2.1 短视频网站的核心表与建表语句
短视频业务看起来功能多,落到关系型数据库里只有两类对象:用户(User)和作品(Video)。关注、点赞、评论都是用户与作品或用户与用户之间的关系表。设计时先别急着加字段,把下面这几张表立住,接口阶段会顺手很多。
CREATE TABLE IF NOT EXISTS `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `nickname` varchar(50) NOT NULL, `avatar_url` varchar(255) DEFAULT '', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS `video` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `title` varchar(100) NOT NULL, `description` text, `video_url` varchar(255) NOT NULL, `cover_url` varchar(255) DEFAULT '', `duration` int DEFAULT 0, `play_count` bigint DEFAULT 0, `like_count` bigint DEFAULT 0, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_created_at` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这两张表是主干。user表用自增主键,短视频场景不需要跨库合并,自增足够了。video表给user_id和created_at分别建索引,是因为后续列表查询和“我的作品”查询都依赖这两个字段。duration放视频时长秒数,用int比字符串更适合排序和展示。play_count和like_count是冗余计数,先落表,没必要在列表页实时 count。
关注、点赞、评论三张关系表
CREATE TABLE IF NOT EXISTS `follow` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `follow_user_id` bigint NOT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_follow` (`user_id`, `follow_user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS `like_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `video_id` bigint NOT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_video` (`user_id`, `video_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS `comment` ( `id` bigint NOT NULL AUTO_INCREMENT, `video_id` bigint NOT NULL, `user_id` bigint NOT NULL, `content` varchar(500) NOT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_video_id` (`video_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;联合唯一键是这三张表的重点。follow表用(user_id, follow_user_id)防止重复关注,like_record表用(user_id, video_id)防止对同一个视频重复点赞。业务里判断“是否已关注”或者“是否已点赞”,本质上就是查这条唯一记录是否存在,索引命中很快,不需要额外加字段。
2.2 字段类型与索引选择参数
字段类型选得对不对,表数据量到几十万的时候会直接体现。下表是短视频网站常见字段的推荐类型。
| 字段含义 | 推荐类型 | 原因 |
|---|---|---|
| 视频时长 | int | 单位秒,排序和计算都方便 |
| 播放量 | bigint | 计数可能超过 int 上限 |
| 标题 | varchar(100) | 标题不是正文,够用且索引友好 |
| 描述 | text | 内容可长可短,text 足够 |
| 视频地址 | varchar(255) | 存相对路径或对象存储 key |
| 创建时间 | datetime | 配合索引做列表排序 |
索引原则是“最左前缀”。列表页最常见的查询是WHERE user_id = ? ORDER BY created_at DESC,所以联合索引(user_id, created_at)比两个独立索引更合适,回表次数更少。如果你用的是 MySQL 8.0,可以顺手把索引名规范成idx_开头,方便SHOW INDEX FROM video排查索引是否真正生效。
2.3 实体类与 MyBatis-Plus 映射
Mapper 层建议直接用 MyBatis-Plus,减少单表 CRUD 代码。实体类通过注解和表结构对应,字段使用驼峰命名,框架会自动做下划线转换。
@Data @TableName("video") public class Video { @TableId(type = IdType.AUTO) private Long id; private Long userId; private String title; private String description; private String videoUrl; private String coverUrl; private Integer duration; private Long playCount; private Long likeCount; private LocalDateTime createdAt; }@TableName指定表名,@TableId标记主键并且用IdType.AUTO走数据库自增。注意play_count对应playCount,MyBatis-Plus 默认开启驼峰映射,不需要额外配置。如果项目用的是 Spring Data JPA,思路完全一致,只是注解换成@Entity和@Id。
2.4 表不存在自动建表的基础配置
开发阶段最省事的方案是用 SQL 初始化脚本。把建表语句放到src/main/resources/schema.sql,然后在application.yml里开启初始化。
spring: sql: init: mode: always schema-locations: classpath:schema.sql continue-on-error: truemode: always表示每次启动都执行,continue-on-error保证CREATE TABLE IF NOT EXISTS之外的重复执行报错不阻断启动。注意这套机制只建议开发环境用,生产环境还是用 Flyway 管理版本更稳妥,否则字段演化没有记录。MyBatis-Plus 本身没有内置自动建表,用上面的 Spring SQL 初始化就是最直接的做法。表结构一旦稳定,就把mode改成never,避免每次启动都执行一遍 DDL。
提示:实际开发中,很多人会把建表语句交给 DBA 手工执行。
schema.sql只是让你本地环境能快速拉起一个可运行的项目。
3. SpringBoot 接口开发与视频文件读取
3.1 接口设计:先定 URL 再写代码
短视频网站后端接口大体可以分成三类:内容上传、内容播放、内容列表。播放和列表是读多写少的典型场景。设计 URL 时,把动词封装进 HTTP 方法,路径只描述资源。
| 操作 | 请求方式 | 路径 |
|---|---|---|
| 发布视频 | POST | /api/videos |
| 播放视频 | GET | /api/videos/{id}/stream |
| 视频分页列表 | GET | /api/videos |
| 点赞视频 | POST | /api/videos/{id}/like |
| 取消点赞 | DELETE | /api/videos/{id}/like |
3.2 视频文件存本地还是对象存储
中小型项目或者课程设计阶段,视频文件直接存本地磁盘就能跑通。用下面这段配置把本地目录映射成静态资源。
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceLocations("file:" + System.getProperty("user.dir") + "/upload/"); } }/files/**是浏览器访问的 URL 前缀,后面的路径指向项目根目录下的 upload 目录。文件上传时把视频写入这个目录,数据库里只存video_url = /files/xxx.mp4,播放时直接拼域名访问。这样做的好处是视频文件和业务数据分离,后续接入对象存储时只需要改上传代码,接口 URL 形态不变。
3.3 视频发布接口的实现
发布接口需要接收文件二进制和基本信息,先把文件保存到磁盘,再把元数据写入 MySQL。
@PostMapping("/api/videos") public Result<VideoVO> publish(@RequestParam("file") MultipartFile file, @RequestParam("title") String title, @RequestParam(value = "description", required = false) String description) { String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String filename = UUID.randomUUID().toString().replace("-", "") + ext; String savePath = System.getProperty("user.dir") + "/upload/" + filename; try { file.transferTo(new File(savePath)); } catch (IOException e) { throw new RuntimeException("视频保存失败", e); } Video video = new Video(); video.setUserId(getCurrentUserId()); video.setTitle(title); video.setDescription(description); video.setVideoUrl("/files/" + filename); video.setDuration(0); videoService.save(video); return Result.success(VideoVO.from(video)); }MultipartFile是 SpringBoot 内置的上传对象,getOriginalFilename()只能拿文件名,服务端不能直接信任,重新用 UUID 生成文件名可以避免路径穿越和文件覆盖。transferTo做的是临时文件到目标目录的原子移动,比FileOutputStream更可靠。video_url只存相对路径,播放时由部署域名拼写,方便后续迁移 CDN。duration字段这里先用 0 占位,真实时长需要调 FFmpeg 读取,不影响主流程。
3.4 视频播放接口与资源下载
播放接口的目的是把视频文件以流的形式返回给前端或播放器。
@GetMapping("/api/videos/{id}/stream") public ResponseEntity<Resource> stream(@PathVariable Long id) { Video video = videoService.getById(id); if (video == null) { return ResponseEntity.notFound().build(); } String filename = video.getVideoUrl().replace("/files/", ""); File file = new File(System.getProperty("user.dir") + "/upload/" + filename); Resource resource = new FileSystemResource(file); return ResponseEntity.ok() .contentType(MediaType.parseMediaType("video/mp4")) .header(HttpHeaders.CONTENT_DISPOSITION, "inline") .body(resource); }返回ResponseEntity<Resource>时,Spring 会利用底层 Servlet 容器的文件流能力做零拷贝传输,不用自己读 byte 数组。contentType要按实际文件类型设置,video/mp4是大多数短视频的默认格式。CONTENT_DISPOSITION用inline而不是attachment,这样浏览器支持直接播放而不是下载。如果视频需要做转码,通常在这条链路里接入 FFmpeg 或者其他媒体处理服务,再单独开一个转码状态字段。
4. 短视频列表查询与 MySQL 慢查询优化
4.1 分页查询的深分页问题
短视频网站最频繁的接口是首页视频列表。最常见的写法是LIMIT offset, size,数据量小的时候没问题,一旦数据到几十万条,偏移量越大,MySQL 需要扫描越多的行再丢弃,慢查询就出现了。
SELECT id, title, video_url, play_count, created_at FROM video ORDER BY created_at DESC LIMIT 200000, 20;这个查询即使created_at有索引,MySQL 也要读取 200020 行,然后丢掉前 200000 行。优化方向是尽量让索引覆盖查询字段,或者改成游标分页。短视频 App 的“下拉刷新”场景天然适合游标分页,因为客户端不需要随机跳页,只需要拿到上一页最后一条数据的时间或 ID。
4.2 开启 MySQL 慢查询日志定位慢 SQL
打开慢查询日志是排查的第一步,MySQL 提供了临时变量和运行时变量两种设置方式。
# 查看当前慢查询配置 SHOW VARIABLES LIKE 'slow_query_log'; # 开启慢查询,阈值设为 1 秒 SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1;设置完成后,执行SHOW VARIABLES LIKE 'slow_query_log_file'得到日志文件路径。接下来跑一次列表接口,只要超过 1 秒就会被记录。日志里会有具体的 SQL 文本和耗时,把它复制出来执行EXPLAIN,重点看type、key、rows三列。type从ALL变成range或ref,说明查询走了索引;rows是估算扫描行数,持续变大就该优化了。注意SET GLOBAL只对当前实例有效,重启 MySQL 会恢复原始值,生产环境需要写入配置文件。
4.3 用游标分页取代深分页
把列表接口的page参数改成cursorTime,用上次返回的最后一条created_at作为查询条件。
SELECT id, title, video_url, play_count, created_at FROM video WHERE created_at < #{cursorTime} ORDER BY created_at DESC LIMIT 20;SQL 在 SpringBoot 里的落地可以用 MyBatis 注解直接写。
@Select("SELECT id, title, video_url, play_count, created_at " + "FROM video " + "WHERE created_at < #{cursorTime} " + "ORDER BY created_at DESC LIMIT #{size}") List<VideoCardVO> listByCursor(@Param("cursorTime") LocalDateTime cursorTime, @Param("size") int size);游标分页的核心条件是用<而不是!=,保证每次都拿当前游标之前的数据。由于created_at有索引,MySQL 可以直接定位到游标位置再顺序读 20 行,扫描量不会随页码变大。隐患是同一条created_at数据可能重复出现,解决方法是加一个id作为二级排序字段,或者把游标换成(created_at, id)联合游标。这里要求created_at索引能覆盖ORDER BY,所以建表时单独建这个索引就够了。
4.4 计数优化与列表字段瘦身
视频列表不需要展示全部字段,尤其是description这种 text 类型。每次列表查询都带上它,会导致行长度变大、回表次数变多。常见的处理方式是把列表 SQL 的字段限定为卡片组件需要的那几列,详情接口再单独查询完整字段。
点赞、播放量这类数字在列表展示时,不要对每行执行子查询 count。现在表里直接用like_count和play_count冗余存储,更新逻辑在点赞和播放接口里通过事务完成。
UPDATE video SET play_count = play_count + 1 WHERE id = #{videoId};这条语句是原地更新,不会产生先查后改的竞态。播放量允许轻微丢失,直接用这条 SQL 即可。要保证精确,就追加一张播放流水表,但列表页仍然读冗余字段。这是短视频项目中很常见的读写分离思路,读和写各走各的表。
注意:
LIKE '%关键词%'的搜索场景走不到索引。如果业务需要搜索视频标题,等标题量大之后再上全文检索(Elasticsearch 或 Meilisearch),不要试图用 MySQL 解决所有搜索问题。
5. 部署配置与短视频接口联调验证
5.1 SpringBoot 版本与 MySQL 8.0 驱动的兼容配置
SpringBoot 版本太高时,最容易踩的是 MySQL 驱动和时区问题。SpringBoot 2.7 默认引入 mysql-connector-j 8.0.x,url 需要写com.mysql.cj.jdbc.Driver。SpringBoot 3.x 对 Jakarta 命名空间有要求,如果升级前没改包名,启动时会出现ClassNotFoundException。
spring: datasource: url: jdbc:mysql://localhost:3306/short_video?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.DriverserverTimezone=Asia/Shanghai避免时间序列化差 8 小时;allowPublicKeyRetrieval=true解决 MySQL 8.0 默认认证插件下首次连接时报Public Key Retrieval is not allowed的问题。这里要留意,useSSL=false只是开发环境跳过证书校验,生产环境要么配置证书,要么走内网连接。
5.2 视频上传大小与请求超时参数
默认情况下 SpringBoot 单次上传大小上限是 1MB,短视频动辄几十 MB,不调整会被拒绝。在application.yml里追加配置:
spring: servlet: multipart: max-file-size: 100MB max-request-size: 100MBmax-file-size限制单个文件,max-request-size限制一次请求的总大小。如果视频要支持分片上传,还需要注意代理层上传超时,Nginx 的client_max_body_size默认只有 1m。生产环境把视频上传和业务接口分离到不同域名,避免大文件上传阻塞核心接口。
5.3 用 curl 验证短视频接口全链路
接口写完,先用 curl 把发布、列表、播放三条链路跑通。
curl -X POST -F "file=@/tmp/demo.mp4" -F "title=测试视频" \ http://localhost:8080/api/videos # 返回中的 videoUrl 记录下来,拼上接口前缀请求 curl -I http://localhost:8080/files/xxx.mp4 # 拉取列表,确认新视频在列表第一条 curl "http://localhost:8080/api/videos?cursorTime=2025-01-01T00:00:00&size=10"第一条命令返回视频 ID 和videoUrl,第二条命令用-I只拿响应头,重点看Content-Type是否video/mp4、Content-Length是否和文件大小一致,状态码是否为 200。第三条命令用游标分页拉列表,确认排序和分页参数都能正常工作。返回结果里带着刚发布的视频,并能在浏览器里直接播放,这一套后端主干就算验证通过了。
本文还有配套的精品资源,点击获取