Skip to main content

全栈面试高频与难点解析

前端转全栈的面试,面试官通常不再局限于 API 的使用,而是更看重你的系统设计能力边界条件处理以及线上问题的排查经验

以下是全栈/Node.js 面试中区分度较高的几个难点问题。

1. 场景设计与高并发篇

💡 核心考察点:应对大流量、保证数据一致性、防范恶意请求。

❓ 题目 1:如何设计一个“秒杀”系统?

场景:1000 个库存的商品,瞬间涌入 10 万人抢购,如何保证不超卖,且系统不崩溃?

解答思路: 不能让 10 万个请求直接打到 MySQL。

  1. 前端拦截:按钮点击后置灰,限制请求频率;必须输入图形验证码防机器刷单。
  2. Nginx 层限流:配置 limit_req,丢弃绝大部分明显恶意的突发流量。
  3. Redis 预扣减(核心)
    • 活动开始前,将库存数量(1000)加载到 Redis 中。
    • 请求到达 Node.js 后,使用 Redis 的 DECR 命令原子扣减库存。如果返回值 < 0,说明没抢到,直接返回失败。
    • 如果 >= 0,说明抢到了,立刻将该用户放入消息队列(MQ)。
  4. MQ 异步写库:后台消费者慢慢从 MQ 取出成功抢到的用户,去 MySQL 生成实际订单(削峰填谷)。

❓ 题目 2:如何实现单点登录 (SSO)?

场景:公司有系统 A 和系统 B,用户在系统 A 登录后,访问系统 B 就不需要再次登录了。

解答思路

  1. 认证中心 (CAS):建立一个独立的认证服务器(如 auth.company.com)。
  2. 登录系统 A:用户访问 A,未登录,跳转到 CAS。在 CAS 登录成功后,CAS 在自己域名下种下全局 Cookie(包含 Token),并携带 Ticket 重定向回系统 A。系统 A 用 Ticket 去 CAS 验证换取 Token。
  3. 登录系统 B:用户访问 B,未登录,跳转到 CAS。因为之前在 CAS 种过 Cookie,CAS 发现该用户已登录,直接携带 Ticket 重定向回系统 B。B 验证 Ticket 成功,实现免密登录。

❓ 题目 3:如何保证接口的幂等性?

场景:用户点击“支付”按钮,因为网络卡顿连点了三次,如何保证只扣一次钱?

解答思路幂等性是指:同一个请求发起一次和发起多次,对系统的影响是一样的。

  1. 唯一流水号 (Token 机制):前端在提交前,先向后端请求一个唯一的 Token(通常存 Redis)。提交支付时带上这个 Token,后端执行 SETNX,成功则处理,失败则说明是重复提交。
  2. 数据库唯一索引:给订单表加上 order_id 的唯一索引,重复插入会直接报错。
  3. 状态机防重:在支付逻辑前,先判断订单状态是否已经是“已支付”,如果是则直接返回成功。

2. Node.js 底层与线上排查篇

💡 核心考察点:对 Node.js 单线程的理解、内存管理、故障排查能力。

❓ 题目 4:Node.js 线上 CPU 飙升到 100%,怎么排查?

解答思路: Node.js 是单线程的,CPU 飙升通常是因为遇到了死循环或者密集的同步计算(如巨大的正则匹配、巨量 JSON 的序列化)。

  1. 定位进程:使用 tophtop 找到 CPU 占用高的 Node.js 进程 ID。
  2. 抓取 CPU Profile:使用 v8-profiler 或 Node.js 自带的 inspector 模块生成 .cpuprofile 文件。
  3. 火焰图分析:将生成的 Profile 文件导入 Chrome DevTools 的 Memory 面板,查看火焰图(Flame Graph),寻找横向最宽的那个函数,就是吃 CPU 的罪魁祸首。

❓ 题目 5:Node.js 内存泄漏怎么排查?

场景:系统运行几天后,内存占用越来越高,最终导致 OOM (Out Of Memory) 崩溃。

解答思路: 常见原因:全局变量未清理、闭包引用导致对象无法回收、事件监听器未移除(Event Emitter Leak)。

  1. 监控告警:通过 Prometheus + Grafana 发现内存呈阶梯状上升,且 GC 后降不下来。
  2. 打快照 (Heap Snapshot):在内存正常时打一个快照 A,在内存飙升时打一个快照 B。可以使用 heapdump 模块。
  3. 对比分析:将两个快照导入 Chrome DevTools,使用 Comparison(对比) 视图。重点关注那些在快照 B 中新增、且大小巨大的对象(通常是 Array 或 Object),沿着 Retainers 树往下找,看看是被哪个全局变量或闭包持有了。

❓ 题目 6:多进程下(如 PM2 Cluster),如何共享 WebSocket 连接?

场景:Node.js 起了 4 个进程。用户 A 连到了进程 1,用户 B 连到了进程 2。此时进程 1 要向用户 B 发送消息,怎么做?

解答思路: 进程之间内存是隔离的,进程 1 根本不知道用户 B 连在进程 2 上。

  1. 引入 Redis Pub/Sub:使用 socket.io-redis 适配器。
  2. 当进程 1 要给 B 发消息时,它向 Redis 发起广播(Publish)。
  3. 所有进程都订阅(Subscribe)了 Redis,进程 2 收到广播后,发现用户 B 在自己这里,于是通过自己维护的 WebSocket 连接把消息发给 B。

3. 数据库与事务篇

💡 核心考察点:数据库底层原理、复杂业务场景下的数据一致性。

❓ 题目 7:MySQL 事务的隔离级别有哪些?会带来什么问题?

解答思路: 从低到高:

  1. 读未提交 (Read Uncommitted):引发脏读(读到了别人还没提交的 rollback 的数据)。
  2. 读已提交 (Read Committed):解决脏读。引发不可重复读(同一个事务内,两次查询同一条数据结果不一样)。
  3. 可重复读 (Repeatable Read):MySQL 默认级别。解决不可重复读。引发幻读(同一个事务内,两次范围查询,第二次查到了别人新插入的数据)。注:MySQL 的 InnoDB 引擎通过 MVCC 在很大程度上解决了幻读。
  4. 串行化 (Serializable):强制所有事务排队执行,性能最差,最安全。

❓ 题目 8:千万级大表如何做分页查询? (LIMIT 10000000, 10)

场景:查询非常靠后的页码时,接口极其缓慢。

解答思路: MySQL 在执行 LIMIT offset, N 时,会把前 offset 条记录全找出来,然后再丢弃,非常慢。

  1. 子查询/延迟关联优化:先利用覆盖索引查出对应页的主键 ID(极快),然后再拿 ID 去连表查详情。
    SELECT a.* FROM table a INNER JOIN (SELECT id FROM table WHERE condition LIMIT 1000000, 10) b ON a.id = b.id;
  2. 游标分页(推荐):如果不要求跳页(只能点“下一页”),可以记住上一页最后一条记录的 ID。
    SELECT * FROM table WHERE id > last_page_max_id LIMIT 10;
  3. 如果是搜索场景,建议直接将数据同步到 Elasticsearch,用 ES 做全文检索和分页。