Java全栈开发 - 视频网站实战教程
项目介绍
本教程将带领大家从零开始搭建一个视频网站,涵盖前后端全栈开发技术。我们将使用以下技术栈:
- 后端: Spring Boot + MyBatis + MySQL
- 前端: Vue.js + Element UI
- 中间件: Redis + RabbitMQ
- 视频存储: 阿里云OSS
功能模块
1. 用户模块
- 注册登录
- 个人中心
- 会员体系
2. 视频模块
- 视频上传
- 视频播放
- 弹幕系统
- 点赞收藏
3. 评论模块
- 发表评论
- 回复功能
- 评论管理
4. 搜索模块
- 视频搜索
- 热门推荐
- 标签分类
技术要点
后端架构
@SpringBootApplication
public class VideoApplication {
public static void main(String[] args) {
SpringApplication.run(VideoApplication.class, args);
}
}
数据库设计
CREATE TABLE `video` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL,
`description` text,
`url` varchar(255) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
视频上传流程
- 前端分片上传
- 后端合并视频
- 转码处理
- 存储到OSS
项目亮点
- 采用微服务架构,提高系统可扩展性
- 使用Redis缓存热点数据,提升访问速度
- 通过RabbitMQ实现异步处理,提高系统吞吐量
- 实现弹幕系统,提升用户体验
性能优化
- 使用Redis缓存
- 数据库索引优化
- 使用CDN加速
- 前端性能优化
项目部署
- 使用Docker容器化部署
- Jenkins自动化部署
- Nginx反向代理
- 服务器集群
总结
通过本项目实战,我们将掌握:
- Spring Boot项目开发全流程
- 主流技术栈的整合使用
- 分布式系统架构设计
- 性能优化最佳实践
后续我们将逐步完善各个功能模块的具体实现,敬请期待!
参考资料
- Spring Boot官方文档
- Vue.js中文文档
- 阿里云OSS开发指南
- MySQL优化指南
Java视频网站实战 - 视频与搜索模块详解
一、视频模块详细设计
1. 数据库设计
-- 视频表
CREATE TABLE `video` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(255) COMMENT '视频标题',
`description` text COMMENT '视频描述',
`cover_url` varchar(255) COMMENT '封面URL',
`video_url` varchar(255) COMMENT '视频URL',
`category_id` bigint COMMENT '分类ID',
`user_id` bigint COMMENT '上传用户ID',
`status` tinyint COMMENT '状态:0-处理中,1-正常,2-下架',
`view_count` bigint DEFAULT 0 COMMENT '播放量',
`like_count` int DEFAULT 0 COMMENT '点赞数',
`create_time` datetime COMMENT '创建时间',
`update_time` datetime COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_category` (`category_id`),
INDEX `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 弹幕表
CREATE TABLE `danmaku` (
`id` bigint NOT NULL AUTO_INCREMENT,
`video_id` bigint COMMENT '视频ID',
`user_id` bigint COMMENT '用户ID',
`content` varchar(255) COMMENT '弹幕内容',
`time_point` float COMMENT '视频时间点',
`create_time` datetime COMMENT '发送时间',
PRIMARY KEY (`id`),
INDEX `idx_video_time` (`video_id`, `time_point`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 核心接口设计
@RestController
@RequestMapping("/api/video")
public class VideoController {
@PostMapping("/upload")
public Result uploadVideo(@RequestParam MultipartFile file) {
// 1. 生成文件名
// 2. 分片上传到OSS
// 3. 创建视频记录
// 4. 发送转码消息
}
@GetMapping("/play/{id}")
public Result getPlayInfo(@PathVariable Long id) {
// 1. 获取视频信息
// 2. 增加播放量
// 3. 返回播放地址
}
@PostMapping("/danmaku/send")
public Result sendDanmaku(@RequestBody DanmakuDTO dto) {
// 1. 保存弹幕
// 2. 推送到WebSocket
}
}
3. 视频转码服务
@Service
public class VideoTranscodeService {
@RabbitListener(queues = "video.transcode")
public void handleTranscode(VideoMessage message) {
// 1. 下载原始视频
// 2. FFmpeg转码处理
// 3. 上传转码后视频
// 4. 更新视频状态
}
private void transcodeToM3U8(File source, File target) {
// 使用FFmpeg将视频转换为HLS格式
String command = String.format("ffmpeg -i %s -codec:v libx264 -codec:a aac -f hls %s",
source.getPath(), target.getPath());
// 执行转码命令
}
}
4. 弹幕系统实现
@ServerEndpoint("/websocket/danmaku/{videoId}")
public class DanmakuWebSocket {
@OnMessage
public void onMessage(String message, Session session) {
// 1. 解析弹幕消息
// 2. 广播给所有观看者
// 3. 异步保存到数据库
}
}
二、搜索模块详细设计
1. Elasticsearch索引设计
{
"mappings": {
"properties": {
"id": { "type": "long" },
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"description": {
"type": "text",
"analyzer": "ik_max_word"
},
"tags": { "type": "keyword" },
"categoryId": { "type": "long" },
"viewCount": { "type": "long" },
"createTime": { "type": "date" }
}
}
}
2. 搜索服务实现
@Service
public class SearchService {
@Autowired
private RestHighLevelClient esClient;
public PageResult<VideoDTO> search(SearchDTO params) {
// 构建搜索条件
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 关键词搜索
if (StringUtils.hasText(params.getKeyword())) {
boolQuery.must(QueryBuilders.multiMatchQuery(params.getKeyword(),
"title^3", "description")); // 标题权重更高
}
// 分类过滤
if (params.getCategoryId() != null) {
boolQuery.filter(QueryBuilders.termQuery("categoryId", params.getCategoryId()));
}
// 排序
SortBuilder<?> sortBuilder = params.getSort() == 1
? SortBuilders.fieldSort("viewCount").order(SortOrder.DESC)
: SortBuilders.fieldSort("createTime").order(SortOrder.DESC);
// 执行搜索
SearchRequest request = new SearchRequest("video")
.source(new SearchSourceBuilder()
.query(boolQuery)
.sort(sortBuilder)
.from(params.getPage() * params.getSize())
.size(params.getSize())
.highlighter(new HighlightBuilder()
.field("title")
.preTags("<em>")
.postTags("</em>")
));
return executeSearch(request);
}
}
3. 搜索数据同步
@Component
public class VideoSearchListener {
@RabbitListener(queues = "video.sync")
public void handleVideoSync(VideoMessage message) {
// 1. 构建索引文档
IndexRequest request = new IndexRequest("video")
.id(String.valueOf(message.getId()))
.source(JSON.toJSONString(message), XContentType.JSON);
// 2. 更新ES索引
try {
esClient.index(request, RequestOptions.DEFAULT);
} catch (IOException e) {
log.error("同步视频索引失败", e);
}
}
}
4. 热门推荐实现
@Service
public class RecommendService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public List<VideoDTO> getHotVideos() {
// 1. 从Redis获取热门视频ID列表
Set<String> ids = redisTemplate.opsForZSet()
.reverseRange("hot_videos", 0, 9);
// 2. 批量查询视频信息
return videoService.listByIds(ids);
}
// 定时更新热门视频
@Scheduled(cron = "0 0 */1 * * ?")
public void updateHotVideos() {
// 1. 统计最近一小时播放量
// 2. 更新Redis排行榜
}
}
三、关键代码实现
1. 视频上传与转码
// VideoController.java
@RestController
@RequestMapping("/api/video")
@Slf4j
public class VideoController {
@Autowired
private VideoService videoService;
@Autowired
private OSSService ossService;
@Autowired
private RabbitTemplate rabbitTemplate;
@PostMapping("/upload")
public Result uploadVideo(@RequestParam MultipartFile file, @RequestParam String title) {
try {
// 生成唯一文件名
String fileName = UUID.randomUUID().toString() +
file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
// 上传到OSS
String videoUrl = ossService.uploadFile(file.getInputStream(), fileName);
// 创建视频记录
Video video = new Video();
video.setTitle(title);
video.setVideoUrl(videoUrl);
video.setUserId(SecurityUtils.getCurrentUserId());
video.setStatus(VideoStatus.PROCESSING.getCode());
videoService.save(video);
// 发送转码消息
rabbitTemplate.convertAndSend("video.transcode", new VideoMessage(video.getId(), videoUrl));
return Result.success(video.getId());
} catch (Exception e) {
log.error("视频上传失败", e);
return Result.error("上传失败");
}
}
}
// VideoTranscodeService.java
@Service
@Slf4j
public class VideoTranscodeService {
@Autowired
private OSSService ossService;
@Autowired
private VideoService videoService;
@RabbitListener(queues = "video.transcode")
public void handleTranscode(VideoMessage message) {
try {
// 下载原始视频
File sourceFile = ossService.downloadFile(message.getVideoUrl());
// 创建转码目录
File targetDir = new File("/tmp/transcode/" + message.getVideoId());
if (!targetDir.exists()) {
targetDir.mkdirs();
}
// 转码处理
transcodeToM3U8(sourceFile, new File(targetDir, "index.m3u8"));
// 上传转码后的文件
String m3u8Url = ossService.uploadDir(targetDir, "videos/" + message.getVideoId());
// 更新视频状态
videoService.update()
.set("status", VideoStatus.NORMAL.getCode())
.set("play_url", m3u8Url)
.eq("id", message.getVideoId())
.update();
} catch (Exception e) {
log.error("视频转码失败", e);
// 更新失败状态
videoService.update()
.set("status", VideoStatus.FAILED.getCode())
.eq("id", message.getVideoId())
.update();
}
}
}
2. 弹幕系统
// DanmakuController.java
@RestController
@RequestMapping("/api/danmaku")
public class DanmakuController {
@Autowired
private DanmakuService danmakuService;
@PostMapping("/send")
public Result sendDanmaku(@RequestBody DanmakuDTO dto) {
Danmaku danmaku = new Danmaku();
danmaku.setVideoId(dto.getVideoId());
danmaku.setUserId(SecurityUtils.getCurrentUserId());
danmaku.setContent(dto.getContent());
danmaku.setTimePoint(dto.getTimePoint());
danmakuService.save(danmaku);
// 推送到WebSocket
DanmakuWebSocket.broadcast(dto.getVideoId(), danmaku);
return Result.success();
}
@GetMapping("/list/{videoId}")
public Result listDanmaku(@PathVariable Long videoId,
@RequestParam Float startTime,
@RequestParam Float endTime) {
return Result.success(danmakuService.list(new QueryWrapper<Danmaku>()
.eq("video_id", videoId)
.between("time_point", startTime, endTime)
.orderByAsc("time_point")));
}
}
// DanmakuWebSocket.java
@ServerEndpoint("/websocket/danmaku/{videoId}")
@Component
public class DanmakuWebSocket {
private static final Map<Long, Set<Session>> VIDEO_SESSIONS = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("videoId") Long videoId) {
VIDEO_SESSIONS.computeIfAbsent(videoId, k -> new CopyOnWriteArraySet<>())
.add(session);
}
@OnClose
public void onClose(Session session, @PathParam("videoId") Long videoId) {
VIDEO_SESSIONS.getOrDefault(videoId, Collections.emptySet())
.remove(session);
}
public static void broadcast(Long videoId, Danmaku danmaku) {
VIDEO_SESSIONS.getOrDefault(videoId, Collections.emptySet())
.forEach(session -> {
try {
session.getBasicRemote().sendText(JSON.toJSONString(danmaku));
} catch (IOException e) {
log.error("发送弹幕失败", e);
}
});
}
}
3. 搜索服务
// SearchController.java
@RestController
@RequestMapping("/api/search")
public class SearchController {
@Autowired
private SearchService searchService;
@Autowired
private RecommendService recommendService;
@GetMapping("/video")
public Result search(SearchDTO params) {
return Result.success(searchService.search(params));
}
@GetMapping("/hot")
public Result getHotVideos() {
return Result.success(recommendService.getHotVideos());
}
}
// SearchService.java
@Service
public class SearchService {
@Autowired
private RestHighLevelClient esClient;
@Autowired
private RedisTemplate<String, String> redisTemplate;
public PageResult<VideoDTO> search(SearchDTO params) {
try {
// 缓存热门搜索
String cacheKey = "search:" + DigestUtils.md5Hex(JSON.toJSONString(params));
String cacheResult = redisTemplate.opsForValue().get(cacheKey);
if (StringUtils.hasText(cacheResult)) {
return JSON.parseObject(cacheResult, new TypeReference<PageResult<VideoDTO>>(){});
}
// ES搜索
SearchRequest request = buildSearchRequest(params);
SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
// 解析结果
PageResult<VideoDTO> result = parseSearchResponse(response);
// 缓存结果(1分钟)
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(result), 1, TimeUnit.MINUTES);
return result;
} catch (Exception e) {
log.error("搜索失败", e);
throw new BusinessException("搜索失败");
}
}
private SearchRequest buildSearchRequest(SearchDTO params) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 关键词搜索
if (StringUtils.hasText(params.getKeyword())) {
boolQuery.must(QueryBuilders.multiMatchQuery(params.getKeyword(),
"title^3", "description"));
}
// 分类过滤
if (params.getCategoryId() != null) {
boolQuery.filter(QueryBuilders.termQuery("categoryId", params.getCategoryId()));
}
// 排序
SortBuilder<?> sortBuilder = params.getSort() == 1
? SortBuilders.fieldSort("viewCount").order(SortOrder.DESC)
: SortBuilders.fieldSort("createTime").order(SortOrder.DESC);
return new SearchRequest("video")
.source(new SearchSourceBuilder()
.query(boolQuery)
.sort(sortBuilder)
.from(params.getPage() * params.getSize())
.size(params.getSize())
.highlighter(new HighlightBuilder()
.field("title")
.preTags("<em>")
.postTags("</em>")
));
}
}
4. 推荐服务
// RecommendService.java
@Service
public class RecommendService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private VideoService videoService;
private static final String HOT_VIDEOS_KEY = "hot_videos";
public List<VideoDTO> getHotVideos() {
// 从Redis获取热门视频ID
Set<String> ids = redisTemplate.opsForZSet()
.reverseRange(HOT_VIDEOS_KEY, 0, 9);
if (CollectionUtils.isEmpty(ids)) {
return Collections.emptyList();
}
// 批量查询视频信息
List<Video> videos = videoService.listByIds(ids);
// 按热度排序
videos.sort((v1, v2) -> {
Double score1 = redisTemplate.opsForZSet().score(HOT_VIDEOS_KEY, v1.getId().toString());
Double score2 = redisTemplate.opsForZSet().score(HOT_VIDEOS_KEY, v2.getId().toString());
return score2.compareTo(score1);
});
return VideoConverter.INSTANCE.toDTO(videos);
}
// 每小时更新热门视频
@Scheduled(cron = "0 0 */1 * * ?")
public void updateHotVideos() {
// 统计最近一小时的播放量
LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
List<VideoViewCountDTO> viewCounts = videoService.statViewCount(oneHourAgo);
// 更新Redis排行榜
for (VideoViewCountDTO dto : viewCounts) {
redisTemplate.opsForZSet().incrementScore(
HOT_VIDEOS_KEY,
dto.getVideoId().toString(),
dto.getViewCount()
);
}
// 只保留最近24小时的数据
redisTemplate.expire(HOT_VIDEOS_KEY, 24, TimeUnit.HOURS);
}
}
5. 相关DTO类
@Data
public class SearchDTO {
private String keyword;
private Long categoryId;
private Integer sort; // 1-最多播放 2-最新发布
private Integer page = 0;
private Integer size = 10;
}
@Data
public class DanmakuDTO {
private Long videoId;
private String content;
private Float timePoint;
}
@Data
public class VideoMessage {
private Long videoId;
private String videoUrl;
}
@Data
public class VideoViewCountDTO {
private Long videoId;
private Long viewCount;
}
这些代码实现了视频网站的核心功能:
- 视频上传与转码
- 实时弹幕系统
- 全文搜索功能
- 热门视频推荐
主要特点:
- 使用Redis缓存热点数据
- 使用RabbitMQ实现异步处理
- 使用WebSocket实现实时弹幕
- 使用Elasticsearch实现全文搜索
- 实现了基本的性能优化
您可以根据实际需求进行进一步的定制和优化。
四、性能优化
-
视频播放优化
- 使用HLS协议实现自适应码率
- CDN加速分发
- 预加载下一个视频片段
-
搜索优化
- 热门关键词缓存
- 搜索结果缓存
- 索引预热
-
弹幕优化
- WebSocket连接池
- 弹幕消息队列
- 批量持久化
五、监控指标
-
视频相关
- 上传成功率
- 转码时长
- 播放成功率
- CDN带宽使用
-
搜索相关
- 搜索QPS
- 搜索响应时间
- 搜索准确率
- 索引同步延迟
通过以上详细设计,我们实现了一个功能完善、性能优异的视频网站核心功能。后续还可以继续优化和扩展更多特性。
原文地址:https://blog.csdn.net/exlink2012/article/details/143970499
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!