ARTICLE DETAIL

资讯详情

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

在线音乐网站系统实战:Java后端架构设计、缓存与性能优化

在线音乐网站系统实战:Java后端架构设计、缓存与性能优化 简介这套基于 Java/SSM 的在线音乐网站系统是一份面向 JavaWeb 学习者、毕业设计者及音乐类产品入门开发者的完整项目资源。它以 JSP、HTML5 和 MySQL 为核心实现类似 QQ 音乐、酷狗的音乐检索、在线播放、MV 观看、用户留言与评价等功能帮助读者解决从搭建页面到打通后台数据库的常见难点。压缩包共 937 个文件、约 32.94MB内含 76 个 JSP 页面、53 个 Java 源码与 53 个 class 文件配套 jar 依赖、JS/CSS/HTML 前端资源、图片素材以及 SQL 数据库脚本同时可见支付接口、用户注册、音乐信息管理等模块文件整体目录结构清晰便于按模块查阅。已有 44 人加入学习适合用于课程设计、毕业设计或 SSM 框架整合练习也可作为答辩演示与功能扩展的基础。借助这套源码可以直观理解真实音乐站点的分层开发方式掌握搜索、播放、评论、后台管理等核心实现并利用 SQL 脚本快速完成数据库初始化从而节省从设计到编码的大量时间。1. 在线音乐网站系统这个标题为什么值得认真做一遍“在线音乐网站系统”大概是 Java 后端岗位面试中出现频率最高的项目标题之一也是很多课设和简历里最常见的写法。它看起来不新不酷但正因为常见面试官反而容易往下追你用了哪些集合、日志怎么处理、Redis 缓存为什么失效、MySQL 那条搜索语句慢在哪。这套系统覆盖了 Java Web 开发的完整链路注册登录、歌单收藏、歌曲管理、上传下载、搜索、排行榜。没有高并发流量但该有的业务逻辑一个不少。这篇文章按我习惯的方式来讲这套系统先把选型和分层定下来再画清楚数据库结构和索引然后写核心接口的 Java 代码最后用压测和数据来验证参数是否合理。适合两类人一类要交课设或毕设能直接照着把项目跑起来另一类在准备 Java 面试需要一个能讲透的项目而不是只会背八股文。2. 技术选型和分层为什么 Spring Boot MyBatis Vue 是主流答案2.1 单体应用还是微服务很多对这套系统感兴趣的人会纠结用不用微服务。我的建议是不要上来就拆微服务。在线音乐网站系统的核心场景是歌曲管理、用户收藏、播放列表和搜索数据量在百万级以内单体应用完全能扛住而且开发效率高得多。Spring Boot 单体应用加一个 Redis 缓存热点数据再配合 MySQL 索引优化已经能支撑课堂项目和小型生产环境。如果你的简历上写着“基于微服务架构的音乐平台”面试官一定会问服务怎么拆分、事务怎么处理、注册中心用什么、服务间调用失败怎么兜底。这些问题一旦深挖很容易暴露项目是拼凑的。所以判断标准很简单你能把单体应用的缓存、索引、事务讲清楚再去谈微服务才有意义。微服务不是银弹在标题是“基于 Java 的在线音乐网站系统”的项目里它通常只是徒增复杂度。2.2 经典三层架构Controller、Service、Mapper无论用什么框架落地时我一般会按 Controller、Service、Mapper 三层来组织代码各层之间用 DTO 传递数据。这个结构的核心原则是实体类Entity不直接暴露给前端Controller 层不写业务逻辑Mapper 层只负责 SQL。// 典型的包结构 com.example.music ├── controller // 接收 HTTP 请求参数校验 ├── service // 业务逻辑事务边界 ├── mapper // MyBatis 接口和 XML 文件对应 ├── entity // 数据库实体和表字段一一对应 ├── dto // 视图对象只包含前端需要的字段 └── config // 拦截器、Redis 配置、跨域配置这里最容易被忽略的就是为什么需要 DTO 层。如果直接把 User 实体返回前端意味着你把密码字段也交出去了。常见做法是用一个 UserVO只保留用户 ID、昵称、头像地址。这不是多写几行代码的问题而是数据安全的底线——很多在线音乐系统在面试时被追问出“密码明文存储”或“用户信息直接返回”的问题往往就是因为省了这一步。2.3 依赖清单和 JDK 版本选择Spring Boot 3.x 要求 JDK 17 起步如果还用 JDK 8就要选 Spring Boot 2.7.x。对在线音乐网站系统这个规模我建议直接 JDK 17 Spring Boot 2.7 或 3.x 都行取决于你本机环境。以下是 maven 核心依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency关于环境变量配置网上搜“java环境变量配置详细教程”能找到很多但最容易踩的坑有两个一是安装 JDK 后忘了配JAVA_HOME二是编辑环境变量时用了中文符号或末尾多了分号。命令行输入java -version能正确输出就说明环境没问题。补充一个细节Spring Boot 3.x 里spring-boot-starter-web内置的 Tomcat 版本对 JDK 版本有要求JDK 8 环境下强行用 Spring Boot 3.x 会直接启动失败。这是新的项目常见报错和传统课程环境默认 JDK 8 有冲突方向比较绕。3. 在线音乐网站系统的数据库设计ER 图、建表 SQL 和搜索索引3.1 核心实体关系在线音乐系统的数据模型最核心的是 5 张表用户表、歌手表、专辑表、歌曲表、歌单表以及一张歌单-歌曲关联表。这和很多课设里的做法一致但要注意两个容易漏的点歌曲和歌手是多对一还是多对多以及歌单和歌曲是多对多需要中间表。建议先用 draw.io 或 MySQL Workbench 画 ER 图再建表。ER 图在面试中也会被问到比如“你的播放列表是怎么设计的”“删除歌手时歌曲怎么办”。实际项目中常见做法是加一个逻辑删除字段deleted避免物理删除导致用户收藏的歌单里出现空内容。3.2 建表 SQL 和关键字段约束以下是最小可运行的表结构其中 tenant_id 这类字段不需要这是单租户项目CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(64) NOT NULL COMMENT 登录名, password varchar(128) NOT NULL COMMENT BCrypt 加密后的密码, nickname varchar(64) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, created_at datetime DEFAULT CURRENT_TIMESTAMP, deleted tinyint DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE song ( id bigint NOT NULL AUTO_INCREMENT, name varchar(128) NOT NULL, singer_id bigint NOT NULL, album_id bigint DEFAULT NULL, duration int DEFAULT NULL COMMENT 时长单位秒, url varchar(255) DEFAULT NULL COMMENT 音频文件地址, play_count bigint DEFAULT 0, deleted tinyint DEFAULT 0, PRIMARY KEY (id), KEY idx_singer (singer_id), KEY idx_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 有两个细节值得说明。第一密码字段用 varchar(128) 而不是 varchar(32)因为推荐用 BCrypt 加密加密后的字符串超过 60 位给 32 位空间就存不下了。第二歌曲表的name字段加了普通索引而不是全文索引因为在线音乐系统的搜索场景大多数是“前缀匹配”idx_name配合后端的LIKE keyword%查询已经够用。如果你的歌曲数量到了千万级再去考虑 Elasticsearch 或全文索引。3.3 MySQL 的搜索语句从模糊匹配到索引优化在线音乐网站系统搜索歌曲最直接的 SQL 是SELECT * FROM song WHERE name LIKE %关键词%。这段代码在数据量小的时候没问题一旦歌曲表超过十万行%关键词%会导致全表扫描。这里有一个常见的面试题点LIKE 关键词%能走索引LIKE %关键词不走索引LIKE %关键词%也不走。所以常见做法是搜索接口只做前缀匹配或者引入 Redis 的 ZSET 做热门搜索词缓存。优化后的搜索实现是先用 Redis 缓存搜索结果如果缓存没有再用慢查询兜底。这个思路几乎适用于所有 Java 后端项目不只音乐网站。// 搜索逻辑先查缓存再查数据库 public ListSongVO searchSongs(String keyword, int page, int size) { String cacheKey search: keyword : page : size; // 先从 Redis 拿 Object cached redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return (ListSongVO) cached; } // 数据库兜底 PageHelper.startPage(page, size); ListSong songs songMapper.searchByName(keyword); // 转 VO 并缓存 ListSongVO result convertToVO(songs); redisTemplate.opsForValue().set(cacheKey, result, 5, TimeUnit.MINUTES); return result; }这段代码里 PageHelper 是分页插件startPage之后的第一个查询会自动拼接 LIMIT。缓存时间设 5 分钟是折中方案歌曲数据的更新频率不高5 分钟内搜索结果基本不变又能避免缓存占用过大。3.4 缓存设计里最容易踩的坑Redis 缓存在线音乐系统中主要缓存两类数据歌曲播放量和热门歌单。播放量的更新是一个典型的高频写场景直接用UPDATE song SET play_count play_count 1每一次播放都打数据库是不行的。常见做法是用 Redis 的increment()方法累加然后定期批量同步到 MySQL。这里有一个高频报错使用redisTemplate.opsForValue().increment()时抛异常提示不是 integer 或 out of range。原因通常是你在同一个 key 上先执行了set(key, abc)然后执行increment(key, 1)Redis 里值是字符串而你要做自增。注意点是increment()的 key 必须不复用存储 JSON 字符串的 key。生产环境建议 key 分开比如播放量用song:play:10001歌曲详情缓存用song:detail:10001不要互相覆盖。另外如果 value 超过了 Redis 的 long 范围也会出现 range 报错这在热点歌曲的播放量上比较容易踩——一个千万级播放量的歌曲没问题但要防止 Java 中误把字符串拼进去了。4. 核心接口实现登录鉴权、歌单列表和播放量统计4.1 登录和鉴权JWT 无状态方案在线音乐系统的登录我一般推荐 JWT 方案而不是传统的 Session。原因是未来你想拆微服务或前后端分离JWT 天然不需要共享会话状态。在单体应用里用 JWT 也方便前端存 token后端不用管存储会话省了 Redis 的 session 序列化问题。PostMapping(/api/auth/login) public ResultString login(RequestBody LoginRequest req) { // 1. 查出用户 User user userMapper.findByUsername(req.getUsername()); if (user null || !BCrypt.checkpw(req.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } // 2. 签发 JWT token String token Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim(nickname, user.getNickname()) .setExpiration(new Date(System.currentTimeMillis() 86400000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); return Result.ok(token); }这段代码有三个点值得展开。第一密码校验用了 BCrypt 的checkpw而不是直接比对字符串因为数据库存储的是加密后的密文不是明文。第二JWT 的过期时间设为 24 小时86400000 毫秒对于音乐网站这个场景比较合适用户长期保持登录但每天需要重新获取一次 token。第三signWith的密钥secretKey不能写死在前端要放在后端配置文件里并且用环境变量注入否则别人可以伪造任意用户的 token。登录之后的鉴权用拦截器实现在 Controller 方法上打RequireLogin注解做标记拦截器里从请求头取Authorization字段解析 token 并校验过期时间。这个方案在答辩或面试时会问“JWT 被偷了怎么办”可以回答把 token 的有效期缩短到 2 小时配合刷新 token 接口或者用 Redis 保存 token 白名单。4.2 歌单列表接口分页参数和 SQL 写法查询歌单列表是音乐网站的高频接口前端首页、搜索页、用户主页都会调。这里要特别注意的是不要把整张表一次性查出来返回给前端。接口设计里分页参数必须给默认值防止有人直接查全表。GetMapping(/api/playlists) public ResultPageResultPlaylistVO listPlaylists( RequestParam(defaultValue 1) int page, RequestParam(defaultValue 12) int size) { // 参数合法性校验 if (page 1 || size 50) { return Result.error(分页参数不合法); } PageHelper.startPage(page, size); ListPlaylist playlists playlistMapper.selectAll(); // 转 VO隐藏数据库敏感字段 ListPlaylistVO voList playlists.stream() .map(p - new PlaylistVO(p.getId(), p.getName(), p.getCoverUrl(), p.getSongCount())) .collect(Collectors.toList()); return Result.ok(new PageResult(voList, page, size)); }上面代码里用了 lambda 表达式这是 java 中最常见的集合转 VO 写法比 for 循环简洁。InputStream 或循环的写法可以跑但在代码评审时会被说写法太老。使用Collectors.toList()时注意Java 8 之后这个操作返回的是 ArrayList如果你需要不可变集合要用toUnmodifiableList()这是 Java 10 才有的 API。分页参数表格如下参数默认值范围说明page11~N页码从 1 开始size121~50每页条数超过 50 强制为 50sortlatestlatest / hot按时间或播放量排序size 的上限设成 50 的原因很简单前端展示歌单列表最多几十个卡片一次性返回全部数据除了拖慢响应时间没有实际意义。真正在面试中能讲出来的设计是 hot 排序的实现——它不可能是全表ORDER BY play_count DESC应该是 Redis ZSET 按播放量排行而不是每次都去数据库 count 统计。4.3 播放量统计的批量同步机制高频播放量的写入场景最好用一个独立的队列或定时任务来异步处理不影响主流程响应。我常用 Redis 做计数器然后每分钟同步一次到 MySQL达到削峰目的。以下是一种最简单的同步实现// 播放时只消费 Redis 计数 public void playSong(Long songId) { String key song:play: songId; redisTemplate.opsForValue().increment(key, 1); } // 定时任务每分钟把 Redis 的增量同步回 MySQL Scheduled(fixedRate 60000) public void syncPlayCount() { // 1. 从 Redis 里找出需要同步的 key SetString keys redisTemplate.keys(song:play:*); // 2. 遍历累加更新数据库 for (String key : keys) { Long songId Long.parseLong(key.replace(song:play:, )); Long delta redisTemplate.opsForValue().increment(key, 0); songMapper.increasePlayCount(songId, delta); // 3. 重置 Redis开始下一轮计数 redisTemplate.delete(key); } }这里有个多线程边界问题如果定时任务运行在高并发环境下redisTemplate.delete(key)和increment(key, 0)之间存在并发丢失的可能。一个比较稳的替代方案是用 Lua 脚本原子地取值并重置或者把同步周期收短到 10 秒。在业务层面每分钟同步一次允许最多丢几秒的播放量对在线音乐系统是可以接受的。但如果被面试官追问一致性要答得上这个方案的取舍牺牲强一致换取数据库压力下降一个量级。4.4 接口响应中的集合序列化问题在写 Controller 返回数据时经常会遇到集合类型导致的 JSON 序列化异常比如LocalDateTime序列化失败。常见的错误提示是java.time.format.DateTimeParseException或者 Jackson 报InvalidDefinitionException。这不是框架 bug而是序列化器没有配置 Java 8 时间类型。解决办法是在application.yml里配置时间格式或者在 DTO 字段上打JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解。这里明确一点数据库里的created_at放在 DTO 里大概率要转成字符串返回给前端TypeHandler 可以处理但更轻的做法是让 SQL 直接格式化成字符串。spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8如果你用的是 MyBatisSQL 里还可以用DATE_FORMAT(created_at, %Y-%m-%d %H:%i:%s)直接返回字符串既省去在 Java 里做格式转换也避免 LocalDateTime 序列化的问题。缺点是 VO 字段类型要定义成 String稍微有点脏。5. 上线前验证和调优压力测试、连接池参数和扩展方向5.1 用 JMeter 做最小冒烟测试在线音乐网站系统上线前至少要对三个接口做压力测试登录接口、歌曲列表接口、搜索接口。常见做法是启动 JMeter用命令行跑一段 TPS 测试脚本。jmeter -n -t music-test.jmx -l result.jtl -e -o report/参数说明-n非 GUI 模式适合在服务器上直接跑-t指定 JMX 脚本文件-l保存原始结果日志-e生成 HTML 报告-o报告输出目录跑完之后重点看两个指标Throughput和Error %。如果一个接口吞吐量低于 500/s 且错误率高于 1%基本说明 SQL 或连接池有瓶颈。这时候不需要急着加机器先回来看慢查询日志看EXPLAIN是否走了索引再看连接池参数是否合理。5.2 三个必调参数在线音乐系统中数据库连接池、线程池和 Redis 连接几个参数几乎每次都要调默认值在真实请求下通常会成为瓶颈。参数默认值建议值理由Druid 最大连接数850~100默认连接数过低并发一上来就会排队Tomcat max-threads200400在线音乐接口有 IO 等待线程数可以大一点Redis 缓存过期无根据数据类型 5~30 分钟长时间不过期容易脏读太短则失去意义数据库连接池建议用 Druid 而不是 HikariCP 吗这取决于团队熟悉度。Spring Boot 默认 HikariCP 性能更好Druid 多了一个监控页面方便看慢 SQL。这里不用纠结两者都行关键是把它单独列在配置里不要用默认值直接上生产。5.3 压测脚本不关注的一个坑登录态压测搜歌接口如果不在脚本里先做登录返回的可能是 401测出来的 TPS 是鉴权失败的 TPS没有参考意义。JMeter 里处理不好登录态的常见问题是一开始 token 没拿到导致后续请求全部 401。解决办法是加一个 HTTP Header Manager把登录接口返回的 token 通过正则表达式提取器抽出来再用${token}填充到后续请求头。这类细节在真实压测中很容易被忽略但一旦明确了整套流程就顺畅了。5.4 后续演进方向这套在线音乐网站系统跑通之后如果要做增量扩展方向有三个歌曲文件接入云存储而不是存本地磁盘搜歌接口从 MySQL 换到 Elasticsearch以及把播放统计做成消息队列异步处理。这三个方向分别解决存储成本、检索效率和写入削峰的问题。但要在心里有一个衡量标准单机 MySQL 在百万级数据且索引合理时可以支撑到几千 QPS对在线音乐网站这个规模没必要提前引入消息队列和搜索引擎。等数据量真正上来再逐个替换替换时对外暴露的接口保持不动对调用方几乎无感知。本文还有配套的精品资源点击获取
返回列表