弹幕、评论、点赞:高并发“屠龙技” 🐉
今天我们不谈 CRUD,也不谈什么“如何用 Spring Boot 十分钟搭建博客”。咱们聊点硬核的——当你面对百万 QPS 的流量洪峰,面对产品经理那句“这个要和 B 站/抖音 一样丝滑”时,你的架构该怎么设计?
主角:弹幕、评论、点赞。这三驾马车看似简单,实则是缓存一致性、最终一致性与实时流处理的经典战场。
1. 点赞系统:不仅仅是 Redis Incr 🚀
很多开发认为点赞就是 Redis.incr()。但在资深架构师眼里,这里有三个雷区:数据倾斜、热 Key 问题、回源风暴。
痛点分析
- 热 Key:某明星官宣,瞬间几十万点赞砸到一个
postId上,单个 Redis 分片直接被打穿。 - 一致性:Redis 挂了,数据怎么补?数据库和缓存怎么对账?
落地方案:LocalCache + Redis + 分段锁
1. 客户端/网关层:请求合并与限流
前端在发起点赞时,必须做防抖(Debounce) 。后端网关层(如 Nginx/OpenResty)直接拦截短时间内的重复请求。
2. 服务端:分段计数(Segmented Counter)
为了解决热 Key,我们不能把所有鸡蛋放在一个篮子里。采用 Counter Sharding。
Key 设计:
like:count:{postId}:{shardIndex} // 例如分成 8 个 shard
逻辑:
- 根据用户 ID 或随机算法,选择一个 shard 进行
incr。 - 读取总数时,执行
mget汇总 8 个 shard 的值。
// 伪代码:分段计数
private static final int SHARD_COUNT = 8;
public void like(Long postId, Long userId) {
int shard = userId.hashCode() & (SHARD_COUNT - 1); // 位运算取模,快!
String key = String.format("like:count:%d:%d", postId, shard);
// 利用 Lua 脚本保证“判断是否点过”和“计数”的原子性
String luaScript = """
if redis.call('sismember', KEYS[1], ARGV[1]) == 1 then
return -1
end
redis.call('sadd', KEYS[1], ARGV[1])
return redis.call('incr', KEYS[2])
""";
// ... 执行脚本
}
3. 持久化:Binlog 异构
不要让 Java 服务直接写 MySQL 点赞明细。通过 Canal 监听 Redis 的 AOF 或业务 Binlog,将增量数据同步到 MQ,再由消费端落库。这样主链路只依赖 Redis,RT(响应时间)极低。
2. 评论系统:无限层级与海量存储 🌳
不用 Adjacency List(邻接表)去递归查询。我们要解决的是深分页和树形结构的聚合效率。。
落地方案:Materialized Path (物化路径) + 冷热隔离
1. 数据库建模:放弃 Parent_Id
使用 path字段(VARCHAR)。
例如:/00001/00002/00003/代表从根到当前节点的路径。
| id | content | post_id | path (索引) |
|---|---|---|---|
| 101 | 一楼大佬 | 888 | /00101/ |
| 102 | 我也觉得 | 888 | /00101/00102/ |
优势:
- 查询子树只需
where path like '/00101/%',一次 IO。 - 查询祖先节点直接解析字符串。
2. 读写分离与冷热数据
- 热数据(最近7天/前100条) :存入 Redis Hash 或 Caffeine 本地缓存。
- 冷数据:MySQL 分库分表(按
post_id哈希)。 - 全文检索:评论内容同步到 Elasticsearch,支持高亮分词搜索。
3. 展示策略:懒加载(Lazy Loading)
永远不要一次性返回 5000 条评论。前端默认展示前 3 层,点击“展开”再加载子评论。后端只提供扁平化的 List 接口,树形结构交给前端组装,减轻 CPU 压力。
3. 弹幕系统:实时流与弱一致性 ⚡
弹幕是典型的 Streaming 场景。关注的是时序性、丢弃策略和广播风暴。
落地方案:WebSocket + Kafka + 本地推拉结合
架构图
Client -> LB (Nginx) -> WS Server (Cluster) -> Kafka (Topic: Danmu) -> WS Server -> Client
核心难点与对策
1. 连接管理(Session 扩散)
不能用 HashMap存 Session。WS 集群中,用户 A 连在 Server 1,用户 B 连在 Server 2。A 发的弹幕如何让 B 收到?
- 方案:引入 Redis Pub/Sub 或 Kafka。
- 逻辑:Server 1 收到弹幕,发送到 Kafka。所有 Server 订阅该 Topic。Server 2 消费到消息后,检查本地是否有对应 Room 的连接,有则推送。
2. 消息协议(Protobuf > JSON)
在高并发下,JSON 的序列化开销太大。弹幕系统推荐使用 Protobuf 或 MessagePack,体积减少 30%-50%,带宽就是钱啊!💰
3. 弹幕池与降级
- 弹幕池:服务端维护一个滑动窗口(如最近 5 分钟),防止内存溢出。
- 熔断:如果 Kafka 积压严重,直接降级为“本地广播”或丢弃新消息,并提示用户“网络拥挤”。
// 伪代码:弹幕分发器
@Service
public class DanmuDispatcher {
// 使用 Disruptor 或 BlockingQueue 做本地缓冲
private final RingBuffer<DanmuEvent> ringBuffer;
public void dispatch(Danmu danmu) {
// 1. 风控过滤 (敏感词)
if (sensitiveFilter.contains(danmu.getContent())) {
return;
}
// 2. 推送到 MQ,而不是同步遍历 Session
kafkaTemplate.send("danmu.topic." + danmu.getRoomId(), danmu);
}
}
4. 资深工程师的 Checklist ✅
为了显得你足够资深,在评审会上你可以抛出以下几个点:
| 模块 | 核心考点 | 避坑指南 |
|---|---|---|
| 点赞 | 热 Key 处理 | 不要用 incr硬抗热 Key,必须 Sharding。 |
| 评论 | 深分页/树结构 | 拒绝递归 SQL,使用 Materialized Path 或闭包表。 |
| 弹幕 | 连接状态管理 | 不要尝试在内存中维护全局 Session 映射,交给消息总线。 |
| 通用 | 监控告警 | Redis 内存预警、Kafka Lag 监控、WS 连接数监控。 |
结语
点赞丢了可以补,但电商下单丢了要赔钱;弹幕卡了没事,直播断了要被投诉。没有最好的架构,只有最适合业务的架构。 🤝