面灵AI→

映客直播实习一面面经

轮次
一面
时间
2026-09
来源
牛客网

《面试题目》

  1. 讲讲你的实习经历。
  2. ZSet 的 member 可以存什么类型的数据结构?
  3. 为什么这里要用 Lua 保证原子性,不能 ZSet 取出删除的同时把值返回吗?
  4. Set 插入失败、消息丢失有想过补偿方案吗?如何补偿?
  5. 在地址栏输入 URL 到页面展示,中间经历了什么?
  6. TCP 三次握手和四次挥手的过程?
  7. GET 和 POST 的区别?
  8. GET 和 POST 携带的内容大小有区别吗?
  9. POST 携带的请求体有大小限制吗?
  10. 说一下 HTTPS,与 HTTP 的区别?
  11. 说一下你对 MySQL 的理解,知道的都可以讲。
  12. 索引建越多越好吗?
  13. 怎么分析索引?
  14. 覆盖索引是什么?
  15. 什么情况不符合最左前缀匹配?
  16. BC 会走索引吗?
  17. MySQL 执行 delete 之后资源还在磁盘上吗?应该如何清理?
  18. 100 万的表里某个值占了 50 万行,会走索引吗?
  19. Redis 的数据结构有哪些?
  20. 如何利用 Redis 设计一个按钮防抖?
  21. 基于 Redis 的分布式锁如何实现?
  22. ZSet 或者 Hash 能够实现「某个值不存在才 set」的操作吗?如何实现?
  23. 如何实现排行榜?
  24. 你这种方式不会出现精度丢失的风险吗?
  25. 平时如何 AI Coding?
  26. 大模型怎么知道你项目的代码风格以及分层?

《参考解析》

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 才能进主干,不要直接把大段生成代码提交上去。