从王鹤棣直播间 500 万点赞说起:Java 后端如何扛住热点互动洪峰?

从王鹤棣直播间 500 万点赞说起:Java 后端如何扛住热点互动洪峰? 从王鹤棣直播间 500 万点赞说起Java 后端如何扛住热点互动洪峰摘要2026 年 5 月 24 日王鹤棣雅迪品牌直播间公开报道中出现 10 万 在线、500 万 点赞的互动数据。本文不讨论娱乐事件本身而是借这个真实高并发场景拆解 Java 后端如何设计点赞幂等、Kafka 异步计数、Caffeine Redis MySQL 三级缓存、单飞锁和计数重建自愈。1. 真实场景直播间突然从 2 万在线冲到 10 万2026 年 5 月 24 日王鹤棣亮相雅迪品牌直播活动。公开报道里提到直播间在线人数从约 2 万快速冲到 10 万整场点赞量突破 500 万。这类新闻从业务侧看是明星热度。从 Java 后端视角看就是典型的热点互动洪峰同一个直播间 同一段时间 同一个热点对象 大量用户同时点赞、关注、评论、刷新信息流如果 500 万次点赞分布在 1 小时内平均每秒就是 1300 次互动如果集中在 10 分钟内平均每秒会到 8000 次。真实平台还会叠加弹幕、关注、榜单、推荐流刷新压力不是只打一个接口而是同时打计数、缓存、消息队列和数据库。所以本文要讲的不是“Redis 很快”而是Java 后端扛高并发互动要把状态、计数、缓存、重建拆开设计。2. 先分清直播点赞和帖子点赞不是一回事直播间点赞通常是互动事件用户可以连续点击用来表达热度。帖子点赞、收藏、关注则更像状态变化场景本质是否幂等后端重点直播间点赞互动事件不一定高吞吐、削峰、近实时聚合帖子点赞用户状态是幂等、计数一致性收藏用户状态是状态切换、重复请求兜底关注主播用户关系是关系变更、粉丝计数一致所以王鹤棣直播间 500 万点赞适合作为热点互动洪峰的真实入口落到 Java 面试和项目亮点时要讲清楚状态型点赞怎么做。3. 为什么 MySQL 直接 UPDATE 扛不住最简单的点赞计数UPDATEpostSETlike_countlike_count1WHEREid?热点来了会有两个问题。第一热点行锁竞争。同一条帖子被大量点赞所有事务都在写同一行。InnoDB 更新时要加排他锁其他事务只能等。最后系统慢不是因为计算复杂而是因为大家都卡在同一行上。第二幂等没解决。用户点一次网络超时重试一次后端收到两次请求计数就加两次。前端防抖管不了重试、多端登录、脚本请求和网关重放。所以给字段加索引也没用。索引只能帮你找到那一行不能解决并发写同一行。4. 为什么 Redis INCR 也不够很多人会想到INCR like_count:post_123Redis 单条命令原子性能确实好。但INCR不知道是谁点的。重复点赞、网络重试、并发请求都会继续 1。于是再加集合SISMEMBER liked_users:post_123 user_10086 INCR like_count:post_123 SADD liked_users:post_123 user_10086问题是这三条命令不是原子的。两个请求可能都先看到用户没点过然后都执行INCR。解决办法是 Lua把“读状态、判断、写状态、返回变化量”放到 Redis 内部一次执行。5. Bitmap Lua让点赞变成状态切换状态型点赞的关键不是把数字加 1而是判断用户状态有没有变化。用 Bitmap 表示用户是否点过赞bitmap:like:post:123:chunk:0 bit[10086] 0 未点赞 bit[10086] 1 已点赞用户 ID 很大时做分片chunk userId / 65536 bit userId % 65536Lua 脚本localbeforeredis.call(GETBIT,KEYS[1],ARGV[1])localtargettonumber(ARGV[2])ifbeforetargetthenreturn0endredis.call(SETBIT,KEYS[1],ARGV[1],target)return1Java 侧只在状态变化时发送计数事件longchangedredis.eval(lua,List.of(bitmapKey),List.of(bitOffset,target));if(changed1){kafkaTemplate.send(counter-events,newCounterEvent(postId,field,delta));}这样重复点赞不会重复计数取消点赞也能返回-1事件。6. Kafka 聚合把高频写折叠成低频写如果每次点赞都同步更新计数快照Redis 计数 key 还是会承压。更合理的链路是点赞请求 - Redis Lua 切换 Bitmap - 状态变化才发送 Kafka 事件 - Consumer 聚合 delta - 每 1 秒 flush 到计数快照同一个帖子 1 秒内被点 100 次最终可以折叠成一次100写入而不是 100 次写入。Spring Kafka 使用手动提交时可以处理成功后再acknowledge()KafkaListener(topicscounter-events,groupIdcounter-agg)publicvoidonMessage(CounterEventevent,Acknowledgmentack){try{aggregate(event);ack.acknowledge();}catch(Exceptionex){throwex;}}这里要接受一个事实Kafka 至少一次投递可能重复。所以系统需要事实层。Bitmap 是事实层计数快照是派生层。快照短暂不准可以从 Bitmap 重新算回来。7. SDS 快照读计数不要每次 BITCOUNTBitmap 准但不能每次读都BITCOUNT。如果一个帖子有 20 个 Bitmap 分片读一次点赞数就要 20 次BITCOUNT再累加。热点帖子高频读时这会把 Redis 打得很重。所以需要计数快照cnt:post:123 - binary snapshot 0-3 like_count 4-7 fav_count 8-11 comment_count这种 SDS 定长快照有几个好处批量读可以 pipeline GET。字段位置固定Java 按 offset 解析。返回数据短没有 Hash field 名开销。快照异常时可以从 Bitmap 重建。读路径读帖子列表 - pipeline GET 多个 cnt:post:{id} - 正常解析快照 - 缺失或长度异常触发重建8. Caffeine Redis MySQL热点读要做三级缓存热点直播或热帖出现时用户不只是点赞还会不断刷新首页、主播页、活动页。读路径可以做成三级缓存L1 Caffeine 本地缓存 - L2 Redis 分页缓存 - L3 MySQLRedis 官方 cache-aside 文档提到读多写少的实体可以先查 Redismiss 后回源主库再写回缓存。但同一份文档也提醒热门 key 在高并发下过期会让大量进程同时查询数据库形成 cache stampede。信息流可以拆成两类 keyfeed:uid:{userId}:p:{page} - 只存帖子 ID 列表 post:detail:{postId} - 存帖子详情Feed 只存 ID是为了降低失效成本。帖子被删除或下架时第二阶段查详情查不到直接过滤即可不需要主动删除所有用户 Feed 缓存。9. 单飞锁热点详情过期时只让一个线程回源热点 Key 过期那一刻最危险。如果post:detail:{id}正好过期100 个请求同时 miss都去查 MySQL就是缓存击穿。单机内可以用ConcurrentHashMap CompletableFuture合并同一个 key 的并发 missprivatefinalConcurrentHashMapLong,CompletableFuturePostDetailinflightnewConcurrentHashMap();publicPostDetailgetPostDetail(longpostId){PostDetailcachedredisCache.get(postId);if(cached!null){returncached;}CompletableFuturePostDetailfutureinflight.computeIfAbsent(postId,id-CompletableFuture.supplyAsync(()-{PostDetaildetailpostMapper.selectById(id);redisCache.set(id,detail);returndetail;},executor).whenComplete((r,ex)-inflight.remove(id)));returnfuture.join();}重点同一个postId共享同一个Future。第一个请求回源其他请求等结果。whenComplete必须清理 inflight key避免失败后内存泄漏。它不是分布式锁但非常划算。100 个请求分散到 10 个实例没有单飞锁是 100 次 DB 查询有单飞锁后大约是 10 次。10. 重建风暴限流 指数退避计数快照 SDS 丢失时也会形成风暴。200 个请求同时读 cnt:post:123 - 都发现 SDS 不存在 - 都去 BITCOUNT 20 个 Bitmap 分片 - 200 * 20 4000 次 BITCOUNT解决思路是控制重建并发RRateLimiterlimiterredisson.getRateLimiter(rebuild:post:postId);limiter.trySetRate(RateType.OVERALL,1,1,RateIntervalUnit.SECONDS);if(!limiter.tryAcquire()){returnfallbackValue;}longtrueCountbitcountAllChunks(postId);writeSnapshot(postId,trueCount);Redisson 的tryAcquire()有许可就返回 true没有许可就立即返回 false。它适合重建场景因为我们不希望线程都阻塞等待。再加指数退避第一次被限流1 秒后再试 第二次被限流2 秒后再试 第三次被限流4 秒后再试 最多退避到 8 秒这样可以把重建风暴压成“一个请求执行其他请求分散尝试”。11. 面试时怎么讲这套方案推荐用这条链路MySQL UPDATE - 热点行锁竞争 Redis INCR - 性能好但幂等没解决 Bitmap Lua - 状态切换原子化 Kafka 聚合 - 高频写削峰 SDS 快照 - 批量读计数 Caffeine Redis MySQL - 热点读分层缓存 单飞锁 限流退避 - 防缓存击穿和重建风暴常见追问追问回答重点为什么不用 MySQL UPDATE热点行锁竞争幂等也没解决为什么不用 Redis INCR性能解决了用户状态没解决Kafka 重复消费怎么办Bitmap 是事实层快照可以重建Caffeine 多实例不一致怎么办L1 只做短 TTL 热点保护单飞锁为什么不用分布式锁本地合并成本低已能显著降压SDS 丢了怎么办限流后从 Bitmap BITCOUNT 重建热点 Key 过期怎么办动态 TTL、单飞锁、限流、退避一起用12. 最小落地方案一个可落地、可面试讲清楚的 Java 版本状态型点赞/收藏Bitmap 分片 Redis Lua。异步计数Kafka 发送 deltaConsumer 聚合后写 SDS。批量读计数pipeline GET 多个 SDS。信息流读缓存Caffeine 短 TTL Redis 分页缓存 MySQL。热点详情保护ConcurrentHashMap CompletableFuture合并 miss。重建保护Redisson 限流 指数退避。监控指标缓存命中率、热点 Key、Kafka lag、重建次数、BITCOUNT 耗时、DB 回源量。总结从直播间 500 万点赞看后端最重要的不是某一个中间件而是热点互动的分层设计状态用 Bitmap 兜住 计数用 Kafka 削峰 快照用 SDS 加速 读取用三级缓存分层 miss 用单飞锁合并 重建用限流和退避保护热点事件最怕的不是 QPS 高而是所有请求都集中在同一个对象上。Java 后端真正要做的是让这个热点对象不会变成数据库热点行、Redis 热点 Key、Kafka 积压点和缓存重建风暴。参考资料新浪新闻王鹤棣雅迪直播的人气数据具体是多少有具体截图吗 https://k.sina.com.cn/article_7879776328_1d5abd84806801lpeu.html?fromentsubchoent新浪娱乐王鹤棣直播人气 https://ent.sina.cn/2026-05-25/detail-inhzawmn3615170.d.htmlRedis 官方文档Cache-aside https://redis.io/docs/latest/develop/use-cases/cache-aside/Caffeine WikiSpecification https://github.com/ben-manes/caffeine/wiki/SpecificationRedisson JavadocRRateLimiter https://javadoc.io/static/org.redisson/redisson/3.10.7/org/redisson/api/RRateLimiter.htmlSpring Kafka 文档Message Listener Containers / Committing Offsets https://docs.spring.io/spring-kafka/reference/3.3-SNAPSHOT/kafka/receiving-messages/message-listener-container.htmlConfluent 文档Kafka Consumer Offset Management https://docs.confluent.io/cloud/current/client-apps/consumer.html