feat(music): 实现热门音乐功能并优化性能
- 新增 HotMusicController 提供热门音乐接口 - 实现基于新热度算法的热门音乐列表获取 - 添加手动刷新缓存接口 - 新增 MusicService 接口和 MusicServiceImpl 实现类 - 使用 Redis 缓存热门音乐列表,提高响应速度 - 引入 HotScoreUtil 工具类计算热度分数 - 在 Music 实体中添加点赞数、收藏数、评论数和热度分数字段 - 更新数据库结构,增加相关字段 - 使用异步任务定期更新热门音乐缓存
This commit is contained in:
@@ -2,6 +2,7 @@ package com.test.musichouduan;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
@@ -11,6 +12,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
@SpringBootApplication
|
||||
@EnableTransactionManagement
|
||||
@EnableScheduling
|
||||
@EnableAsync
|
||||
public class MusicHouduanApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.test.musichouduan.controller;
|
||||
|
||||
import com.test.musichouduan.dto.MusicDTO;
|
||||
import com.test.musichouduan.dto.Result;
|
||||
import com.test.musichouduan.service.MusicService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 热门音乐控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/hot-music")
|
||||
public class HotMusicController {
|
||||
|
||||
@Autowired
|
||||
private MusicService musicService;
|
||||
|
||||
/**
|
||||
* 获取基于新热度算法的热门音乐列表
|
||||
*
|
||||
* @param limit 限制数量
|
||||
* @return 响应结果
|
||||
*/
|
||||
/**
|
||||
* 获取基于新热度算法的热门音乐列表
|
||||
*
|
||||
* @param limit 限制数量
|
||||
* @return 响应结果
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Result<List<MusicDTO>> getHotMusic(@RequestParam(defaultValue = "10") Integer limit) {
|
||||
// 调用 service 层的新方法,该方法会优先从 Redis 缓存获取
|
||||
List<MusicDTO> musicList = musicService.getHotMusic(limit);
|
||||
return Result.success(musicList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发一次热门音乐缓存的更新。
|
||||
* 主要用于测试或紧急情况。
|
||||
* @return 响应结果
|
||||
*/
|
||||
@GetMapping("/refresh-cache")
|
||||
public Result<String> refreshHotMusicCache() {
|
||||
// 异步执行,立即返回,不阻塞当前请求
|
||||
musicService.updateHotMusicCache();
|
||||
return Result.success("热门音乐缓存更新任务已触发,请稍后查看结果。");
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,30 @@ public class Music {
|
||||
@Column
|
||||
private Long playCount;
|
||||
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
@Column
|
||||
private Integer likeCount;
|
||||
|
||||
/**
|
||||
* 收藏数
|
||||
*/
|
||||
@Column
|
||||
private Integer collectCount;
|
||||
|
||||
/**
|
||||
* 评论数
|
||||
*/
|
||||
@Column
|
||||
private Integer commentCount;
|
||||
|
||||
/**
|
||||
* 热度分数
|
||||
*/
|
||||
@Column
|
||||
private Double hotScore;
|
||||
|
||||
/**
|
||||
* 歌手列表
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.test.musichouduan.entity.Music;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* 音乐服务接口
|
||||
@@ -121,4 +122,15 @@ public interface MusicService {
|
||||
* @return 音乐实体
|
||||
*/
|
||||
Music getById(Long id);
|
||||
|
||||
/**
|
||||
* 定时任务接口,用于更新热门音乐缓存
|
||||
*/
|
||||
void scheduleUpdateHotMusic();
|
||||
|
||||
/**
|
||||
* 异步更新热门音乐缓存的接口
|
||||
* @return CompletableFuture 包装的音乐DTO列表
|
||||
*/
|
||||
CompletableFuture<List<MusicDTO>> updateHotMusicCache();
|
||||
}
|
||||
|
||||
@@ -9,21 +9,29 @@ import com.test.musichouduan.entity.Singer;
|
||||
import com.test.musichouduan.entity.User;
|
||||
import com.test.musichouduan.entity.UserMusic;
|
||||
import com.test.musichouduan.exception.BusinessException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.test.musichouduan.repository.*;
|
||||
import com.test.musichouduan.service.FileService;
|
||||
import com.test.musichouduan.service.MusicService;
|
||||
import com.test.musichouduan.util.HotScoreUtil;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.*;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import java.util.ArrayList;
|
||||
@@ -38,9 +46,22 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class MusicServiceImpl implements MusicService {
|
||||
|
||||
// Redis 缓存键
|
||||
private static final String HOT_MUSIC_CACHE_KEY = "music:hot_list";
|
||||
// Redis 缓存过期时间(小时)
|
||||
private static final long CACHE_EXPIRATION_HOURS = 2;
|
||||
// 热门音乐列表缓存数量
|
||||
private static final int HOT_MUSIC_LIMIT = 50;
|
||||
|
||||
@Autowired
|
||||
private MusicRepository musicRepository;
|
||||
|
||||
@Autowired
|
||||
private StringRedisTemplate redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private MusicTypeRepository musicTypeRepository;
|
||||
|
||||
@@ -146,14 +167,106 @@ public class MusicServiceImpl implements MusicService {
|
||||
|
||||
@Override
|
||||
public List<MusicDTO> getHotMusic(Integer limit) {
|
||||
// 查询热门音乐
|
||||
List<Music> musics = musicRepository.findMostPlayed(PageRequest.of(0, limit));
|
||||
// 1. 尝试从 Redis 缓存中获取热门音乐列表
|
||||
String hotMusicJson = redisTemplate.opsForValue().get(HOT_MUSIC_CACHE_KEY);
|
||||
|
||||
// 转换为DTO
|
||||
if (StringUtils.hasText(hotMusicJson)) {
|
||||
try {
|
||||
// 如果缓存命中,则反序列化并返回
|
||||
List<MusicDTO> cachedList = objectMapper.readValue(hotMusicJson, new TypeReference<List<MusicDTO>>() {});
|
||||
// 根据请求的 limit 参数返回相应数量的音乐
|
||||
return cachedList.stream().limit(limit).collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
// 日志记录反序列化失败
|
||||
System.err.println("Failed to deserialize hot music list from Redis: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果缓存未命中或反序列化失败,则立即计算并阻塞等待结果
|
||||
// 这是一个同步调用,确保首次请求能拿到数据
|
||||
List<MusicDTO> hotList = calculateAndCacheHotMusicSync();
|
||||
return hotList.stream().limit(limit).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步计算并缓存热门音乐列表。
|
||||
* 用于在缓存失效时,立即为用户提供数据。
|
||||
* @return 计算后的热门音乐列表
|
||||
*/
|
||||
private List<MusicDTO> calculateAndCacheHotMusicSync() {
|
||||
try {
|
||||
// 调用异步方法并等待其完成
|
||||
return updateHotMusicCache().get();
|
||||
} catch (Exception e) {
|
||||
// 在异常情况下,返回一个降级方案:例如按播放量排序的列表
|
||||
System.err.println("Failed to calculate hot music list synchronously: " + e.getMessage());
|
||||
List<Music> musics = musicRepository.findMostPlayed(PageRequest.of(0, HOT_MUSIC_LIMIT));
|
||||
return musics.stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时任务,定期更新热门音乐缓存。
|
||||
* cron表达式表示每小时的0分执行 (e.g., 1:00, 2:00, 3:00)
|
||||
*/
|
||||
@Scheduled(cron = "0 0 * * * ?")
|
||||
@Override
|
||||
public void scheduleUpdateHotMusic() {
|
||||
System.out.println("Scheduled task started: Updating hot music cache...");
|
||||
updateHotMusicCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步计算所有音乐的热度,并将热度最高的前50首缓存到Redis。
|
||||
* 使用 @Async 注解使其在独立的线程中执行。
|
||||
* @return 返回一个 CompletableFuture,包含计算后的热门音乐列表
|
||||
*/
|
||||
@Async
|
||||
@Transactional(readOnly = true) // 使用只读事务,提高查询性能
|
||||
@Override
|
||||
public CompletableFuture<List<MusicDTO>> updateHotMusicCache() {
|
||||
// 为了处理大数据量,我们使用分页查询来代替一次性加载所有数据
|
||||
List<Music> allMusic = new ArrayList<>();
|
||||
Page<Music> musicPage;
|
||||
int pageNum = 0;
|
||||
int pageSize = 500; // 每次查询500条
|
||||
|
||||
do {
|
||||
Pageable pageable = PageRequest.of(pageNum, pageSize);
|
||||
musicPage = musicRepository.findAll(pageable);
|
||||
allMusic.addAll(musicPage.getContent());
|
||||
pageNum++;
|
||||
} while (musicPage.hasNext());
|
||||
|
||||
// 使用并行流来加速计算过程
|
||||
allMusic.parallelStream().forEach(music -> {
|
||||
double score = HotScoreUtil.calculateHotScore(music);
|
||||
music.setHotScore(score);
|
||||
});
|
||||
|
||||
// 对所有音乐按计算出的热度分进行降序排序
|
||||
List<MusicDTO> sortedHotMusic = allMusic.stream()
|
||||
.sorted(Comparator.comparing(Music::getHotScore).reversed())
|
||||
.limit(HOT_MUSIC_LIMIT) // 取热度最高的前50
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
try {
|
||||
// 将排序后的列表序列化为JSON字符串
|
||||
String jsonToCache = objectMapper.writeValueAsString(sortedHotMusic);
|
||||
// 存入Redis,并设置过期时间
|
||||
redisTemplate.opsForValue().set(HOT_MUSIC_CACHE_KEY, jsonToCache, CACHE_EXPIRATION_HOURS, TimeUnit.HOURS);
|
||||
System.out.println("Hot music cache updated successfully with " + sortedHotMusic.size() + " items.");
|
||||
} catch (Exception e) {
|
||||
// 日志记录序列化或Redis操作失败
|
||||
System.err.println("Failed to cache hot music list to Redis: " + e.getMessage());
|
||||
}
|
||||
|
||||
// 返回一个已完成的 CompletableFuture
|
||||
return CompletableFuture.completedFuture(sortedHotMusic);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.test.musichouduan.util;
|
||||
|
||||
import com.test.musichouduan.entity.Music;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 热度分数计算工具类
|
||||
*/
|
||||
public class HotScoreUtil {
|
||||
|
||||
// 定义各种交互的权重
|
||||
private static final double LIKE_WEIGHT = 1.0;
|
||||
private static final double COLLECT_WEIGHT = 2.0;
|
||||
private static final double COMMENT_WEIGHT = 1.5;
|
||||
|
||||
// 时间衰减因子中的指数,可以调整以改变时间衰减的速度
|
||||
private static final double TIME_DECAY_EXPONENT = 1.8;
|
||||
|
||||
/**
|
||||
* 计算单首音乐的热度分数
|
||||
* 采用类似 Hacker News 的热度算法: 热度 = (交互权重和) / (时间衰减因子)
|
||||
*
|
||||
* @param music 音乐实体对象
|
||||
* @return 计算出的热度分数
|
||||
*/
|
||||
public static double calculateHotScore(Music music) {
|
||||
// 1. 计算交互权重和
|
||||
// 如果交互数据为 null,则默认为 0
|
||||
long likeCount = music.getLikeCount() != null ? music.getLikeCount() : 0;
|
||||
long collectCount = music.getCollectCount() != null ? music.getCollectCount() : 0;
|
||||
long commentCount = music.getCommentCount() != null ? music.getCommentCount() : 0;
|
||||
|
||||
double interactionScore = (likeCount * LIKE_WEIGHT) +
|
||||
(collectCount * COLLECT_WEIGHT) +
|
||||
(commentCount * COMMENT_WEIGHT);
|
||||
|
||||
// 2. 计算时间衰减因子
|
||||
// (当前时间 - 创建时间)的小时数
|
||||
LocalDateTime createTime = music.getCreateTime();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long hoursElapsed = Duration.between(createTime, now).toHours();
|
||||
|
||||
// 为了避免新发布内容因分母过小(接近0)而导致分数无限大的问题,
|
||||
// 我们给分母加上一个常数(这里是2),确保其初始值不会太小。
|
||||
double timeDecayFactor = Math.pow(hoursElapsed + 2, TIME_DECAY_EXPONENT);
|
||||
|
||||
// 3. 计算最终热度分数
|
||||
return interactionScore / timeDecayFactor;
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,18 @@ CREATE TABLE `music` (
|
||||
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '音乐文件URL',
|
||||
`lyric` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '歌词',
|
||||
`duration` int NULL DEFAULT 0 COMMENT '时长(秒)',
|
||||
`album` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
|
||||
-- Interaction and Hot Score Fields
|
||||
`play_count` bigint NULL DEFAULT 0 COMMENT '播放次数',
|
||||
`like_count` int NULL DEFAULT 0 COMMENT '点赞数',
|
||||
`collect_count` int NULL DEFAULT 0 COMMENT '收藏数',
|
||||
`comment_count` int NULL DEFAULT 0 COMMENT '评论数',
|
||||
`hot_score` double NULL DEFAULT 0.0 COMMENT '热度分数',
|
||||
|
||||
-- Timestamps
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`album` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '音乐表' ROW_FORMAT = Dynamic;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user