面试官:爆火视频点赞每秒 10w,数据库行锁直接卡死,你怎么破?
最近有个兄弟去面某短视频大厂,前面的架构设计聊得挺嗨,结果面试官最后抛出一个看似简单的“点赞”题,直接把他问到了自闭。
面试官:“现在有个爆火视频,比如春晚刘谦变魔术,一秒钟有 10 万人同时点赞。你代码里写 UPDATE video_statistics SET like_count = like_count + 1 WHERE video_id = 1001;,数据库会发生什么?”
兄弟:“那就加 Redis 缓存啊,异步写库。”
面试官冷笑:“如果这 10 万人都点赞同一个视频,Redis 的单 Key 会被打爆(Hot Key)吗?异步写库时,如果 MQ 消费者还是一条条更新,数据库的行锁排队能解决吗?”
这兄弟支支吾吾半天没答上来。其实,这道题考的不是“点赞”,而是高并发架构中极其硬核的“写热点(Write Hotspot)”问题。
今天 Fox 带你三步拆解,教你如何用架构师的思维,优雅地消除数据库行锁地雷。
第一招:青铜回答——直接操作数据库(自杀式打法)
很多人的第一反应是:数据库不是有索引吗?直接更新不就行了?
-- 看起来人畜无害的 SQL UPDATE video_statistics SET like_count = like_count + 1 WHERE video_id = 1001;Fox 深度解析:
这是典型的“行锁地雷”。在 InnoDB 引擎中,更新这一行必须获取排他锁(X 锁)。
当 10 万个请求同时涌入时,数据库内部会发生以下惨剧:
- 行锁竞争:
- 只有一个线程能拿到锁,其余 99,999 个线程全部进入等待队列。
- 上下文切换:
- 随着队列变长,操作系统的 CPU 资源会全部浪费在线程的唤醒和挂起上。
- 死锁检测:
- MySQL 内部的死锁检测机制会疯狂扫描这个巨大的等待队列,导致 CPU 瞬时飙升到 100%。
结论:在高并发面前,单行更新就是系统的死穴。此时的吞吐量(TPS)会掉到个位数。
第二招:黄金回答——Redis 计数 + 异步落库
稍微有点经验的会说:不能直接压数据库,得用内存挡一下。
- 方案:
- 利用 Redis 的 INCR 命令,点赞先在 Redis 里加 1,然后通过 MQ 异步同步回 MySQL。
- 瓶颈:
- 如果是超级热点(比如热搜第一),10w QPS 全部压在 Redis 的一个 Key 上,单机 Redis 吞吐量也会到极限。而且,如果 MQ 消费端依然是“拿一条消息、调一次 +1 库操作”,数据库的行锁压力依然没有任何缓解,只是把压力从前端推迟到了后端。
第三招:王者回答——多级分层聚合 + 批量写合并
真正的工业级方案,核心思想只有四个字:分而治之。
1. 接入层本地缓冲(Local Coalescing)
不要让每一个点赞都变成一次网络请求!
- 策略:
- 在 Web Server 的内存里,利用 AtomicLong 开辟一个 200ms 的时间窗口。
- 效果:
- 这 200ms 内的 500 次点赞在内存里直接合并成一个“点赞 +500”的请求,再发给后端服务。
2. Redis 热点 Key 拆分(Sharding)
模仿 Java 中 LongAdder 的设计思想。
- 策略:
- 将 video_id_9527 拆分成多个子 Key:count:9527:1、count:9527:2 ... count:9527:10。
- 分流:
- 请求按用户 ID 取模,分散到不同的 Key 和不同的 Redis 节点上。
- 查询:
- 查总数时,把所有子 Key 的值求和即可:
3. 消费端批量写合并(Batch Update)—— 绝杀行锁
这是解决数据库行锁的终极手段。
- 操作:
- MQ 消费者开启批量消费(一次取 500 条消息),在内存里按 video_id 进行聚合。
- 执行:
- 最终在数据库里只执行一条合并后的 SQL:
-- 将 500 次锁竞争转化为 1 次 SQL 执行 UPDATE video_statistics SET like_count = like_count + 500 WHERE video_id = 1001;由于加锁频率降低了三个数量级,数据库的行锁竞争瞬间消失,吞吐量原地起飞。
面试满分模板(直接背诵)
面试官:“如何设计一个支撑 10w QPS 点赞的热点系统?”
你:“我会采取‘层层聚合,异步合并’的架构:
- 端侧聚合:
- 利用本地内存做微小的窗口聚合(如 100ms),将高频散点请求转化为低频批量请求,减少 RPC 调用。
- 缓存分片:
- 针对热点 Key,采用Redis 计数器分片方案,将写压力分摊到多个分片,规避单点 Hot Key 问题。
- 批量写合并:
- 这是核心。通过 MQ 异步削峰,并在消费端实现批量聚合。将同一行数据的多次更新在内存中累加,最终以一次 UPDATE ... SET count = count + N 的形式更新数据库。
- 容灾兜底:
- 配合定期快照和 Binlog 日志,确保在追求极致性能的同时,满足最终一致性。”
写在最后
架构师的价值,就在于面对“物理限制”(如行锁、IOPS)时,能够利用内存和异步化手段把矛盾转移。
