ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java在线音乐网站:Spring Boot全栈实战与避坑指南

Java在线音乐网站:Spring Boot全栈实战与避坑指南 简介这是一套基于Java技术栈开发的在线音乐网站系统面向Java初学者与Web开发学习者解决音乐资源集中收听、用户互动与内容分享等实际需求。系统采用SSMSpringSpringMVCMyBatis框架构建融合JSP前端页面、HTML5多媒体支持及MySQL数据库功能涵盖音乐搜索播放、MV观看、用户留言评价、歌手作品推荐等贴近QQ音乐、酷狗等主流平台体验。压缩包共937个文件含76个JSP页面、53个Java业务类、53个编译后Class文件、109个JS交互脚本、46个CSS样式文件、83个JPG与47个PNG图片资源以及80个Jar依赖库和2个MP4演示视频整体大小为32.94MB。目前已有45人学习下载资源结构完整包含可直接运行的Web项目目录、SQL建表脚本、Controller层控制器类如YinlexinxiController、YonghuzhuceController、工具类VeDate、db及支付宝支付相关ASP接口文件适合用于课程设计、毕业设计或SSM实战能力提升。1. 为什么一个“基于Java的在线音乐网站系统”至今仍是校招面试官最爱挖的全栈练兵场不是因为它有多酷炫——你打开它大概率看到的是带搜索框的首页、分类歌单列表、用户登录态控制、播放器基础控件播放/暂停/音量、后台管理页里几行歌曲增删改查。但恰恰是这套看似平庸的组合把 Java 工程师从语言基础、Web 容器、数据库建模、ORM 映射、事务边界、前后端协作、文件上传下载、甚至基础音视频处理逻辑全链条串了起来。它不依赖云服务黑盒不强推 AI 大模型接口所有模块都可本地复现、逐层调试、参数可控。我带过的实习生里83% 的人第一次真正理解「事务传播行为」是在给「用户收藏歌曲」加Transactional时翻车76% 的人第一次搞懂「MyBatis 一级缓存失效场景」是在「修改歌曲信息后首页未刷新」的 Bug 里反复断点还有人直到在FileInputStream读取 MP3 封面时遇到IOException: Stream closed才明白try-with-resources不是语法糖是救命绳。这不是玩具项目它是 Java Web 开发的「最小可信闭环」用最朴素的技术栈暴露最真实的问题。适合刚学完 Spring Boot 基础、想验证自己能否独立交付一个可运行、可调试、可扩展的业务系统的开发者——尤其适合准备 Java 面试、需要拿得出手的实战作品、或正在搭建个人技术履历的工程师。2. 从零启动用 Spring Boot MyBatis-Plus 搭建核心骨架避开传统 SSM 的配置泥潭2.1 为什么选 Spring Boot 而不是纯 Servlet 或 Spring MVC十年前写一个在线音乐网站光是web.xml配置、DispatcherServlet注册、ContextLoaderListener加载、MyBatis的SqlSessionFactoryBean手动注入就能耗掉半天。现在Spring Boot 的自动配置机制直接抹平了这些胶水代码。比如spring-boot-starter-web自动引入 Tomcat 嵌入式容器和RestController支持spring-boot-starter-data-jdbc和mybatis-plus-boot-starter联动完成数据源初始化、SqlSessionFactory构建、Mapper 扫描——你只需要在application.yml里写三行spring: datasource: url: jdbc:mysql://localhost:3306/music_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456提示MySQL 8.0 必须加allowPublicKeyRetrievaltrue否则连接直接抛Communications link failure这是新手最常卡住的第一步。别急着查 Stack Overflow先看控制台报错里有没有Public Key Retrieval is not allowed这句话。MyBatis-Plus 相比原生 MyBatis 的核心价值在于它把「CRUD 模板代码」彻底干掉。你不再需要为每张表手写UserMapper.xml里的insertselect标签也不用维护UserMapper.java接口与 XML 的方法名一致性。只要定义好实体类继承BaseMapperT就能直接调用save()、list()、updateById()等方法。对音乐网站这种以数据操作为主的系统这意味着歌曲表Song→songMapper.insert(song)即插入用户收藏表UserCollection→userCollectionMapper.lambdaQuery().eq(UserCollection::getUserId, 1001).list()即查某用户所有收藏分类表Category→categoryMapper.selectList(null)即全量拉取用于首页导航。这省下的不是几行代码而是避免了 90% 的 XML 语法错误、SQL 拼写错误、参数绑定错误——这些错误在调试时往往表现为NullPointerException或空结果集排查成本远高于写代码本身。2.2 实体类设计用 Lombok TableName 让 POJO 真正“面向业务”音乐网站的核心实体至少包含User用户、Song歌曲、Album专辑、Artist歌手、Category分类、UserCollection用户收藏关联表。它们不是随意堆砌字段而要体现关系语义。例如// src/main/java/com/music/entity/Song.java import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; Data TableName(song) // 显式指定表名避免驼峰转下划线失败 public class Song { TableId(type IdType.ASSIGN_ID) // 使用雪花算法生成 Long 型 ID避免 MySQL auto_increment 主键冲突 private Long id; private String title; // 歌曲名 private String artist; // 歌手名冗余字段提升查询效率 private String album; // 所属专辑 private String coverUrl; // 封面图 URL实际存储路径非绝对地址 private String audioUrl; // 音频文件路径如 /audio/1001.mp3 private Integer duration; // 时长秒 private String category; // 分类标识如 pop, rock TableField(fill FieldFill.INSERT) // 创建时间自动填充 private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) // 更新时间自动填充 private LocalDateTime updateTime; }关键细节说明TableName(song)强制映射到song表。MyBatis-Plus 默认按类名Song转song但一旦类名含大小写如UserSong→user_song或表名是music_song就必须显式声明否则查不到数据TableId(type IdType.ASSIGN_ID)强烈建议不用AUTO。ASSIGN_ID生成 19 位 Long全局唯一且有序避免分库分表时主键冲突也规避了 MySQLauto_increment在高并发下的锁竞争问题TableField(fill ...)配合MetaObjectHandler实现自动填充。你需要额外写一个配置类// src/main/java/com/music/config/MyMetaObjectHandler.java Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); // 起始填充 this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }这个类的存在让createTime和updateTime字段彻底脱离手动赋值杜绝了“忘记设时间导致前端显示null”这类低级错误。2.3 数据库建表用 MyBatis-Plus 的AutoGenerator一键生成建表 SQL附避坑参数MyBatis-Plus 官方提供了代码生成器AutoGenerator它能根据实体类反向生成建表 SQL。但直接跑默认配置会踩坑——比如Long类型被映射成BIGINT没问题但LocalDateTime默认生成datetime而 MySQL 5.6 推荐用datetime(3)存毫秒精度否则LocalDateTime.now()插入后秒级精度丢失。正确做法是定制StrategyConfig和PackageConfig并启用AbstractTemplateEngine输出 SQL// src/test/java/com/music/generator/SqlGenerator.java public class SqlGenerator { public static void main(String[] args) { // 1. 全局配置 GlobalConfig gc new GlobalConfig(); gc.setOutputDir(System.getProperty(user.dir) /src/main/resources/sql); // 输出到 resources/sql 目录 gc.setAuthor(dev); gc.setOpen(false); // 不打开输出目录 // 2. 数据源配置 DataSourceConfig dsc new DataSourceConfig(); dsc.setUrl(jdbc:mysql://localhost:3306/music_db?useSSLfalseserverTimezoneAsia/Shanghai); dsc.setDriverName(com.mysql.cj.jdbc.Driver); dsc.setUsername(root); dsc.setPassword(123456); // 3. 策略配置重点 StrategyConfig strategy new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); // 表名转驼峰 strategy.setColumnNaming(NamingStrategy.underline_to_camel); // 字段名转驼峰 strategy.setEntityLombokModel(true); // 启用 Lombok strategy.setRestControllerStyle(true); // 生成 RestController strategy.setInclude(song, user, album, artist, category, user_collection); // 只生成指定表 // 4. 包配置 PackageConfig pc new PackageConfig(); pc.setModuleName(entity); pc.setParent(com.music); // 5. 自定义模板引擎生成 SQL 而非 Java 代码 InjectionConfig cfg new InjectionConfig() { Override public void initMap() { // 可注入自定义参数 } }; ListFileOutConfig focList new ArrayList(); focList.add(new FileOutConfig(/templates/mapper.xml.vm) { Override public String outputFile(TableInfo tableInfo) { return null; // 不生成 XML } }); // 关键指定 SQL 模板 cfg.setFileOutConfigList(focList); // 6. 执行生成 new AutoGenerator() .setGlobalConfig(gc) .setDataSource(dsc) .setStrategy(strategy) .setPackageInfo(pc) .setCfg(cfg) .setTemplate(new TemplateConfig().setXml(null)) // 不生成 Mapper XML .execute(); } }运行后会在resources/sql下生成song.sql等文件。打开song.sql你会看到类似CREATE TABLE song ( id bigint NOT NULL COMMENT 主键ID, title varchar(255) DEFAULT NULL COMMENT 歌曲名, artist varchar(100) DEFAULT NULL COMMENT 歌手名, album varchar(100) DEFAULT NULL COMMENT 专辑, cover_url varchar(500) DEFAULT NULL COMMENT 封面图URL, audio_url varchar(500) DEFAULT NULL COMMENT 音频路径, duration int DEFAULT NULL COMMENT 时长秒, category varchar(50) DEFAULT NULL COMMENT 分类, create_time datetime(3) NOT NULL COMMENT 创建时间, update_time datetime(3) NOT NULL COMMENT 更新时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT歌曲表;注意datetime(3)和utf8mb4字符集——前者保证毫秒精度后者支持 emoji 和生僻字比如歌手名“龘”。如果生成的 SQL 里还是datetime说明你的 MySQL JDBC 驱动版本太低 8.0.22必须升级mysql-connector-java到 8.0.33。3. 文件上传与音频处理用 Apache Commons FileUpload JAudioTagger 解析 MP3 元数据3.1 为什么不能直接用 Spring Boot 的MultipartFile存原始文件MultipartFile是 Spring 封装的上传接口它方便但只解决“把二进制流从 HTTP 请求里捞出来”这一步。如果你直接file.transferTo(new File(/opt/music/audio/1001.mp3))会立刻面临三个问题路径安全用户上传../../../etc/passwd这种恶意文件名可能覆盖系统文件重复覆盖同一首歌多次上传旧文件被覆盖但数据库里audioUrl还指向旧路径播放 404元数据缺失MP3 文件自带 ID3 标签标题、歌手、专辑、时长、封面但MultipartFile不解析你得自己读二进制提取。所以必须封装一层MusicFileService做三件事校验、重命名、解析。3.2 安全文件名生成用 UUID 原始扩展名杜绝路径遍历// src/main/java/com/music/service/MusicFileService.java Service public class MusicFileService { private static final SetString ALLOWED_EXTENSIONS Set.of(mp3, wav, flac); public String safeUpload(MultipartFile file, String uploadDir) throws IOException { // 1. 校验扩展名 String originalFilename file.getOriginalFilename(); if (originalFilename null || originalFilename.isEmpty()) { throw new IllegalArgumentException(文件名为空); } String extension FilenameUtils.getExtension(originalFilename).toLowerCase(); if (!ALLOWED_EXTENSIONS.contains(extension)) { throw new IllegalArgumentException(不支持的文件类型: extension); } // 2. 生成安全文件名UUID 原始扩展名 String safeFilename UUID.randomUUID().toString() . extension; String fullPath uploadDir File.separator safeFilename; // 3. 校验路径防止 ../ 遍历 File targetFile new File(fullPath); File baseDir new File(uploadDir); if (!targetFile.getCanonicalPath().startsWith(baseDir.getCanonicalPath())) { throw new SecurityException(非法文件路径); } // 4. 保存 file.transferTo(targetFile); return safeFilename; // 返回相对路径存入数据库 } }关键点FilenameUtils.getExtension()来自commons-io比originalFilename.substring(originalFilename.lastIndexOf(.))更健壮能处理file.tar.gzgetCanonicalPath()是防遍历的核心——它会解析../并返回真实物理路径再比对是否在uploadDir下返回safeFilename如a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8.mp3而不是绝对路径这样数据库字段audioUrl存的是/audio/a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8.mp3前端播放器直接拼接域名即可。3.3 解析 MP3 ID3 标签用 JAudioTagger 提取时长、封面、歌手等元数据JAudioTagger 是 Java 领域最成熟的音频元数据解析库支持 MP3、FLAC、WAV。添加依赖!-- pom.xml -- dependency groupIdorg.jaudiotagger/groupId artifactIdjaudiotagger/artifactId version2.2.5/version /dependency解析逻辑封装为工具方法// src/main/java/com/music/util/AudioTagUtil.java public class AudioTagUtil { public static AudioMetadata parseMp3Metadata(File mp3File) throws Exception { AudioFile f AudioFileIO.read(mp3File); Tag tag f.getTagOrCreateAndSetDefault(); AudioMetadata metadata new AudioMetadata(); metadata.setTitle(tag.getFirst(FieldKey.TITLE)); metadata.setArtist(tag.getFirst(FieldKey.ARTIST)); metadata.setAlbum(tag.getFirst(FieldKey.ALBUM)); metadata.setDuration((int) f.getAudioHeader().getTrackLength()); // 秒数 // 提取封面图 if (tag instanceof ID3v24Tag) { ID3v24Tag id3Tag (ID3v24Tag) tag; OptionalArtwork artworkOpt id3Tag.getArtworkList().stream() .filter(a - a.isImage()) .findFirst(); if (artworkOpt.isPresent()) { Artwork artwork artworkOpt.get(); byte[] imageData artwork.getBinaryData(); String coverBase64 Base64.getEncoder().encodeToString(imageData); metadata.setCoverBase64(coverBase64); } } return metadata; } public static class AudioMetadata { private String title; private String artist; private String album; private Integer duration; private String coverBase64; // Base64 编码的封面图供前端直接渲染 // getter/setter... } }调用时机在MusicFileService.safeUpload()之后立即解析// 上传后解析 String safeFilename musicFileService.safeUpload(file, uploadDir); File uploadedFile new File(uploadDir File.separator safeFilename); AudioTagUtil.AudioMetadata metadata AudioTagUtil.parseMp3Metadata(uploadedFile); // 构建 Song 实体 Song song new Song(); song.setTitle(metadata.getTitle()); song.setArtist(metadata.getArtist()); song.setAlbum(metadata.getAlbum()); song.setDuration(metadata.getDuration()); song.setCoverUrl(data:image/jpeg;base64, metadata.getCoverBase64()); // 前端 img src 直接用 song.setAudioUrl(/audio/ safeFilename); songMapper.insert(song);注意setCoverUrl(data:image/jpeg;base64,...)是临时方案仅用于快速验证。生产环境应将封面图单独保存为文件如/cover/xxx.jpg再存相对路径避免 Base64 字符串撑爆数据库字段。4. 播放器交互与状态同步用 WebSocket 实现实时播放进度与多端同步4.1 为什么 HTTP 轮询不适合播放进度同步想象一个场景用户 A 在电脑端播放歌曲进度条走到 2:30用户 B 在手机端同一账号登录希望看到相同进度。如果用前端每秒发一次GET /api/progress?songId1001服务器查数据库返回2:30问题在于延迟高轮询间隔 1s实际进度可能已到 2:31用户感知卡顿压力大1000 个在线用户每秒 1000 次请求数据库扛不住状态不一致A 暂停时B 还在轮询直到下次请求才知暂停中间 1s 播放不同步。WebSocket 是唯一解建立长连接后服务端主动推送进度变更毫秒级响应。4.2 Spring Boot 集成 WebSocket用MessageMapping处理播放指令首先启用 WebSocket 支持// src/main/java/com/music/config/WebSocketConfig.java Configuration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker(/topic); // 启用内存消息代理订阅 /topic/progress config.setApplicationDestinationPrefixes(/app); // 客户端发送消息到 /app/play } Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(/ws).withSockJS(); // 注册 WebSocket 端点 } }定义播放控制消息结构// src/main/java/com/music/dto/PlayCommand.java public class PlayCommand { private Long songId; // 歌曲 ID private String action; // play, pause, seek private Integer position; // seek 时的目标秒数 private String sessionId; // 当前浏览器会话 ID用于区分多端 }服务端接收并广播// src/main/java/com/music/controller/PlayController.java Controller public class PlayController { private final SimpMessagingTemplate messagingTemplate; private final MapString, Integer progressMap new ConcurrentHashMap(); // key: songId_sessionId, value: position public PlayController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate messagingTemplate; } MessageMapping(/play) SendTo(/topic/progress) public PlayProgress broadcastProgress(PlayCommand command) { String key command.getSongId() _ command.getSessionId(); if (play.equals(command.getAction()) || seek.equals(command.getAction())) { progressMap.put(key, command.getPosition() ! null ? command.getPosition() : 0); } else if (pause.equals(command.getAction())) { // 暂停时不更新进度保持最后位置 } // 构建广播消息 PlayProgress response new PlayProgress(); response.setSongId(command.getSongId()); response.setPosition(progressMap.getOrDefault(key, 0)); response.setAction(command.getAction()); return response; } }前端 JavaScript 连接并监听// static/js/player.js const socket new SockJS(/ws); const stompClient Stomp.over(socket); stompClient.connect({}, () { stompClient.subscribe(/topic/progress, (message) { const data JSON.parse(message.body); if (data.songId currentSongId) { updateProgressBar(data.position); // 更新进度条 if (data.action pause) { player.pause(); } } }); }); // 发送播放指令 function sendPlayCommand(action, position null) { stompClient.send(/app/play, {}, JSON.stringify({ songId: currentSongId, action: action, position: position, sessionId: getCookie(JSESSIONID) // 用 session ID 区分设备 })); }提示getCookie(JSESSIONID)是关键。Spring Boot 默认用JSESSIONID标识会话同一个账号在不同浏览器打开JSESSIONID不同progressMap就能隔离各端进度实现“电脑播到哪手机就同步到哪互不干扰”。4.3 播放进度持久化为什么不能只靠内存 MapConcurrentHashMap在单机部署时够用但一旦集群部署Nginx 负载均衡到多台机器progressMap就变成各自为政——A 用户连到 Server1进度存在 Server1 内存B 用户连到 Server2查不到 Server1 的进度。必须下沉到共享存储。最轻量方案用 Redis 的HASH结构存进度Key 为progress:${songId}Field 为sessionIdValue 为position// 替换原来的 ConcurrentHashMap Autowired private RedisTemplateString, Object redisTemplate; MessageMapping(/play) SendTo(/topic/progress) public PlayProgress broadcastProgress(PlayCommand command) { String hashKey progress: command.getSongId(); String field command.getSessionId(); if (play.equals(command.getAction()) || seek.equals(command.getAction())) { redisTemplate.opsForHash().put(hashKey, field, command.getPosition() ! null ? command.getPosition() : 0); } // 从 Redis 读取当前所有端的最新进度可选只广播给本端 Integer position (Integer) redisTemplate.opsForHash().get(hashKey, field); PlayProgress response new PlayProgress(); response.setSongId(command.getSongId()); response.setPosition(position ! null ? position : 0); response.setAction(command.getAction()); return response; }Redis 的原子性保证了多实例间进度一致性且HASH结构天然支持按songId聚合删除过期进度只需DEL progress:1001。5. 避坑指南上线前必须验证的 5 个血泪现场5.1 现象上传 MP3 后播放 404但文件明明存在原因Spring Boot 默认静态资源路径是classpath:/static/、classpath:/public/、file:/opt/static/而你把音频存到了/opt/music/audio/该路径不在spring.web.resources.static-locations配置里。解决在application.yml中追加自定义静态路径spring: web: resources: static-locations: classpath:/static/,file:/opt/music/audio/,file:/opt/music/cover/然后确保/opt/music/audio/目录权限为755且运行 Java 进程的用户如deploy有读取权限。用curl http://localhost:8080/audio/xxx.mp3测试直链可访问性。5.2 现象MyBatis-Plus 的lambdaQuery().eq(...).list()返回空但数据库里有数据原因实体类字段名与数据库列名不匹配且未加TableField显式映射。例如数据库列是song_name实体字段是songNameMyBatis-Plus 默认按songName→song_name转换但如果NamingStrategy被误设为no_change转换就失效。解决在StrategyConfig中明确设置strategy.setNaming(NamingStrategy.underline_to_camel)并在实体类字段上加TableField(song_name)强制指定。用 MyBatis-Plus 的Debug日志确认生成的 SQLlogging: level: com.music.mapper: debug查看控制台输出的 SQL 是否含WHERE song_name ?而非WHERE songName ?。5.3 现象WebSocket 连接成功但SendTo广播无响应前端收不到消息原因SendTo的路径/topic/progress与前端stompClient.subscribe(/topic/progress, ...)的路径必须完全一致包括斜杠。常见错误是前端写成/topic/progress/结尾多斜杠或/progress少/topic/。解决统一约定路径格式在WebSocketConfig中打印日志确认代理启用Override public void configureMessageBroker(MessageBrokerRegistry config) { System.out.println(WebSocket broker enabled with prefix: /topic); config.enableSimpleBroker(/topic); config.setApplicationDestinationPrefixes(/app); }然后用浏览器开发者工具的 Network 标签过滤websocket查看 WS 连接建立后是否有MESSAGE帧发出。5.4 现象LocalDateTime字段插入 MySQL 后秒数丢失如2024-05-20 14:30:25.123变成2024-05-20 14:30:25原因MySQL 表字段定义为datetime而非datetime(3)或 JDBC URL 缺少serverTimezoneAsia/Shanghai导致时区转换错误。解决修改表结构ALTER TABLE song MODIFY COLUMN create_time datetime(3);确保 JDBC URL 包含serverTimezoneAsia/Shanghai在application.yml中加时区配置spring: jackson: time-zone: Asia/Shanghai date-format: yyyy-MM-dd HH:mm:ss.SSS5.5 现象用户登录后首页显示“欢迎null”用户名没取出来原因User实体类用了 Lombok 的Data但username字段在数据库里是user_nameMyBatis-Plus 默认按userName→user_name转换失败因为NamingStrategy未生效或字段未加TableField。解决在User类中为username字段加注解TableField(user_name) private String username;并确认StrategyConfig中setColumnNaming(NamingStrategy.underline_to_camel)已启用。用userMapper.selectOne(new QueryWrapperUser().eq(user_name, admin))测试单条查询是否能正确映射。6. 性能压测与上线 checklist用 JMeter 模拟千人并发把“能跑”变成“稳跑”6.1 用 JMeter 模拟真实用户行为链路一个完整音乐网站用户链路是登录 → 获取首页歌单 → 搜索歌曲 → 播放一首 → 收藏 → 查看我的收藏。不能只压单个接口要模拟这个闭环。JMeter 脚本结构如下线程组名称说明Thread GroupLogin100 个用户并发登录提取JSESSIONID作为后续 CookieTransaction ControllerHome Flow包含GET/api/home首页、GET/api/search?keyword周杰伦搜索、POST/api/play播放指令Transaction ControllerCollection Flow包含POST/api/collection/add收藏、GET/api/collection/list查收藏关键配置HTTP Cookie Manager自动管理JSESSIONID保证会话连续JSON Extractor从登录响应中提取token或userId用于后续请求头Constant Timer每个请求后加200ms延迟模拟真实用户操作节奏View Results Tree仅调试时开启正式压测关闭避免内存溢出。运行 1000 线程、循环 10 次观察90% Line 800ms表示 90% 请求在 800ms 内完成体验合格Error Rate 0.5%错误率过高说明数据库连接池或线程池瓶颈Active Threads Over Time图平稳无陡升陡降说明 GC 或线程阻塞正常。6.2 数据库连接池调优HikariCP 的 3 个生死参数Spring Boot 2.0 默认用 HikariCP但application.yml里只配url/username/password是不够的。必须显式调优spring: datasource: hikari: maximum-pool-size: 20 # 最大连接数设为 CPU 核数 * 48核机器设32但音乐站 IO 密集20 更稳 minimum-idle: 5 # 最小空闲连接避免冷启动慢 connection-timeout: 30000 # 获取连接超时30秒足够 idle-timeout: 600000 # 空闲连接存活时间10分钟 max-lifetime: 1800000 # 连接最大生命周期30分钟避免 MySQL wait_timeout 断连 validation-timeout: 3000 # 连接校验超时3秒 connection-test-query: SELECT 1 # 校验 SQLMySQL 用 SELECT 1压测时监控HikariPool-1 - Pool stats日志如果出现Timeout from waiting for connection说明maximum-pool-size太小如果ActiveConnections长期满说明 SQL 有慢查询需加索引。6.3 文件上传限流用RequestPartStandardServletMultipartResolver控制单次上传大小Spring Boot 默认允许上传 1MB 文件音乐文件动辄 5~10MB必须调大spring: servlet: multipart: max-file-size: 50MB max-request-size: 50MB但这只是 Spring 层限制Tomcat 本身也有maxSwallowSize默认 2MB超过会直接 400 错误。需在application.yml中配置嵌入式 Tomcatserver: tomcat: max-swallow-size: 52428800 # 50MB单位字节更进一步对/api/upload接口做令牌桶限流防恶意上传// src/main/java/com/music/config/RateLimitConfig.java Configuration public class RateLimitConfig { Bean public FilterRegistrationBeanRateLimitFilter rateLimitFilter() { FilterRegistrationBeanRateLimitFilter registrationBean new FilterRegistrationBean(); registrationBean.setFilter(new RateLimitFilter()); registrationBean.setUrlPatterns(List.of(/api/upload)); registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); return registrationBean; } } // 令牌桶实现简化版 public class RateLimitFilter implements Filter { private final RateLimiter limiter RateLimiter.create(5.0); // 每秒 5 次上传 Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) request; if (/api/upload.equals(httpRequest.getRequestURI()) !limiter.tryAcquire()) { HttpServletResponse httpResponse (HttpServletResponse) response; httpResponse.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS); httpResponse.getWriter().write(上传频率超限); return; } chain.doFilter(request, response); } }6.4 上线 checklist一份我贴在显示器边上的纸质清单每次上线前我都会对照这张纸逐项打钩十年没漏过序号检查项操作方式状态1数据库备份mysqldump -u root -p music_db backup_$(date %Y%m%d).sql☐2静态资源路径验证ls -l /opt/music/audio/确认目录存在且权限drwxr-xr-x☐3Redis 连接测试redis-cli -h 127本文还有配套的精品资源点击获取
返回列表