简介:弹幕系统是理解高并发写入场景的经典工程模型,其核心在于实时性(<500ms延迟)、高吞吐(百级QPS)与数据一致性之间的平衡。技术原理上依赖WebSocket全双工通信实现低延迟广播,结合Redis内存缓存应对突发写压,再通过异步落库+定时对账保障MySQL最终一致性。该架构规避了轮询的资源浪费和SSE的单向局限,也比秒杀系统更聚焦于‘写多读少’的典型业务特征。在Java后端工程实践中,它自然串联起Spring Boot的MVC分层、MyBatis-Plus批量操作、Redis缓存穿透防护及Nginx反向代理等关键技术点,是毕业设计与新人进阶的理想闭环项目。
1. 项目概述:一个真实跑起来的弹幕视频网站到底长什么样?
你搜“JAVA毕业设计 springboot弹幕视频网站”,页面刷出来一堆压缩包,标题都差不多,点开却常是空目录、报错堆栈、或者连登录页都打不开的半成品。我带过三届计算机专业毕设,每年帮学生 debug 这类项目不下二十个——真正能从零部署、用户可注册、上传视频、发弹幕、后台能删内容、不崩不卡的完整闭环系统,不到两成。这个标题里的“springboot完整源码+说明”,不是指打包了几个 Java 文件就叫完整,而是指它必须覆盖用户端行为流(注册→登录→浏览→播放→发弹幕→点赞)、服务端数据流(视频元信息入库→弹幕实时写入→Redis 缓存命中→MySQL 持久化→后台管理同步刷新)、以及运维支撑流(Nginx 静态资源代理→Spring Boot Actuator 健康检查→Logback 日志分级→Linux 下 service 启动脚本)。它解决的不是“能不能跑”,而是“能不能像一个真实小站那样稳住 50 个并发用户同时发弹幕不丢帧、不延迟、不炸库”。适合两类人:一是大四学生赶毕设 deadline,需要可演示、可答辩、能改能调的底座;二是刚转 Java 的后端新人,想用一个有血有肉的业务场景,把 Spring Boot 的 Controller 层校验、Service 层事务、Mapper 层批量插入、WebSocket 弹幕推送、Redis 缓存穿透防护、MySQL 索引优化这些散知识点串成一条线。别被“弹幕”二字吓住——它本质是个高并发写场景的简化模型,比电商秒杀门槛低,但比博客系统更能暴露真实工程问题。
2. 整体架构设计与技术选型逻辑:为什么不用 Vue+Spring Cloud,而选这套组合?
2.1 分层结构:三层不是摆设,是为毕设答辩留出解释空间
这个项目的分层不是教科书照搬,而是为应对答辩时老师那句“你这个弹幕是怎么实时推给所有人的?”预留了清晰的解释路径。它采用经典MVC + WebSocket + 缓存双写架构:
表现层(View):Thymeleaf 模板引擎 + 原生 JavaScript。没上 Vue/React,因为毕设答辩时老师更关心“你怎么控制弹幕位置和速度”,而不是“你怎么做组件通信”。Thymeleaf 能直接在 HTML 里写
th:if="${user != null}",答辩时打开浏览器开发者工具,指着 DOM 树说“这里判断登录态”,比解释 Vue 的响应式原理直观十倍。JS 部分只做两件事:监听键盘回车触发弹幕发送、用 Canvas 渲染弹幕流——所有计算都在前端,后端只管收和存,降低服务器压力。控制层(Controller):Spring MVC 标准写法,但关键在接口粒度设计。比如
/video/play/{id}不只是返回视频地址,而是返回{videoUrl, title, duration, danmuList}四个字段。其中danmuList是从 Redis 缓存里取的最近 200 条弹幕(不是查库!),避免播放页加载时拖慢首屏。而发弹幕接口/danmu/send是 POST,接收{videoId, content, time},不做任何业务校验(如敏感词过滤放 Service 层),只做基础参数校验(@Valid注解),让 Controller 保持轻量,方便答辩时快速说明“这一层只做路由和参数检查”。服务层(Service):真正的业务中枢。这里藏着三个关键设计:
- 弹幕写入策略:不是简单
danmuMapper.insert(danmu)。而是先写 Redis List(LPUSH danmu:1001 "{'content':'666','time':123.45}"),再异步写 MySQL(用@Async注解标记方法)。为什么?因为弹幕是典型的“写多读少”,Redis 写入毫秒级,MySQL 插入可能因索引、锁导致几十毫秒延迟。如果同步写库,用户发完弹幕要等 200ms 才看到成功提示,体验极差。异步写库还能扛住突发流量——Redis 队列当缓冲池,MySQL 慢点写完就行。 - 视频播放统计:每次
/video/play/{id}被访问,Service 层会执行redis.incr("video:playcount:" + id),同时用EXPIRE设置 24 小时过期。这样首页热门榜直接ZINCRBY hot:videos 1 video:1001就能更新权重,不用每秒查 MySQL 更新播放数,避免热点视频引发的数据库连接池耗尽。 - 后台管理联动:管理员在
/admin/video/delete?id=1001删除视频时,Service 层不仅删 MySQL 的video表记录,还会DEL danmu:1001清 Redis 弹幕缓存,并PUBLISH video:deleted 1001发布消息到 Redis Channel。前端管理页用SUBSCRIBE video:deleted监听,收到消息立刻刷新视频列表——实现“删完即见效果”,比轮询接口优雅得多。
- 弹幕写入策略:不是简单
数据层(DAO):MyBatis-Plus 生成器 + 手动优化 SQL。自动生成的
VideoMapper.xml只保留selectById和insert,复杂查询如“按标签查视频+统计弹幕数”必须手写<select>标签,里面明确写LEFT JOIN danmu ON video.id = danmu.video_id GROUP BY video.id,并加@SelectKey获取自增主键。为什么不用 JPA?因为毕设答辩时老师问“你这个关联查询用了什么优化”,你能指着 XML 里写的USE INDEX (idx_video_tag)说清楚,比解释 Hibernate 的二级缓存机制实在。
2.2 技术栈取舍:为什么选 WebSocket 而不是 SSE 或轮询?
弹幕实时性要求高(延迟 < 500ms),但并发量中等(单视频峰值 200 人),所以必须在实现复杂度和实时性间找平衡点。有人用 Server-Sent Events(SSE),但它单向(服务端→客户端),无法处理“用户发弹幕”的反向请求;也有人用 Ajax 轮询,但每秒一次请求对服务器是灾难——200 个用户就是 200 QPS,光 HTTP 头部解析就压垮 Tomcat。WebSocket 是唯一合理选择,但关键在如何集成进 Spring Boot。
项目用的是spring-boot-starter-websocket,而非 Netty 手写。理由很实际:毕设时间紧,Netty 需要自己处理心跳、断线重连、消息编解码,调试成本太高。Spring 的@MessageMapping注解能直接把 JSON 消息映射到 Java 对象,比如前端发{type:"send",videoId:1001,content:"顶"},后端@MessageMapping("/app/danmu")方法参数直接接DanmuMessage message,Spring 自动完成 JSON → Object 转换。更关键的是,它内置SimpMessagingTemplate,支持向指定用户或频道广播。比如用户 A 发弹幕,后端处理完存库,立刻执行template.convertAndSend("/topic/danmu/1001", danmu),所有订阅/topic/danmu/1001的客户端(即正在看视频 1001 的所有人)都会收到消息。这个设计让答辩时能清晰画出“前端发→WebSocket 入口→Service 处理→Redis 写→WebSocket 广播→前端收”的全链路图,比解释“我用 Redis Pub/Sub 自己实现”更有说服力。
提示:WebSocket 连接数限制是隐形坑。Tomcat 默认最大连接数 10000,但 Linux 系统对单进程文件描述符限制通常是 1024。毕设演示时如果模拟 2000 人并发,必现
java.io.IOException: Too many open files。解决方案不是调大 ulimit(答辩现场没权限),而是在 application.yml 中显式配置:server: tomcat: max-connections: 5000 accept-count: 100这样即使系统级限制未改,Tomcat 也会在队列满时拒绝新连接,返回 503 而非崩溃,保证演示稳定性。
2.3 为什么用 Redis 而不是 MySQL 存弹幕?数据一致性怎么保?
弹幕数据有三大特征:写频次极高(每秒百条)、读时效性强(只读最近 200 条)、持久化要求低(丢了 10 条不影响体验)。MySQL 为保证 ACID,每次写都要落盘、写 redo log、更新索引 B+ 树,单条插入 10ms 是常态。而 Redis 的LPUSH命令是纯内存操作,平均 0.1ms。实测对比:1000 条弹幕写入,MySQL 耗时 8.2 秒,Redis 耗时 0.03 秒。
但问题来了:Redis 数据断电就丢,MySQL 才是唯一真相源。怎么保证“用户看到的弹幕”和“数据库存的弹幕”一致?项目采用缓存双写 + 最终一致性策略:
- 写流程:用户发弹幕 → 先
LPUSH danmu:{videoId} {json}到 Redis → 再异步INSERT INTO danmu (...) VALUES (...)到 MySQL。 - 读流程:播放页加载 →
LRANGE danmu:{videoId} 0 199取最近 200 条 → 如果 Redis 返回空(首次加载或缓存被清),则SELECT * FROM danmu WHERE video_id = ? ORDER BY time DESC LIMIT 200查 MySQL,并将结果RPUSH回 Redis。 - 一致性兜底:每天凌晨 2 点执行定时任务,扫描 Redis 中所有
danmu:*key,对每个 key 执行LLEN获取当前长度,再查 MySQL 对应video_id的弹幕总数。如果 Redis 长度 < MySQL 总数的 95%,则全量同步:SELECT * FROM danmu WHERE video_id = ? ORDER BY id DESC LIMIT 200,然后DEL danmu:{videoId}再RPUSH新数据。这个任务在@Scheduled(cron = "0 0 2 * * ?")下运行,不影响白天业务。
这个设计答辩时很好讲:“Redis 是高速缓存,MySQL 是保险柜。用户看到的是缓存里的快照,保险柜里永远有完整备份。我们用定时任务定期对账,确保快照不偏离真相太远。”比强行追求强一致性(如用分布式事务)更符合毕设务实精神。
3. 核心功能实现细节:从发一条弹幕到它飞过屏幕的全过程
3.1 弹幕发送与接收:WebSocket 如何扛住并发写入?
前端发送弹幕的 JS 代码只有 7 行,但背后是精心设计的防刷机制:
// 前端:发送前本地限频 let lastSendTime = 0; function sendDanmu() { const now = Date.now(); if (now - lastSendTime < 2000) { // 2秒内只能发1条 alert("发送太频繁,请稍后再试"); return; } lastSendTime = now; stompClient.send("/app/danmu", {}, JSON.stringify({ videoId: currentVideoId, content: document.getElementById("danmuInput").value, time: getCurrentVideoTime() // 当前播放进度,单位秒 })); }后端接收端点@MessageMapping("/app/danmu")的方法签名是:
@MessageMapping("/app/danmu") @SendTo("/topic/danmu/{videoId}") // 动态订阅主题 public DanmuVO handleDanmu(@DestinationVariable Long videoId, @Valid DanmuDTO dto, SimpMessageHeaderAccessor header) { // 1. 从 header 获取用户ID(WebSocket 连接时已认证) String username = (String) header.getSessionAttributes().get("username"); if (username == null) throw new RuntimeException("未登录"); // 2. 敏感词过滤(调用 Service 层) String filteredContent = sensitiveWordService.filter(dto.getContent()); // 3. 构建弹幕实体 Danmu danmu = new Danmu(); danmu.setVideoId(videoId); danmu.setContent(filteredContent); danmu.setTime(dto.getTime()); danmu.setUserId(getUserIdByUsername(username)); danmu.setCreateTime(LocalDateTime.now()); // 4. 写 Redis(核心:LPUSH + EXPIRE) redisTemplate.opsForList().leftPush("danmu:" + videoId, JSONObject.toJSONString(danmu)); redisTemplate.expire("danmu:" + videoId, 7, TimeUnit.DAYS); // 7天过期 // 5. 异步写 MySQL danmuService.asyncSave(danmu); // 6. 返回 VO 给前端(含用户头像URL,用于前端显示) return new DanmuVO(danmu, getUserAvatarUrl(username)); }关键点在于@DestinationVariable Long videoId—— 它从 WebSocket 路径/app/danmu/1001中自动提取videoId,让@SendTo("/topic/danmu/{videoId}")能精准广播到该视频专属频道。这样 100 个视频就有 100 个独立频道,互不干扰。实测 200 人同时发弹幕,Tomcat 线程池http-nio-8080-exec-xx占用稳定在 15 个以内,CPU 使用率 35%,远低于警戒线。
注意:
SimMessageHeaderAccessor是获取 WebSocket Session 属性的关键。很多毕设项目在这里翻车——把用户 ID 存在 HTTP Session 里,但 WebSocket 连接是独立的,HTTP Session 属性拿不到。正确做法是在建立 WebSocket 连接时(@EventListener监听SessionConnectedEvent),把用户信息存入header.getSessionAttributes(),后续所有消息都能通过header.getSessionAttributes().get("username")取到。
3.2 弹幕渲染引擎:Canvas 如何实现千条弹幕不卡顿?
前端弹幕渲染不用 CSS 动画(animation: move 10s linear infinite),因为 CSS 动画在 100+ 条弹幕时,浏览器重排重绘压力巨大,iPhone SE 上直接掉帧。项目用 Canvas 2D API 手写渲染循环,核心思想是对象池复用 + requestAnimationFrame 节流:
// 弹幕对象池(避免频繁 new/delete) const danmuPool = []; function getDanmu() { return danmuPool.length > 0 ? danmuPool.pop() : new DanmuObject(); } function recycleDanmu(d) { d.reset(); // 清空属性 danmuPool.push(d); } // 渲染主循环 let danmuList = []; // 当前屏幕上的弹幕数组 function renderLoop() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 清空画布 // 1. 更新所有弹幕X坐标(根据速度和时间差) const now = Date.now(); for (let i = 0; i < danmuList.length; i++) { const d = danmuList[i]; d.x -= d.speed * (now - d.lastTime) / 16; // 16ms 一帧 d.lastTime = now; // 2. 移除飞出屏幕的弹幕 if (d.x + d.width < 0) { recycleDanmu(d); danmuList.splice(i, 1); i--; // 数组变短,索引要回退 } } // 3. 绘制剩余弹幕 for (let d of danmuList) { ctx.font = `${d.fontSize}px sans-serif`; ctx.fillStyle = d.color; ctx.fillText(d.content, d.x, d.y); } requestAnimationFrame(renderLoop); }每条弹幕对象DanmuObject包含x,y,speed,fontSize,color,content等属性,reset()方法将其全部置为默认值。这样创建 1000 条弹幕,实际只 new 了 100 个对象(池大小),其余都是复用。Canvas 渲染帧率稳定在 60fps,即使 iPhone 6s 也能流畅播放。答辩时打开 Chrome DevTools 的 Rendering 面板,能看到“FPS Meter”始终绿条满格,比展示 CSS 动画的掉帧截图更有说服力。
3.3 视频上传与转码:为什么用 FFmpeg 而不是纯 Java 实现?
毕设网站不可能让用户上传 2GB 的原始 MP4,必须转码为 Web 友好格式(H.264+AAC,分辨率 ≤ 1280x720)。Java 生态没有成熟视频转码库,硬编码效率低下。项目采用FFmpeg 命令行调用 + 异步队列方案:
用户上传文件 → Controller 接收
MultipartFile→ 保存到临时目录/tmp/upload/xxx.mp4;Service 层构建 FFmpeg 命令:
ffmpeg -i /tmp/upload/xxx.mp4 \ -c:v libx264 -preset fast -crf 23 -vf "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2" \ -c:a aac -b:a 128k \ -movflags +faststart \ /data/video/1001.mp4关键参数解释:
-preset fast:编码速度与质量平衡,比ultrafast体积小 30%,比medium快 2 倍;-crf 23:恒定质量模式,23 是视觉无损的临界值,再低体积暴增,再高画质肉眼可见下降;-vf:视频滤镜链,先缩放保持宽高比,再加黑边填满 1280x720,避免拉伸变形;-movflags +faststart:把 moov atom 移到文件开头,实现“边下边播”,否则用户得等整个文件下载完才能开始播放。
用
ProcessBuilder执行命令,并重定向 stdout/stderr 到日志文件;转码完成后,用
Files.move()将输出文件移到正式目录,更新 MySQL 中video.url字段。
这个方案的好处是:FFmpeg 是工业标准,参数调优文档齐全,答辩时能说出每个参数的作用。比用 JavaCV 封装 FFmpeg 更透明,也比用云转码服务(如阿里云媒体转码)更可控——毕设演示不能依赖外部网络。
4. 毕设落地关键步骤:从解压源码到成功演示的全流程
4.1 环境准备:JDK、MySQL、Redis、Nginx 四件套怎么配?
这不是复制粘贴就能跑的项目,环境配置是第一道坎。按顺序来:
JDK 1.8:必须用 1.8,因为 Spring Boot 2.7.x(项目所用版本)官方最低要求 JDK 8。
java -version输出必须是1.8.0_3XX。如果装了 JDK 17,mvn clean package会报Unsupported class file major version 61错误。Windows 用户注意:环境变量JAVA_HOME指向C:\Program Files\Java\jdk1.8.0_3XX,PATH加%JAVA_HOME%\bin,别漏掉%符号。MySQL 5.7:用 8.0 会遇到
mysql-connector-java驱动兼容性问题。建库语句:CREATE DATABASE danmu_video DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'danmu'@'%' IDENTIFIED BY 'Danmu@123'; GRANT ALL PRIVILEGES ON danmu_video.* TO 'danmu'@'%'; FLUSH PRIVILEGES;关键是
utf8mb4字符集,否则用户昵称里的 emoji(如 👍)存不进去,变成??。application.yml中 JDBC URL 必须带?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai。Redis 6.2:Windows 用户别用老版 Redis for Windows,推荐用 WSL2 安装原生 Redis。
redis.conf关键配置:bind 0.0.0.0 # 允许远程连接(毕设演示需外网访问) requirepass DanmuRedis@2024 # 密码,application.yml 中要匹配 maxmemory 512mb # 防止吃光内存 maxmemory-policy allkeys-lru # 内存满时淘汰最久未用keyNginx 1.20:不是必须,但强烈建议。作用有三:
- 静态资源代理:
location /video/ { alias /data/video/; },让视频直连 Nginx,不走 Spring Boot,减轻 JVM 压力; - WebSocket 反向代理:
location /ws/ { proxy_pass http://localhost:8080/ws/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; },解决 WebSocket 跨域和连接中断问题; - HTTPS 支持:用 Let's Encrypt 免费证书,演示时地址栏显示绿色小锁,显得更专业。
- 静态资源代理:
实操心得:MySQL 和 Redis 的密码千万别用
root或123456。答辩老师会用telnet your-server-ip 3306测试 MySQL 是否开放,如果密码弱,当场指出“安全性缺失”扣分。我建议密码用Danmu@2024这种带年份和项目名的组合,既易记又满足复杂度要求。
4.2 源码编译与启动:Maven 依赖冲突怎么破?
解压后进入danmu-video目录,执行mvn clean package -Dmaven.test.skip=true。常见失败原因及解法:
Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile:说明 JDK 版本不对。检查
pom.xml中<java.version>1.8</java.version>,再确认mvn -v输出的 Java 版本。Could not resolve dependencies for project ...:spring-boot-starter-websocket:jar:2.7.18:Maven 中央仓库抽风。在
pom.xml的<repositories>下加阿里云镜像:<repository> <id>aliyunmaven</id> <name>Aliyun Maven</name> <url>https://maven.aliyun.com/repository/public</url> </repository>Caused by: java.lang.NoClassDefFoundError: org/apache/commons/io/FileUtils:MyBatis-Plus 依赖的 commons-io 版本冲突。在
pom.xml中强制指定版本:<dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency>
打包成功后,target/danmu-video-1.0.jar就是可执行文件。启动命令:
java -jar -Dspring.profiles.active=prod danmu-video-1.0.jar-Dspring.profiles.active=prod指定生产环境配置,会读取application-prod.yml,里面包含真实的 MySQL 和 Redis 地址。别用devprofile,它默认 H2 内存数据库,演示时数据全丢。
4.3 首次访问与数据初始化:如何快速获得可演示内容?
Jar 启动后,浏览器访问http://your-server-ip:8080,会看到登录页。但此时数据库是空的,没视频、没用户。项目提供init.sql初始化脚本,包含:
- 管理员账号:
admin / Admin@123(MD5 加密存储); - 3 个测试视频:
/data/video/1001.mp4(已转码好的 10MB 文件),MySQL 中对应video表记录; - 50 条测试弹幕:
INSERT INTO danmu (video_id, content, time, user_id, create_time) VALUES (1001, '这视频太棒了', 12.34, 1, '2024-01-01 10:00:00');。
执行方式:用 MySQL 客户端连接,source /path/to/init.sql。注意路径要用绝对路径,且init.sql中的视频文件路径/data/video/1001.mp4必须和你服务器上的实际路径一致。如果视频文件放在/home/user/videos/,就要把 SQL 里的路径全替换成那个。
踩过的坑:
init.sql里INSERT语句的create_time用'2024-01-01 10:00:00',但 MySQL 严格模式下会报错Incorrect datetime value。解决方案是改成NOW(),或在my.cnf中关闭严格模式:sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION。
4.4 演示话术设计:答辩时如何讲清楚技术亮点?
毕设答辩不是代码朗诵,而是讲故事。我帮学生设计的 5 分钟话术框架:
开场(30秒):“老师好,我做的是一款支持实时弹幕互动的视频网站,核心目标是验证 Spring Boot 在高并发写场景下的工程实践能力。系统已部署在腾讯云轻量应用服务器,公网可访问。”
架构亮点(90秒):“架构上采用分层设计。表现层用 Thymeleaf 保证答辩可解释性;控制层用 RESTful 接口,发弹幕接口
/danmu/send做了参数校验;服务层重点解决了弹幕高并发写问题——先写 Redis 缓存,再异步写 MySQL,实测 200 并发下延迟 < 200ms;数据层用 MyBatis-Plus,但复杂查询手写 SQL 并加索引优化。”技术难点(90秒):“最大的难点是弹幕实时性与稳定性的平衡。我选 WebSocket 而非轮询,用
@SendTo("/topic/danmu/{videoId}")实现精准广播;为防刷,前端加 2 秒限频,后端用 Redis List 做缓冲;为保数据,设计缓存双写+每日对账机制,确保 Redis 和 MySQL 数据最终一致。”演示环节(60秒):“现在我演示:登录管理员账号,上传一个新视频(点击‘上传视频’按钮,选择本地文件,等待转码完成);然后用普通用户账号登录,播放这个视频,发送弹幕(输入‘答辩顺利’,回车);可以看到弹幕实时飞过屏幕,同时后台管理页的弹幕数立刻增加——证明 WebSocket 广播和数据同步生效。”
总结(30秒):“通过这个项目,我掌握了 Spring Boot 全栈开发流程,特别是高并发场景下的缓存设计、异步处理和 WebSocket 集成。代码已开源,文档齐全,欢迎老师批评指正。”
这套话术把技术点转化为问题-方案-效果的逻辑链,比罗列“用了 Spring Boot、Redis、MySQL”有力得多。
5. 常见问题排查与避坑指南:那些让毕设挂掉的隐形炸弹
5.1 启动报错:java.lang.IllegalStateException: Failed to load ApplicationContext
这是 Spring Boot 启动失败的万能错误,根源在ApplicationContext初始化失败。按优先级排查:
| 现象 | 原因 | 解决方案 |
|---|---|---|
Caused by: java.net.ConnectException: Connection refused (Connection refused) | MySQL 或 Redis 服务没启动,或application.yml中 host/port 写错 | systemctl status mysqld和redis-cli -h 127.0.0.1 -p 6379 -a DanmuRedis@2024 ping测试连通性;检查application-prod.yml中spring.redis.host是否为127.0.0.1(不是localhost,某些 DNS 解析慢) |
Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure | MySQL 连接超时,或max_connections被占满 | 在 MySQL 中执行SHOW VARIABLES LIKE 'max_connections';,如果 < 200,执行SET GLOBAL max_connections = 500;;检查application.yml中spring.datasource.hikari.connection-timeout是否设为30000(30秒) |
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' | MyBatis Mapper XML 文件路径错误,或@MapperScan注解没扫到包 | 确认resources/mapper/目录下有VideoMapper.xml,且pom.xml中<build>段有<resources>配置,确保 XML 文件被打包进 jar;检查@MapperScan("com.danmu.mapper")的包路径是否和实际 mapper 接口路径一致 |
注意:
application.yml中数据库密码如果含特殊字符(如@),必须用单引号包裹:password: 'Danmu@2024'。否则 YAML 解析器会把@当作锚点,导致密码截断。
5.2 功能异常:弹幕发不出、视频播不了、后台登不上
| 问题 | 排查路径 | 关键命令/操作 |
|---|---|---|
弹幕发不出,控制台报WebSocket is already in CLOSING or CLOSED state | WebSocket 连接被意外关闭 | 检查 Nginx 配置中proxy_read_timeout 300;是否设置(默认 60 秒,WebSocket 长连接需更长);浏览器 F12 Network 标签页,筛选WS,看连接状态是否为Established |
视频播放页空白,Network 显示404 Not Found | Nginx 静态资源代理失效 | curl -I http://localhost/video/1001.mp4,如果返回404,检查 Nginxlocation /video/的alias路径是否指向/data/video/,且该目录有1001.mp4文件,权限为nginx用户可读(chown -R nginx:nginx /data/video/) |
| 管理员登录失败,提示“用户名或密码错误” | 密码加密方式不匹配 | 检查UserServiceImpl中密码加密是否用BCryptPasswordEncoder.encode("Admin@123");对比数据库user.password字段值,是否以$2a$10$开头(BCrypt 标识);如果不是,手动执行UPDATE user SET password='$2a$10$...' WHERE username='admin'; |
5.3 性能瓶颈:演示时卡顿、响应慢、CPU 爆表
| 现象 | 根因分析 | 优化措施 |
|---|---|---|
播放页加载慢,Chrome Network 显示video/1001.mp4请求耗时 > 5s | 视频文件太大,或 Nginx 没启用 gzip | 在nginx.conf的http块中加:gzip on;gzip_types application/javascript text/css text/xml;(注意:视频文件不 gzip,但 HTML/CSS/JS 会压缩,减少首屏加载时间) |
| 发弹幕后,其他用户 3 秒才看到 | WebSocket 广播延迟,或 Redis 写入慢 | 检查redis-cli monitor,看LPUSH danmu:1001命令是否立即执行;如果延迟,可能是 Redis 持久化阻塞(save 900 1触发 RDB),改为appendonly yes+appendfsync everysec |
Tomcat 线程池http-nio-8080-exec-xx占满,top显示 Java 进程 CPU 99% | 某个接口死循环,或数据库慢查询 | jstack -l <pid>导出线程堆栈,搜索RUNNABLE状态的线程,看是否卡在DanmuService.save()或VideoMapper.selectById();用show processlist查 MySQL 是否有Sleep状态的长连接 |
实操心得:演示前必做三件事:1.
free -h看内存剩余 > 1G;2.df -h看磁
本文还有配套的精品资源,点击获取