在内容类产品中,评论系统几乎是标配。无论是博客、社区、视频平台,评论都是提升互动的关键组成部分。然而,评论系统的设计并非"一套走天下",它的底层结构需要根据业务的流量体量灵活调整。
本篇文章将结合不同流量场景,从表结构设计、查询逻辑、技术选型三个维度,讨论评论功能在不同阶段的落地方案,让你轻松应对面试!
场景一:中小流量系统(单库单表 + 两级评论)
适用场景:CSDN、掘金这类文字内容为主、日活低于 10 万的站点。
表结构设计
采用单表结构支持两级评论(一级评论 + 回复),结构简单、易维护:
sql
CREATE TABLE comment (
id BIGINT PRIMARY KEY,
biz_id BIGINT NOT NULL, -- 文章、视频等业务 ID
biz_type TINYINT NOT NULL, -- 枚举:1=文章 2=视频
user_id BIGINT NOT NULL, -- 评论人
parent_id BIGINT DEFAULT 0, -- 父评论 ID,0 表示一级评论
root_id BIGINT DEFAULT 0, -- 根评论 ID,自己是一级时等于 id
content TEXT NOT NULL,
like_count INT DEFAULT 0,
reply_count INT DEFAULT 0,
status TINYINT DEFAULT 0, -- 0=正常 1=审核中 2=已删除
created_at DATETIME,
INDEX idx_biz (biz_id, parent_id, id),
INDEX idx_root (root_id),
INDEX idx_user (user_id)
);
查询逻辑设计
查询某个业务对象的一级评论(首屏 / 下拉翻页)
sql
SELECT * FROM comment
WHERE biz_id = #{bizId} AND parent_id = 0 AND status = 0
ORDER BY id DESC
LIMIT #{pageSize} OFFSET #{offset};
查询某条评论的回复
sql
SELECT * FROM comment
WHERE root_id = #{commentId} AND parent_id != 0 AND status = 0
ORDER BY id ASC;
示例代码
java
public List<Comment> getTopComments(Long bizId, int page, int size) {
return commentMapper.selectList(
new LambdaQueryWrapper<Comment>()
.eq(Comment::getBizId, bizId)
.eq(Comment::getParentId, 0)
.eq(Comment::getStatus, 0)
.orderByDesc(Comment::getId)
.last("LIMIT " + size + " OFFSET " + (page - 1) * size)
);
}
public List<Comment> getReplies(Long rootId) {
return commentMapper.selectList(
new LambdaQueryWrapper<Comment>()
.eq(Comment::getRootId, rootId)
.ne(Comment::getParentId, 0)
.eq(Comment::getStatus, 0)
.orderByAsc(Comment::getId)
);
}
场景二:中高流量系统(分库分表 + 异步更新)
适用场景:知乎、B 站、头条等中大型社区,评论量过亿,日均写入上百万。
架构策略
项目 | 策略 |
---|---|
分表方式 | 按 biz_id 哈希路由,例如 comment_0 ~ comment_63 |
ID 生成 | 雪花算法(Snowflake) |
点赞计数 | Redis + MQ 异步聚合写库 |
审核系统 | 评论先落审核队列,审核通过再入主表 |
热度排序 | 预计算 hot_score ,或 Redis ZSET 排序 |
表结构(逻辑结构)
sql
comment_${n} (
id BIGINT PRIMARY KEY,
biz_id BIGINT,
user_id BIGINT,
parent_id BIGINT,
root_id BIGINT,
content TEXT,
like_count INT,
reply_count INT,
hot_score DOUBLE,
status TINYINT,
created_at DATETIME,
INDEX(biz_id, parent_id),
INDEX(root_id)
)
点赞、回复计数等异步写入:
java
// 点赞后异步写 MQ
likeService.like(commentId, userId);
mqProducer.send("COMMENT_LIKE", commentId);
查询某个视频的所有一级评论(分页 + 热度)
sql
SELECT * FROM comment_03
WHERE biz_id = 1001 AND parent_id = 0 AND status = 0
ORDER BY hot_score DESC
LIMIT 20;
热度分数一般为:
ini
hot_score = like_count * 0.8 + reply_count * 1.0 + time_decay(create_time)
热度排序 往往是分数衰减函数:
hot_score = like_count * 0.8 + reply_count * 1.0 + create_ts / decay
在 MySQL 里做会很慢,B 站/知乎大多把热排算分移到 Redis ZSET 或 ES 脚本排序。
技术栈建议
- 分库分表:ShardingSphere 或 MyCat
- 缓存层:Redis
- MQ:RocketMQ / Kafka
- 审核:阿里绿网 / 自建敏感词 + 模型系统
场景三:超大流量系统(KV 存储 + 时间线索引)
适用场景:抖音、快手、Instagram 等亿级日活的移动平台
架构模型
评论内容存储在 TiKV、HBase、DynamoDB 等 KV 型数据库中;列表分页使用 Redis Timeline(ZSet/List)索引。
-
写入通道 :先落 Kafka,流式计算(Flink / Spark Streaming)做审核与反垃圾,最后异步写入 HBase / TiKV。
-
读取通道:双层时间线(Timeline)模型
- Root TL:视频 ID → 顶级评论的 ID 列表(ZSET or List,内存保 1 万条,冷数据分页补齐到 HBase)
- Reply TL:顶级评论 ID → 子评论 ID 列表
- 接口按「cursor = last_id」方式增量拉取;天然支持滚动上拉。
数据结构设计
text
# 存评论
HBase: comment:{commentId} → 评论内容结构
# 根评论时间线
Redis: tl:root:{videoId} = ZSET(commentId, hot_score)
# 回复评论时间线
Redis: tl:reply:{rootId} = List(commentId1, commentId2,...)
查询流程(伪代码)
java
List<Long> topIds = redis.zrevrange("tl:root:video123", offset, offset + 19);
Map<Long, Comment> commentMap = hbase.batchGet("comment", topIds);
点赞系统优化
- 用户点赞写入 Redis Set(
like:{userId}
),前端展示批量判断 - 定时汇总写入 ClickHouse 或 MySQL
- 热门评论
like_count
每分钟同步
优化建议
问题场景 | 优化方式 |
---|---|
评论首屏加载慢 | Redis 缓存首屏 JSON,MQ 同步更新 |
点赞/回复不一致 | 所有修改走 MQ 异步聚合写库 |
"热评"插入后楼层错乱 | 楼层号稀疏生成,避免重排 |
评论审核敏感 | 写入审核表,通过后再搬迁 |
分页 OFFSET 性能差 | 用 id < last_id 做滚动翻页 |
总结
流量规模 | 技术方案 | 查询方式 |
---|---|---|
中小体量 | 单表 + 两级 root_id | MySQL 直接分页 + join |
中高体量 | 分表 + root_id + 异步聚合 | 分表查询 + Redis 缓存 |
超大规模 | HBase/TiKV + Redis Timeline | 多层缓存 + KV 查询 |
抛开业务谈设计都是耍流氓,根据不同的规模选择最合适的方案,满足现有业务发展需求,便是优秀的架构设计。 小体量可采用通用设计模型,预留关键扩展点(如 ID 算法、root_id、MQ 链路),未来才能轻松应对流量爬坡。
最后
如果文章对你有帮助,点个免费的赞鼓励一下吧!关注公众号:加瓦点灯, 每天推送干货知识!