弹幕、评论、点赞:高并发“屠龙技”

6 阅读5分钟

弹幕、评论、点赞:高并发“屠龙技” 🐉

今天我们不谈 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

逻辑

  1. 根据用户 ID 或随机算法,选择一个 shard 进行 incr
  2. 读取总数时,执行 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/代表从根到当前节点的路径。

idcontentpost_idpath (索引)
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 连接数监控。

结语

点赞丢了可以补,但电商下单丢了要赔钱;弹幕卡了没事,直播断了要被投诉。没有最好的架构,只有最适合业务的架构。 ​ 🤝