一、核心概念
抖音评论系统是一个典型的高并发、海量数据、读多写多的场景,需要重点考虑:
- 亿级用户、千万级视频的评论数据存储
- 热点视频可能瞬间产生百万级评论的写入压力
- 评论列表加载需要支持分页、排序(时间/热度)、嵌套回复
- 好友关系引入后要优先展示好友评论,涉及多维度查询
二、原理与设计关键点
2.1 基础评论系统设计
核心表结构
-- 评论表(分库分表)
CREATE TABLE t_comment (
id BIGINT PRIMARY KEY, -- 雪花ID
video_id BIGINT NOT NULL, -- 视频ID
user_id BIGINT NOT NULL, -- 用户ID
parent_id BIGINT DEFAULT 0, -- 父评论ID(0表示根评论)
root_id BIGINT DEFAULT 0, -- 根评论ID(用于快速查询某评论的所有回复)
content VARCHAR(500) NOT NULL, -- 评论内容
like_count INT DEFAULT 0, -- 点赞数
reply_count INT DEFAULT 0, -- 回复数
status TINYINT DEFAULT 1, -- 状态(1正常 0删除)
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
INDEX idx_video_root (video_id, root_id, create_time),
INDEX idx_user (user_id, create_time)
) ENGINE=InnoDB;
分库分表策略
按 video_id 取模分表(因为评论查询通常是”查某视频下的评论”):
// 分表路由示例
public class CommentShardingStrategy {
private static final int TABLE_COUNT = 256;
public String getTableName(Long videoId) {
int tableIdx = (int) (videoId % TABLE_COUNT);
return "t_comment_" + String.format("%03d", tableIdx);
}
}
读取优化
- 一级评论+二级评论分离
- 一次请求只加载根评论(
parent_id=0)+ 每条根评论的前 3 条回复 - “查看更多回复”时再按
root_id分页加载
- 一次请求只加载根评论(
- Redis 缓存热点视频评论
// 缓存结构示例 // Key: comment:video:{videoId}:page:{page} // Value: List<CommentDTO> (JSON序列化) String cacheKey = "comment:video:" + videoId + ":page:" + page; List<CommentDTO> comments = redisTemplate.opsForValue().get(cacheKey); if (comments == null) { comments = loadFromDB(videoId, page); redisTemplate.opsForValue().set(cacheKey, comments, 5, TimeUnit.MINUTES); } - 异步写入
- 评论提交后先写 MQ(Kafka/RocketMQ),快速返回
- 消费者批量写入 MySQL + 更新缓存
2.2 热点视频处理
问题场景
热门视频可能在短时间内产生数十万评论写入,导致:
- DB 写入热点分片压力过大
- 缓存频繁失效(缓存击穿)
解决方案
- 写入削峰
// 使用消息队列缓冲 @Service public class CommentService { @Autowired private KafkaTemplate<String, CommentEvent> kafkaTemplate; public void submitComment(CommentDTO dto) { // 前置校验(限流、敏感词) validateComment(dto); // 发送到MQ,异步处理 CommentEvent event = new CommentEvent(dto); kafkaTemplate.send("comment-topic", event); // 立即返回成功(实际写入异步完成) } } - 缓存双层设计
- 本地缓存(Caffeine):存储超热视频的评论列表(1分钟过期)
- Redis:存储热门视频评论(5分钟过期)
- 读取顺序:本地缓存 → Redis → DB
- 分布式限流
// 基于Redis令牌桶限流 @RateLimiter(key = "comment:video:#{videoId}", rate = 1000, capacity = 5000) public void submitComment(Long videoId, CommentDTO dto) { // ... }
2.3 加入好友关系的设计
需求分析
- 用户希望优先看到好友的评论
- 评论列表需要混合展示:好友评论在前 + 普通评论补充
实现方案
方案一:客户端聚合(推荐中小规模)
@Service
public class CommentWithFriendService {
public CommentListVO getComments(Long videoId, Long currentUserId, int page) {
// 1. 查询用户的好友列表(走缓存)
Set<Long> friendIds = friendService.getFriendIds(currentUserId);
// 2. 分别查询好友评论和普通评论
List<CommentDTO> friendComments = queryFriendComments(videoId, friendIds, page);
List<CommentDTO> normalComments = queryNormalComments(videoId, page);
// 3. 合并:好友评论在前,去重
List<CommentDTO> result = mergeComments(friendComments, normalComments, 20);
return new CommentListVO(result);
}
private List<CommentDTO> queryFriendComments(Long videoId, Set<Long> friendIds, int page) {
// 从评论表中筛选 user_id IN friendIds
return commentMapper.selectByVideoAndUsers(videoId, friendIds, page);
}
}
方案二:索引优化(大规模场景)
- 建立倒排索引(Elasticsearch)
{ "comment_id": "123456789", "video_id": "987654321", "user_id": "111222333", "is_friend": true, // 冗余好友标识(异步更新) "content": "太棒啦!", "create_time": "2025-11-20T10:00:00", "like_count": 100 } - 查询时按好友关系加权排序
// ES查询示例 SearchQuery query = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.termQuery("video_id", videoId)) .withSort(SortBuilders.scriptSort( new Script("doc['is_friend'].value ? 1000000 : doc['like_count'].value"), ScriptSortBuilder.ScriptSortType.NUMBER ).order(SortOrder.DESC)) .withPageable(PageRequest.of(page, 20)) .build();
方案三:预计算好友评论(超大规模)
- 用户发表评论时,通过 MQ 触发异步任务
- 将评论推送到该用户所有好友的”好友评论 Feed 流”(类似朋友圈)
- 读取时优先从 Feed 流读取
// 伪代码示例
@Async
public void pushToFriendsFeed(CommentDTO comment) {
Set<Long> friendIds = friendService.getFriendIds(comment.getUserId());
for (Long friendId : friendIds) {
String feedKey = "friend_comment_feed:" + friendId + ":" + comment.getVideoId();
redisTemplate.opsForZSet().add(feedKey, comment, System.currentTimeMillis());
}
}
三、性能与分布式考量
3.1 性能优化
| 优化点 | 方案 |
|---|---|
| DB 查询 | 覆盖索引、只查必要字段、Limit 限制 |
| 缓存命中 | 多级缓存(本地+Redis)、预热热点数据 |
| 网络IO | 批量查询、CDN 加速用户头像 / 昵称 |
| 序列化 | Protobuf 替代 JSON(RPC 场景) |
3.2 一致性保证
- 评论计数:通过 MQ 异步更新
reply_count/like_count(最终一致性) - 缓存与 DB:删除评论时先删 DB 再删缓存(Cache-Aside)
- 分布式事务:评论写入 + 消息发送使用本地消息表或 Seata AT 模式
3.3 线程安全
- 点赞计数:使用 Redis
INCR原子操作,定期同步到 MySQL - 热点评论更新:分布式锁(Redisson)防止缓存击穿
RLock lock = redissonClient.getLock("comment:lock:" + videoId);
try {
if (lock.tryLock(100, 10, TimeUnit.SECONDS)) {
// 重建缓存
rebuildCache(videoId);
}
} finally {
lock.unlock();
}
四、答题总结
核心设计要点:
- 存储:MySQL 按
video_id分库分表,热点数据 Redis 缓存 - 写入:MQ 削峰异步写入,分布式限流防止热点视频打爆
- 读取:多级缓存 + 分页懒加载(根评论 + 少量回复)
- 好友关系:
- 小规模:客户端查询好友列表后聚合排序
- 大规模:ES 建索引加权排序,或预计算 Feed 流
面试加分项:
- 提及敏感词过滤(DFA 算法本地缓存词库)
- 提及审核机制(AI 识别 + 人工复审的异步流程)
- 提及数据归档(冷数据定期迁移到对象存储)