自学内容网 自学内容网

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;

视频上传流程

  1. 前端分片上传
  2. 后端合并视频
  3. 转码处理
  4. 存储到OSS

项目亮点

  1. 采用微服务架构,提高系统可扩展性
  2. 使用Redis缓存热点数据,提升访问速度
  3. 通过RabbitMQ实现异步处理,提高系统吞吐量
  4. 实现弹幕系统,提升用户体验

性能优化

  1. 使用Redis缓存
  2. 数据库索引优化
  3. 使用CDN加速
  4. 前端性能优化

项目部署

  1. 使用Docker容器化部署
  2. Jenkins自动化部署
  3. Nginx反向代理
  4. 服务器集群

总结

通过本项目实战,我们将掌握:

  1. Spring Boot项目开发全流程
  2. 主流技术栈的整合使用
  3. 分布式系统架构设计
  4. 性能优化最佳实践

后续我们将逐步完善各个功能模块的具体实现,敬请期待!

参考资料

  • 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;
}

这些代码实现了视频网站的核心功能:

  1. 视频上传与转码
  2. 实时弹幕系统
  3. 全文搜索功能
  4. 热门视频推荐

主要特点:

  1. 使用Redis缓存热点数据
  2. 使用RabbitMQ实现异步处理
  3. 使用WebSocket实现实时弹幕
  4. 使用Elasticsearch实现全文搜索
  5. 实现了基本的性能优化

您可以根据实际需求进行进一步的定制和优化。

四、性能优化

  1. 视频播放优化

    • 使用HLS协议实现自适应码率
    • CDN加速分发
    • 预加载下一个视频片段
  2. 搜索优化

    • 热门关键词缓存
    • 搜索结果缓存
    • 索引预热
  3. 弹幕优化

    • WebSocket连接池
    • 弹幕消息队列
    • 批量持久化

五、监控指标

  1. 视频相关

    • 上传成功率
    • 转码时长
    • 播放成功率
    • CDN带宽使用
  2. 搜索相关

    • 搜索QPS
    • 搜索响应时间
    • 搜索准确率
    • 索引同步延迟

通过以上详细设计,我们实现了一个功能完善、性能优异的视频网站核心功能。后续还可以继续优化和扩展更多特性。


原文地址:https://blog.csdn.net/exlink2012/article/details/143970499

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!