当前位置: 首页 > news >正文

视频平台的数据库设计:从用户体系到弹幕系统的Schema架构复盘

视频平台的数据库设计:从用户体系到弹幕系统的Schema架构复盘

一、背景与问题定义

视频平台的数据库设计与传统业务系统有显著差异:读多写少但写入峰值尖锐、冷热数据分化严重、以及弹幕这类高吞吐写入场景对数据库选型提出挑战。一个典型的千万 DAU 视频平台,弹幕写入的峰值 QPS 可达 50 万以上,远超出单机 MySQL 的承载能力。

本文以"用户—视频—互动"三条核心业务线为骨架,复盘整个平台的 Schema 设计、弹幕高吞吐写入方案、以及数据归档策略。

二、核心业务 Schema 设计

2.1 用户体系

用户表的核心设计原则是:高频查询字段与低频字段垂直拆分,认证信息与基础信息隔离。

-- 用户基础信息表(高频读) CREATE TABLE `user_base` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL COMMENT '业务用户ID,对外暴露', `nickname` VARCHAR(64) NOT NULL, `avatar_url` VARCHAR(512) DEFAULT '', `bio` VARCHAR(256) DEFAULT '' COMMENT '个人简介', `follower_count` INT NOT NULL DEFAULT 0, `following_count` INT NOT NULL DEFAULT 0, `video_count` INT NOT NULL DEFAULT 0 COMMENT '发布视频数', `total_likes` BIGINT NOT NULL DEFAULT 0, `creator_level` TINYINT NOT NULL DEFAULT 0 COMMENT '创作者等级0-10', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 2冻结 3注销', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_id` (`user_id`), KEY `idx_creator_level` (`creator_level`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 用户认证信息表(低频访问,安全隔离) CREATE TABLE `user_auth` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `phone` VARCHAR(32) DEFAULT '' COMMENT 'AES加密存储', `email` VARCHAR(128) DEFAULT '', `password_hash` VARCHAR(256) NOT NULL, `last_login_at` DATETIME DEFAULT NULL, `last_login_ip` VARCHAR(64) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

垂直拆分的动机:user_auth表仅在登录/注册时访问,与user_base每页都查的模式完全不同。分开后,user_auth可以放在加密存储卷上,甚至使用独立的数据库实例。

2.2 视频信息表

CREATE TABLE `video_info` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `video_id` BIGINT NOT NULL COMMENT '业务视频ID', `user_id` BIGINT NOT NULL, `title` VARCHAR(256) NOT NULL, `description` TEXT DEFAULT NULL, `cover_url` VARCHAR(512) DEFAULT '', `duration` INT NOT NULL DEFAULT 0 COMMENT '视频时长(秒)', `category_id` INT NOT NULL DEFAULT 0, `tags` JSON DEFAULT NULL COMMENT 'AI生成的标签JSON数组', `play_count` BIGINT NOT NULL DEFAULT 0 COMMENT '播放次数', `like_count` INT NOT NULL DEFAULT 0, `comment_count` INT NOT NULL DEFAULT 0, `share_count` INT NOT NULL DEFAULT 0, `barrage_count` INT NOT NULL DEFAULT 0 COMMENT '弹幕总数', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0转码中 1正常 2审核中 3下架', `audit_result` JSON DEFAULT NULL COMMENT '多模态审核结果', `publish_at` DATETIME DEFAULT NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_video_id` (`video_id`), KEY `idx_user_status` (`user_id`, `status`), KEY `idx_category_publish` (`category_id`, `publish_at`), KEY `idx_play_count` (`status`, `play_count`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

关键设计决策:

  1. 计数器冗余play_countlike_count等计数字段直接冗余在视频表上。虽然违反了严格的规范化,但避免了 SELECT COUNT(*) 的昂贵开销。计数器更新通过 Redis 原子操作 + 异步刷 MySQL。
  2. JSON 字段用于动态属性tags(AI 标签)和audit_result(审核结果)使用 JSON 类型。这两个字段结构变化频繁——标签体系每季度迭代,审核维度持续增加——JSON 的 Schema-less 特性避免了频繁 DDL。
  3. status 字段的状态机:严格遵循 0→2→1 的流转(转码→审核→正常),不允许逆向流转(审核不过直接到 3 下架)。

2.3 互动体系

-- 评论表 CREATE TABLE `comment` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `comment_id` BIGINT NOT NULL, `video_id` BIGINT NOT NULL, `user_id` BIGINT NOT NULL, `parent_id` BIGINT NOT NULL DEFAULT 0 COMMENT '0=一级评论', `reply_to_uid` BIGINT NOT NULL DEFAULT 0 COMMENT '被回复者', `content` TEXT NOT NULL, `like_count` INT NOT NULL DEFAULT 0, `status` TINYINT NOT NULL DEFAULT 1, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_comment_id` (`comment_id`), KEY `idx_video_created` (`video_id`, `created_at`), KEY `idx_parent` (`video_id`, `parent_id`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 点赞表(只记录关系,计数器在Redis) CREATE TABLE `like_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `target_type` TINYINT NOT NULL COMMENT '1视频 2评论', `target_id` BIGINT NOT NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_target` (`user_id`, `target_type`, `target_id`), KEY `idx_target` (`target_type`, `target_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

三、弹幕高吞吐写入方案

3.1 整体写入链路

弹幕的写入链路遵循"先广播,后落盘"的原则。用户发送弹幕后,先写入 Redis(保证实时广播),同时投递到 Kafka(保证持久化),Kafka Consumer 批量写入 MySQL。

3.2 Redis 实时存储

@Service public class BarrageWriteService { private final StringRedisTemplate redisTemplate; private final KafkaTemplate<String, BarrageMessage> kafkaTemplate; public void sendBarrage(BarrageMessage msg) { // 1. 写入 Redis(实时查询用) String redisKey = "barrage:video:" + msg.getVideoId(); long score = msg.getTimestamp(); // 视频时间戳作为score redisTemplate.opsForZSet().add(redisKey, JSON.toJSONString(msg), score); // 2. Redis ZSet 只保留最近 5000 条 redisTemplate.opsForZSet().removeRange(redisKey, 0, -5001); // 3. 异步投递到 Kafka 做持久化 kafkaTemplate.send("barrage-persist", String.valueOf(msg.getVideoId()), msg); // 4. 实时广播给同房间用户(通过 WebSocket) broadcastToRoom(msg.getVideoId(), msg); } }

3.3 Kafka 批量写入 MySQL

@Component public class BarragePersistConsumer { private static final int BATCH_SIZE = 500; private static final int FLUSH_INTERVAL_MS = 2000; private final List<BarrageMessage> buffer = new ArrayList<>(); private long lastFlushTime = System.currentTimeMillis(); @KafkaListener(topics = "barrage-persist", concurrency = "3") public void onMessage(BarrageMessage msg) { synchronized (buffer) { buffer.add(msg); if (buffer.size() >= BATCH_SIZE || System.currentTimeMillis() - lastFlushTime >= FLUSH_INTERVAL_MS) { flushBuffer(); } } } private void flushBuffer() { if (buffer.isEmpty()) return; List<BarrageMessage> batch; synchronized (buffer) { batch = new ArrayList<>(buffer); buffer.clear(); lastFlushTime = System.currentTimeMillis(); } // INSERT ... ON DUPLICATE KEY UPDATE 实现幂等 jdbcTemplate.batchUpdate( """INSERT INTO barrage_{tableSuffix} (barrage_id, video_id, user_id, content, video_time, created_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE content = VALUES(content)""", batch, BATCH_SIZE, (ps, msg) -> { ps.setLong(1, msg.getBarrageId()); ps.setLong(2, msg.getVideoId()); ps.setLong(3, msg.getUserId()); ps.setString(4, msg.getContent()); ps.setDouble(5, msg.getVideoTime()); ps.setTimestamp(6, Timestamp.from(msg.getCreatedAt())); }); } }

3.4 弹幕按月分表

弹幕表按月分表(barrage_202607barrage_202608),依据是弹幕的查询场景高度集中于当前视频对应的月份——用户看弹幕时,绝大多数请求落在最近几周的视频。历史视频的弹幕查询量占比不到 2%。

CREATE TABLE `barrage_202607` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `barrage_id` BIGINT NOT NULL, `video_id` BIGINT NOT NULL, `user_id` BIGINT NOT NULL, `content` VARCHAR(512) NOT NULL, `video_time` DOUBLE NOT NULL COMMENT '弹幕在视频中的时间位置(秒)', `status` TINYINT NOT NULL DEFAULT 1, `created_at` DATETIME(3) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_barrage_id` (`barrage_id`), KEY `idx_video_time` (`video_id`, `video_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

四、Schema 设计原则总结

4.1 反范式化

计数器字段(play_countlike_count)直接冗余在主表上,是典型的"用存储换查询性能"。一个视频详情页每次被访问都需要展示这些数字,如果每次 SELECT COUNT(*) 从统计表实时计算,在百万 QPS 的读压力下会直接击穿数据库。

4.2 预留字段

视频表的tagsaudit_result使用 JSON 类型而非结构化字段,就是在为未来的属性扩展预留空间。当 AI 团队说"下个月我们要新增 3 个维度的标签"时,JSON 字段只需改代码逻辑,不需要 DDL。

4.3 归档策略

数据分为热、温、冷三层:

  • 热数据(近 3 个月):完整保留在 MySQL 主库,读写均可。
  • 温数据(3~12 个月):保留在 MySQL 只读副本,查询延迟略高但可接受。
  • 冷数据(12 个月以上):归档到对象存储(Parquet 格式),按需通过 Presto/Trino 查询,不占用 MySQL 存储。

弹幕的归档最激进:3 个月以上的弹幕直接从 MySQL 迁移到对象存储,前端播放时通过 CDN 边缘节点加载归档弹幕文件。

五、总结

视频平台的数据库设计围绕三个核心原则:读写分离(高频读字段垂直拆分、计数缓存到 Redis)、冷热分离(弹幕按月分表、3 个月归档)、以及用存储换性能(合理反范式化)。弹幕的高吞吐写入通过"Redis → Kafka → 批量 MySQL"三级链路实现,峰值写入从单机 MySQL 的 5000 QPS 提升到 50 万 QPS。

后续优化方向:引入 TiDB 替代部分按月分表的 MySQL 集群(减少运维成本);弹幕的查询链路引入 Redisearch 做全文检索(支持"在这部剧的第 5 集搜索所有红色弹幕");以及冷数据查询的统一化(构建 Iceberg + Trino 的冷数据查询层)。

http://www.cnnetsun.cn/news/3597308.html

相关文章:

  • Android Studio医院挂号系统毕设开发指南
  • 上下文工程:提升AI对话连贯性与计算效率的关键技术
  • AI训练数据准备:用OpenClaw自动化下载海量图片,如何搭配隧道防封?
  • C++高性能图像处理库ximage:轻量级替代OpenCV的设计与实现
  • 探索密封胶新选择:单组分硅酮免垫密封胶哪家更值得信赖?
  • Selenium屏幕截图全攻略:从基础实现到工程化集成
  • 西门子S7-200 SMART编程软件安装与配置全攻略
  • 飞轮储能精密储能舱车间通风 易互德防静电稳温布风管保障储能设备装配安全
  • 工业时序数据库怎么选?多模融合架构实战,附写入性能与压缩比实测
  • 手写SGI STL内存池:从原理到实现,深入C++性能优化核心
  • 深入解析C++函数:从参数传递到现代函数式编程实践
  • C++入门实战:从环境搭建到项目开发,掌握核心概念与STL应用
  • 学术论文降重十大方案与查重系统应对策略
  • 放弃财产继承公证需要带什么手续?放弃财产继承公证怎么办理?
  • Unity混合现实开发:MRTK框架核心交互与空间感知实战指南
  • Unity游戏模组开发实战:基于MelonLoader的代码注入与Harmony补丁技术
  • 低功耗蓝牙实时图像传输方案设计与优化
  • Anthropic为Claude新增录屏生成Skill功能,降低操作门槛,重塑工作护城河
  • 7z加密压缩包密码恢复实战:基于hashcat的自动化测试技术指南
  • RB-花生四烯酸/猪去氧胆酸/亚油酸/鹅脱氧胆酸,荧光染料标记脂质类化合物
  • 解压缩软件怎么选?从格式兼容到文件安全的四个判断标准
  • Kimi K3与AI智能体开发实战:从长文本处理到自动化工作流
  • C++网络验证对接模板:安全授权与反破解实践
  • Lua与C/C++交互实战:从动态库编译到性能优化全解析
  • NS-3网络模拟器在Ubuntu下的安装与配置指南
  • 嘎嘎降AI和PaperRR哪个更适合硕士论文:2026年硕士论文降AI工具实测对比
  • 算力与CDN融合架构实践:FP16加速与边缘计算优化
  • 深入解析Tiva™ TM4C129x以太网控制器:从MAC、DMA到驱动开发实践
  • 单对以太网是什么?SPE连接器选型与10BASE-T1L应用
  • Midjourney AI绘画:从入门到精通的30天指南