映客直播后端一面:广告召回、MySQL、Redis 与秒杀设计
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 你做的广告召回数据结构优化,难点在哪里?
- 使用位图(Roaring Bitmap)的时候踩到什么坑,除了刚才说的数据不连续问题之外?
- 你们这个广告召回的数据量级有多大?
- 你们 Redis 是使用的集群吗?
- 候选召回是跑定时任务还是异步的?
- 你这边做的预存,是有服务去拉取结果,还是主动推给你存入存储里面?
- 针对接近千万级的数据量,是怎么去同步的?是一次性跑出来后续增量同步,还是定时全量刷新?
- 定时刷新一次大概要跑多少数据?跑完的数据是直接往 Redis 里面写吗,还是怎么写的?
- 存储到内存里,如果服务重启了数据不就丢失了吗?还是说使用了类似 Redis 的外部存储介质?
- 一两百万的数据,拉取并写入内存大概需要多久?拉取数据是直接跑表,还是从哪里拉取?
- 服务不重启一直往内存写的话,过期数据有进行删除复用的操作吗?
- 你日常开发是怎么开发的?纯手敲还是用 AI 进行辅助开发?
- 如果让你使用 AI 进行开发,你的具体流程是怎么做的?
- 你怎么去保证 AI 生成的代码不会出现”漂移”,比如与预期不符、代码风格与原有工程不一致、引入业务逻辑错误?
- 你现在 AI 开发就只有 Cursor 吗?有用过其他工具开发吗?结合使用体验你觉得哪个更好用一点?
- 你能完整描述一下一个 HTTP 请求的全流程吗,从浏览器点击链接到请求发起再到结束?
- 在业务开发中,GET 和 POST 一般出于什么样的使用场景?怎么区分使用?
- GET 和 POST 两者之间的本质区别是什么?请求参数有区别吗?请求长度与大小有区别吗?
- POST 请求就一定安全吗?不能用 GET 请求进行敏感数据操作吗?
- 如果要让接口请求做到绝对安全,在工程上一般会怎么去做?
- 业务中有接触过三方业务接口(如支付宝、微信支付接入)的数据签名与加密防篡改逻辑吗?
- MySQL 为什么常用 B+ 树作为索引结构,而不是普通二叉树或 B 树?
- MySQL 里面的索引类型有哪几种?
- 针对联合索引(如 A、B、C 三列建索引),如果查询条件只用 A 和 C,能走索引吗?如果只用 B 和 C 呢?
- 创建表时,索引是建得越多越好吗?索引过多会有什么危害,在存储空间等方面有什么影响?
- 如果一张表有上千万甚至上亿的数据,需要删除一部分历史无用数据并释放磁盘空间,你在实际操作中会怎么做?直接执行 delete 能释放磁盘空间吗?
- 事务的 ACID 原则分别是指什么?常见的隔离级别有哪几种?
- Redis 里面常见的数据结构有哪些?
- 如果用 Redis 的 String 结构去实现一个分布式锁,具体怎么实现?
- 如果让你基于 Redis 做一个排行榜,你会怎么实现?
- 排行榜同分排序问题:A、B 两个用户积分相同,要求先达到该积分的用户排在前面,ZSet 该如何设计 score 值来保证先来后到?
- 如果在送礼榜单中面临高并发写入(不能直接简单覆盖 score),如何保证 score 计算的准确性与并发安全?
- 什么是缓存穿透、缓存击穿、缓存雪崩?各自有什么成熟的解决方案?
- 针对热点数据击穿,如果把 Key 设置为永不过期,随着系统运行不过期的 Key 越来越多导致内存耗尽怎么办?
- SingleFlight 机制是只能在单台机器上使用,还是多台机器(分布式)都可以用?
- Redis 内部针对过期 Key 的清除机制(过期策略)是什么?
- Redis 的持久化方式有几种,AOF 与 RDB 的区别是什么?如果机器突然宕机,刚才写入的数据还能恢复吗?
- 内存淘汰策略中的 LRU 和 LFU 分别是什么原理,两者的区别是什么?
- Kafka 架构中包含哪几种核心角色?从生产到消费的完整链路流程是怎样的?
- 一个消费者组(Consumer Group)可以同时订阅多个 Topic 吗?
- Kafka 在消费时能保证消息绝对有序吗?具体怎么通过 Partition 保证有序?
- 在消费端消费消息时,是拿到数据就直接提交 Offset,还是等业务逻辑全部处理完成后再提交?
- 如果业务耗时较长、长事务导致长时间不提交 Offset,或者突发流量引发消费积压,在机器资源受限的情况下该怎么解决堆积问题?
- 如果让你设计一个秒杀系统,面对 100 件库存与上万人抢购,你的架构链路、表结构和缓存预热该怎么设计?
- 秒杀系统设计需要重点考虑的核心关键点有哪些?
- 用户秒杀成功创建订单时,如何保证防重与幂等,防止用户狂点按钮生成多个订单?
- 如果下沉到底层数据库表,该如何设计字段和唯一索引来防止重复下单?
- 订单支付环节如何保证支付幂等,避免多次扣款?
- 在更新数据库订单或库存状态时,怎么通过 UPDATE 语句的执行结果(如受影响行数)来判断状态流转是否成功?
《参考解析》
MySQL 为什么用 B+ 树做索引:B+ 树是多路平衡树,非叶子节点只存键和子节点指针、不存数据行,所以同样大小的页能塞下更多键值,扇出大、树高很低——千万级数据通常三层就够,一次查询最多三次磁盘 IO;叶子节点之间用双向链表串起来,范围查询和 ORDER BY 可以直接顺序扫描,不需要回溯。对比之下,普通二叉树高度太高、每个节点一次随机 IO,而且退化时会变成链表;B 树把数据分散在所有节点上,同页扇出更小,范围查询还要中序遍历来回跳。真正落到 InnoDB 还要往前一步:聚簇索引的叶子存整行数据,二级索引的叶子存主键值,查非索引列要回表,所以主键要短、最好自增(避免页分裂和随机插入),这也解释了覆盖索引为什么能省一次回表。
缓存穿透、击穿、雪崩与永不过期:穿透是请求查一个数据库里根本不存在的 Key,每次都落到 DB——解法是缓存空值并给短 TTL、前置布隆过滤器拦截、接口层做参数合法性校验。击穿是某个热点 Key 过期瞬间并发全部打到 DB——解法是逻辑过期或永不过期加后台异步刷新、重建时加互斥(本地 SingleFlight 或分布式锁)、以及热点数据预热。雪崩是大量 Key 同时过期或缓存集群整体故障——解法是 TTL 加随机抖动打散、多级缓存兜底、限流降级熔断、集群高可用。永不过期导致内存耗尽这一问,答案是”永不过期”只能限定在真正的小热点集合上,并且要配套治理:value 里带逻辑过期时间按需后台刷新、定期扫离线 Key 清理、用 LRU/LFU 淘汰兜底、监控内存与命中率。SingleFlight 的本质是进程内合并并发请求,单机天然可用;跨机器要用分布式协调(Redis 锁或中心化合并服务)才等价,并且要注意合并后错误如何传播、超时怎么算。
秒杀系统的防重与幂等:链路设计的原则是层层过滤——页面静态化加 CDN 挡掉绝大部分流量,网关限流加验证码、答题、风控挡脚本,库存预热到 Redis,扣减用 Lua 脚本把”判断加扣减”做成一次原子操作,抢到的人拿到令牌再生成订单、异步落库,前端轮询或推送结果,数据库只承接和库存同量级的写。防重不能只靠按钮置灰,服务端要按”用户 + 活动”做唯一约束:请求携带幂等 token 或服务端生成唯一订单号,数据库用唯一索引(如 user_id + activity_id)兜底,插入冲突就当重复下单直接返回失败。支付幂等是另一层:业务订单号和支付流水号各建唯一约束,回调处理先查状态再推进,状态机只允许单向流转且必须可重入,因为支付平台的回调一定会重复推送;扣款与订单状态更新各自幂等,用本地消息表加重试保证最终一致。判断状态流转是否成功要看 UPDATE 的受影响行数——update ... set status=2 where id=? and status=1 影响一行才算抢到,影响零行说明已被别人处理,这是典型的乐观并发控制,比”先查再改”安全得多。
ZSet 排行榜的同分排序与并发写:ZSet 的 score 是 double,只有 53 位有效整数精度,所以把”积分 + 时间”编码进一个 score 时要先算好位数:让积分占高位,时间戳取反(或用一个大常数减去毫秒时间戳)占低位,这样积分高者靠前、同分时先到者的 score 更靠前。做法上通常取 score = 积分 * 2^K + (MAX_TS - 时间戳),K 要保证低位放得下时间范围且不越过精度上限,超出精度就得换方案(用整数编码的成员名再加一层排序索引,或者把同分名次交给应用层处理)。并发写的关键是绝不”读出来改完写回去”,那是典型的丢更新:累加用 ZINCRBY 原子完成,需要”先判断再更新”的逻辑用 Lua 脚本打包成一次原子执行,或者按用户 ID 分片把同一用户的写操作路由到单个线程队列串行处理再合并。榜单还能分层:实时增量榜写 Redis,全量榜定时重算,读时合并,既保准确又扛住峰值。
Roaring Bitmap 与内存驻留数据的同步:广告召回用位图是把”某个定向条件命中的广告集合”表示成位集合,位运算求交并差非常快;Roaring Bitmap 的改进是按高 16 位分桶,桶内根据稀疏程度自适应选择有序数组、位图或 run-length 编码,因此稀疏数据不会像裸位图那样浪费空间。踩坑点主要在写入与版本上:桶的容器类型会在增删中来回转换(array 到 bitmap 到 run),大量随机写比顺序写慢;序列化格式跨版本要兼容;不同机房、不同批次的数据必须带版本号。把结果常驻内存则有四个必须回答的问题:一是发布新版本时不能原地改,要双缓冲——新位图构建完成后原子换指针,读者无锁切换;二是重启要能恢复,从快照或 Redis 回灌并校验版本与校验和;三是增量同步要幂等且必须有全量兜底(定时全量刷新加增量补偿),只靠增量迟早会出现长期漂移;四是内存要有上限与淘汰——过期的定向条件及时删除、分片按需懒加载,否则内存只会单调上涨。一到两百万条量级的位图灌内存的时间主要取决于序列化体积和反序列化方式,可以按分片并行加载,并让冷分片懒加载而不是全部预热。