ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SpringBoot+MySQL短视频网站开发:从建表到分页优化实战

2026/9/17 8:22:20 拓冰建站 浏览量
SpringBoot+MySQL短视频网站开发:从建表到分页优化实战 简介基于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. SpringBootMySQL 开发短视频网站的选型边界短视频网站从技术栈上看并不复杂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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这两张表是主干。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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;联合唯一键是这三张表的重点。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对应playCountMyBatis-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}/like3.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 ResultVideoVO 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 ResponseEntityResource 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); }返回ResponseEntityResource时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}) ListVideoCardVO 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.xurl 需要写com.mysql.cj.jdbc.Driver。SpringBoot 3.x 对 Jakarta 命名空间有要求如果升级前没改包名启动时会出现ClassNotFoundException。spring: datasource: url: jdbc:mysql://localhost:3306/short_video?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.DriverserverTimezoneAsia/Shanghai避免时间序列化差 8 小时allowPublicKeyRetrievaltrue解决 MySQL 8.0 默认认证插件下首次连接时报Public Key Retrieval is not allowed的问题。这里要留意useSSLfalse只是开发环境跳过证书校验生产环境要么配置证书要么走内网连接。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?cursorTime2025-01-01T00:00:00size10第一条命令返回视频 ID 和videoUrl第二条命令用-I只拿响应头重点看Content-Type是否video/mp4、Content-Length是否和文件大小一致状态码是否为 200。第三条命令用游标分页拉列表确认排序和分页参数都能正常工作。返回结果里带着刚发布的视频并能在浏览器里直接播放这一套后端主干就算验证通过了。本文还有配套的精品资源点击获取