映客直播实习一面面经
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 讲讲你的实习经历。
- ZSet 的 member 可以存什么类型的数据结构?
- 为什么这里要用 Lua 保证原子性,不能 ZSet 取出删除的同时把值返回吗?
- Set 插入失败、消息丢失有想过补偿方案吗?如何补偿?
- 在地址栏输入 URL 到页面展示,中间经历了什么?
- TCP 三次握手和四次挥手的过程?
- GET 和 POST 的区别?
- GET 和 POST 携带的内容大小有区别吗?
- POST 携带的请求体有大小限制吗?
- 说一下 HTTPS,与 HTTP 的区别?
- 说一下你对 MySQL 的理解,知道的都可以讲。
- 索引建越多越好吗?
- 怎么分析索引?
- 覆盖索引是什么?
- 什么情况不符合最左前缀匹配?
- BC 会走索引吗?
- MySQL 执行 delete 之后资源还在磁盘上吗?应该如何清理?
- 100 万的表里某个值占了 50 万行,会走索引吗?
- Redis 的数据结构有哪些?
- 如何利用 Redis 设计一个按钮防抖?
- 基于 Redis 的分布式锁如何实现?
- ZSet 或者 Hash 能够实现「某个值不存在才 set」的操作吗?如何实现?
- 如何实现排行榜?
- 你这种方式不会出现精度丢失的风险吗?
- 平时如何 AI Coding?
- 大模型怎么知道你项目的代码风格以及分层?
《参考解析》
1. ZSet 的 member 能存什么:Redis 的 key 和 value 都是二进制安全的字符串(SDS),所以严格说 ZSet 的 member 只能是字节串。业务上把对象塞进去有两种做法:一是自己序列化成 JSON/Protobuf/MessagePack 再当 member;二是用「前缀 + 业务 ID」拼成字符串,只把 member 当索引,真正的对象另存 Hash。要注意 member 在同一个 key 内唯一,score 才允许重复,所以「同一对象多条记录」必须靠 member 里带上区分维度(时间戳、场次)来保证唯一。底层编码随规模切换:元素少且短时是 listpack(老版本 ziplist),超过阈值(默认 128 个元素或 64 字节长度)会转成 skiplist + dict,这个转换不可逆,且大 ZSet 上做 ZRANGE 全量会阻塞主线程,要用 ZSCAN 或分页。
2. 为什么用 Lua 保证原子性,以及插入失败怎么补偿:ZSet 的 ZPOPMIN/ZMPOP 确实是「弹出即删除」,单条命令本身就原子,也能拿到 member。但业务里往往不止一步:先看分数是否达标、再决定弹不弹、弹出后还要写回失败集或计数,这一串在多客户端并发下会互相穿插(读到的状态可能已经过期),所以要用 Lua 把「校验 + 弹出 + 回写」打包成一次原子执行;Redis 6.2 之后也可以考虑用 ZMPOP 减少脚本依赖,但多条件判断仍要脚本。补偿方面,如果是消费者取走后写库/发消息失败,可靠做法是本地消息表或失败队列:Lua 里在弹出时同步写一条「处理中」记录(或用 Stream 的 pending list + XACK),业务侧处理成功后确认,定时任务扫描超时未确认的记录重投,并且消费端必须做幂等(业务唯一键去重)。Set 插入失败同理——先落本地表再异步重试,而不是指望一次成功。
3. 从地址栏输入 URL 到页面展示:先做 URL 解析与 HSTS/缓存判断;查浏览器 DNS 缓存、hosts、系统缓存,未命中才递归查询 DNS 拿到 IP;建立 TCP 连接(三次握手),HTTPS 再叠加 TLS 握手(证书校验、密钥协商);发出 HTTP 请求,中间可能经过 CDN、Nginx/网关、负载均衡到达应用服务,服务端处理后返回响应;浏览器拿到 HTML 后开始解析,边解析边构建 DOM 与 CSSOM,遇到 JS 会阻塞(defer/async 除外),合成渲染树后布局、绘制、合成上屏;期间还会并发请求 CSS/JS/图片等子资源,走强缓存/协商缓存(Cache-Control、ETag)。面试里常被追问的点是 DNS 与 TCP/TLS 的耗时占比,以及首屏优化从哪一环下手。
4. TCP 三次握手与四次挥手:握手是 SYN → SYN+ACK → ACK,三次而不是两次的根本原因是双方都要确认「自己的发送能力和对方的接收能力」,且要同步初始序列号、防止历史连接的过期 SYN 造成错误的半开连接。挥手是 FIN → ACK → FIN → ACK,因为 TCP 是全双工,一方不再发数据不代表另一方也没有数据要发,所以被动方的 ACK 与 FIN 通常不能合并,需要四次;主动方最后进入 TIME_WAIT,等待 2MSL 以确保最后的 ACK 能到达、并让本次连接的残留报文在网络中消散,避免影响使用相同四元组的新连接。TIME_WAIT 过多时可以开 tcp_tw_reuse、扩大端口范围,但不要动 tcp_tw_recycle。
5. GET 与 POST 的区别,以及请求体大小限制:语义上 GET 是安全且幂等的「取」,POST 是「提交」,会改状态、不幂等;GET 参数放在 URL query 里,POST 放在请求体。但从协议层面看,两者都是 HTTP 请求方法,报文结构一致,GET 也能带 body(只是没有约定语义,很多实现会忽略)。GET 的长度限制来自实现而非协议:浏览器与 Nginx 对 URL 长度有限制(Nginx 默认 large_client_header_buffers 32k 左右),超过会返回 414。POST 请求体在 HTTP 协议里没有大小上限,实际限制来自 Nginx 的 client_max_body_size(默认 1M)、Tomcat 的 maxPostSize/max-http-form-post-size、框架自身的 multipart 配置等,超限会返回 413。所以答案要落在「协议无限制、链路各层有限制」,并说出常见的几个配置项。
6. HTTPS 与 HTTP 的区别:HTTPS 是 HTTP over TLS,默认 443 端口,在 TCP 之上加了一层 TLS,提供机密性、完整性与身份认证。握手阶段客户端发 ClientHello(支持的版本、密码套件、随机数),服务端回 ServerHello 并出示证书链;客户端校验证书有效期、域名匹配、签发链与吊销状态,验证通过后用非对称算法(ECDHE)协商出共享密钥,之后的通信全部用对称加密(AES-GCM/ChaCha20)保证性能和完整性。TLS 1.3 把握手压到 1-RTT 并支持 0-RTT 恢复,去掉了 RSA 密钥交换等不安全套件。代价是握手延迟与加解密开销,所以有会话复用、TLS 终止在网关、HTTP/2 多路复用等优化。中间人抓包时 HTTPS 只能看到 SNI 与流量特征。
7. 索引是不是越多越好,以及怎么分析索引:不是。每个二级索引都要单独占空间,且任何一次写入都要同步维护所有索引——写放大、页分裂、redo/binlog 体积都会增加;索引太多还会让优化器在候选方案上花更多时间、甚至选错。判断一个索引该不该建要看:它是否真的被高频查询命中、选择性是否足够高(区分度大)、是否与已有索引前缀重复((a,b) 已覆盖 (a))。分析手段是 EXPLAIN(重点看 type、key、key_len、rows、filtered、Extra),慢查询日志 + pt-query-digest 找top SQL,performance_schema 或 sys.schema_unused_indexes 查从未被使用的索引,再用 force index 做对照实验验证。核心指标是扫描行数与返回行数的比值,越接近 1 越好。
8. 覆盖索引、最左前缀,以及 BC 走不走索引:覆盖索引指查询需要的列全部能从索引里拿到,不需要回表,EXPLAIN 的 Extra 会显示 Using index,代价是索引要容纳更多列、体积变大。最左前缀指联合索引 (a,b,c) 只能从最左列开始连续匹配:where a=?、a=? and b=?、a=? and b=? and c=? 都能用;where b=?、where c=? 用不上;where a=? and c=? 只能用到 a,c 用不上(5.6 以后有索引下推,c 可以在存储引擎层过滤,但不算用于定位)。至于 BC 走不走索引,取决于索引定义:如果建的是 (b,c) 或 (b,c,…),where b=? and c=? 可以完整使用;如果建的是 (a,b,c),只查 b、c 则不符合最左前缀,通常走全表扫描或全索引扫描。另外范围查询(>、<、between、like ‘x%‘)会使它右边的列失去索引定位能力。
9. delete 之后数据还在磁盘上吗:InnoDB 的 delete 是标记删除,只在页里把记录打上删除标记并维护版本链,空间不会立刻还给操作系统。页内空洞靠后续插入复用,整页空了会被标记可复用但数据文件(表空间)通常不会自动缩小;如果开了 innodb_file_per_table,可以用 OPTIMIZE TABLE 或 ALTER TABLE ... ENGINE=InnoDB 重建表回收空间(本质是建新表再搬迁,期间会锁表或需要 online DDL 支持),大表要用 pt-online-schema-change/gh-ost 平滑操作。删除全部数据且要快速回收时 TRUNCATE 比 DELETE 高效得多。还要注意 delete 不会减少 undo/redo 的历史占用,长事务不提交会让 purge 无法清理,这是磁盘暴涨的常见原因。
10. 100 万的表里某值占 50 万行,会走索引吗:大概率不走,即使这个列上有索引。优化器是基于代价估算的:当条件命中的行数占到全表 30% 甚至 50% 时,走二级索引意味着要先扫索引、再对命中的 50 万行逐行回表(大量随机 I/O),代价远高于直接全表顺序扫描所有聚簇索引页;所以 EXPLAIN 很可能显示 type=ALL。想让优化器改主意可以:建覆盖索引避免回表(Extra 出现 Using index 时走索引的概率大增)、用 force index 验证、或者从业务上避免这类查询(改成范围查询、按时间分片、预聚合、加组合条件提高选择性)。要明白索引不是「建了就一定用」,最终决定权在优化器的成本模型上。
11. Redis 的数据结构,以及怎么用 Redis 做按钮防抖:基础类型五种:String(缓存、计数器、分布式锁)、Hash(对象、大 key 风险高)、List(队列、栈)、Set(去重、交并集)、ZSet(排行榜、延时队列);进阶结构有 Bitmap(签到、统计活跃)、HyperLogLog(基数估算)、GEO(附近的人)、Stream(消息队列,支持消费组与 ACK)。按钮防抖的本质是「同一用户 + 同一操作在时间窗内只允许一次」,最直接的是 SET key value NX PX 3000(key 形如 debounce:order:{userId}),抢到就放行、抢不到直接返回「操作太频繁」;剩余时间可用 PTTL 返回给前端做倒计时。如果业务需要放开粒度,就把 key 加上接口名或业务单号。要小心的是超时误伤(业务没做完锁就过期)以及释放锁时误删别人的锁——释放要用 Lua 比对 value,或者干脆让 key 自然过期。
12. Redis 分布式锁的实现与「不存在才 set」:正确姿势是 SET lock:xx <唯一随机值> NX PX 30000,value 用 UUID/线程标识保证只有持有者能释放;释放时必须用 Lua 校验 value 再 DEL,避免超时后误删他人锁。业务执行时间可能超过锁租期,就要有看门狗续期(Redisson 的 watchdog 默认每 10 秒续到 30 秒);仍要承认单实例主从切换会丢锁,强一致场景要么用 Redlock(多节点多数派,但争议大),要么在业务层加幂等与版本号兜底。至于「某个值不存在才 set」:String 用 SETNX,Hash 用 HSETNX(对 field 粒度),ZSet 没有 HSETNX 那样的原生命令,可以用 ZADD 的 NX 选项(只新增不更新,成员已存在则不做任何事),或者用 Lua 包 ZSCORE 判断后再 ZADD;List 侧可用 LPUSH 前 EXISTS,归根到底原子性都要靠单命令或 Lua 保证。
13. 排行榜实现与 score 精度风险:常规做法是 ZSet:ZINCRBY rank:20260922 <delta> <userId> 更新分数,ZREVRANGE key start stop WITHSCORES 取榜,需要「同分先到先得」时把时间戳编进 member 或用 ZREVRANGEBYSCORE ... LIMIT 配合二次排序,也可以用 ZSet 的 lex 序在同一个 score 下按 member 排。分页用 ZREVRANGE 的区间或 ZRANGEBYSCORE + LIMIT,别用 ZRANGE 全量再切。规模大时按维度或时间分片(日榜/周榜),把热数据留在内存、冷数据落 Hive/OLAP。精度风险确实存在:score 是 IEEE754 double,只有 53 位能精确表示整数,约 9×10^15。像「秒级时间戳 × 10^6 + 用户权重」这种编码在 1.7×10^12 × 10^6 时就会丢精度;常见做法是把 score 控制在安全范围内(例如用毫秒时间戳、或用整数分 + 小数点后两位),或者把时间戳与分数拆成两个 key/两段处理,必要时改用 String 存 JSON 或换用支持精确小数的结构。
14. 平时怎么 AI Coding,大模型怎么知道你的代码风格与分层:把 AI 当结对工程师用:先给上下文再让它写,改完自己 review 并跑测试。让模型理解项目约定的有效手段是「把规范写进仓库」而不是每次口头描述——在根目录放 AGENTS.md/CLAUDE.md 之类的说明文件,写清目录职责、分层规则、命名与错误处理约定、禁止事项,再用 .editorconfig、ESLint/Prettier、Checkstyle 这类工具把能自动化的部分强制掉,模型看到配置文件自然会遵守。分层这类隐式约定最好配一两个「范例文件」作为 few-shot 参照,让它照抄结构而不是自创。实践上还要给它反馈回路:编译报错、单测结果、类型检查输出都回灌给它,比反复提示词有效;生成的东西必须过一遍 lint 与 code review 才能进主干,不要直接把大段生成代码提交上去。