跳到主要内容

服务端热门面试题

使用说明

本篇共 60 道题。

服务端题要回答可用性、一致性、安全和运维取舍。对全栈偏前端岗位,重点是能设计可靠 API 并理解调用链,而不是背中间件参数。

Q1: Nginx 在系统中通常承担什么职责?

答案

常见职责是反向代理、TLS 终止、静态资源、负载均衡、压缩、缓存和限流,也可统一路由多个后端服务。

它不是业务权限的唯一防线。配置要版本化、测试并监控连接、上游失败和延迟,避免成为单点。

  • 典型链路是客户端先到 Nginx,由它完成 TLS、静态资源和路由,再把动态请求转发给 Node/Java 等应用实例。
  • 生产上还要配置上游超时、连接复用、请求体限制、真实 IP 传递和健康检查;Nginx 自身也要多实例或由云负载均衡托管,避免单点。

Q2: 反向代理和 API Gateway 有什么区别?

答案

反向代理主要处理网络层面的转发、TLS、负载和缓存;API Gateway 还会承载认证、配额、协议转换、路由治理和可观测性等 API 管理能力。

边界要克制,复杂业务编排不应全部塞进网关,否则发布和故障影响面会过大。

  • 反向代理侧重 L4/L7 转发能力;网关面向 API 生命周期,通常理解用户身份、租户、配额、版本和协议。
  • 通用横切能力放网关,领域规则留在服务内。否则网关会成为所有团队共同发布的“超级业务服务”,故障半径和组织耦合都会增大。

Q3: 微服务适合什么场景?

答案

当系统和组织足够大,需要按领域独立开发、扩容和发布,且团队能承担分布式治理成本时适合。

代价包括网络失败、数据一致性、服务发现、观测和运维。业务边界不清时先做模块化单体通常更稳妥。

  • 判断信号包括领域边界清晰、团队需要独立发布、不同模块扩容曲线差异大,以及单体协作成本已经成为主要瓶颈。
  • 拆分前应先做好模块化、自动化测试、CI/CD 和可观测性;否则只是把进程内调用变成更不可靠的网络调用。

Q4: 认证和授权有什么区别?

答案

认证回答“你是谁”,授权回答“你能做什么”。登录、Token 验证属于认证,角色、资源归属和策略判断属于授权。

前端隐藏按钮不是授权,所有敏感操作必须服务端校验,并记录关键权限决策。

  • 认证通常产生可验证的身份上下文,授权再根据主体、动作、资源和环境做策略判断。二者都必须在可信服务端执行。
  • 每次敏感资源访问都要校验归属,不能只在登录时判断一次;权限变更、拒绝原因和管理员操作应进入审计日志。

Q5: RBAC 和 ABAC 怎么选?

答案

RBAC 按角色授予权限,简单易审计;ABAC 根据用户、资源、动作和环境属性计算策略,灵活但规则更复杂。

实际可组合:角色提供基础权限,属性处理资源归属和上下文。策略要有默认拒绝、测试和变更审计。

  • RBAC 适合权限相对稳定、以岗位角色为主的系统;ABAC 能表达“只能访问本部门、本人订单、工作时间内”等动态条件。
  • 常见落地是 RBAC 给粗粒度能力,ABAC/资源归属做细粒度限制。策略需要集中测试、默认拒绝,并防止角色爆炸和规则冲突。

Q6: 服务端缓存有哪些层次?

答案

可以有进程内缓存、分布式缓存、数据库缓存、反向代理/CDN 和客户端缓存。越靠近用户延迟越低,但一致性和失效更复杂。

先明确数据新鲜度、容量和失败策略,再选 Cache Aside 等模式;不要把缓存当主数据源。

  • 进程内缓存最快但不共享;Redis 等分布式缓存跨实例;反向代理/CDN 更靠近用户;客户端缓存还能完全消除请求。
  • 每层都要定义 key、TTL、容量、失效和降级策略。越靠近用户越难即时失效,敏感或强一致数据不能只依赖长 TTL。

Q7: 如何解决缓存穿透、击穿和雪崩?

答案

穿透是查询不存在数据,可做空值缓存、Bloom Filter 和请求校验;击穿是热点键过期并发回源,可互斥重建或逻辑过期;雪崩是大量键同时失效,可随机 TTL、多级缓存和限流降级。

所有方案都要设置上限,防止缓存保护机制本身拖垮系统。

  • 穿透针对不存在数据,可用参数校验、空值缓存或 Bloom Filter;击穿针对单个热点过期,可用 singleflight、互斥重建或逻辑过期;雪崩针对大量同时失效,要打散 TTL、限流和多级降级。
  • 还要给回源并发设上限并监控命中率、重建耗时,避免保护机制本身把数据库压垮。

Q8: 消息队列解决什么问题?

答案

它用于异步解耦、削峰、缓冲和事件分发,让生产者不必同步等待所有下游。

代价是延迟、一致性、重复、顺序和运维复杂度。消费者要幂等,消息要有重试、死信和可追踪状态。

  • 队列把生产者与耗时下游解耦,并在流量峰值时缓冲;它适合通知、异步处理和事件分发,不适合用户必须立即得到强一致结果的路径。
  • 引入后必须面对至少一次投递、积压、顺序、死信和 Schema 演进,因此消费者幂等和可观测性不是可选项。

Q9: 如何保证消息至少处理一次时不产生重复副作用?

答案

使用业务幂等键或消息 ID,在数据库唯一约束/幂等表中记录处理结果,并让状态变更与记录尽量处于同一事务边界。

“消费后再 ACK”仍可能在 ACK 前崩溃,所以不能依赖 Broker 保证业务恰好一次。

  • 以订单号、业务请求号或消息 ID 作为幂等键,通过数据库唯一约束原子地“查重并写入结果”;只在 Redis 里先查再做业务仍可能有竞态。
  • 外部副作用可使用状态机、Outbox 和可查询结果。重复消息应返回之前的处理结果,而不是简单报错。

Q10: RESTful API 应如何设计?

答案

围绕资源使用一致 URL 和 HTTP 语义,正确状态码,分页/过滤/排序规则统一,错误结构可机器处理,并对并发更新、幂等和版本演进有方案。

不要机械追求纯 REST;团队和客户端能稳定理解、缓存和演进更重要。

  • 资源命名、HTTP 方法和状态码保持一致,列表接口统一分页/过滤/排序,错误返回稳定 code、message 和 traceId。
  • 创建操作支持幂等键,并发更新可用版本号或 ETag;破坏性变更通过版本化或兼容字段渐进演进,同时维护 OpenAPI 契约。

Q11: 限流、熔断和降级分别是什么?

答案

三者保护的对象和触发条件不同:

  • 限流:入口流量超过系统可承受速率时,排队、拒绝或返回 429,防止资源被耗尽。
  • 熔断:下游持续超时或失败时,调用方暂时停止请求,快速失败;经过探测后再逐步恢复。
  • 降级:主动减少功能或一致性要求,例如返回缓存、关闭推荐、只保留核心下单链路。

常见链路是先限流控制总量,下游异常时熔断,熔断后执行降级。阈值应根据容量、错误率、延迟和 SLO 调整,并防止所有实例同时探测造成二次冲击。

Q12: 分布式系统为什么会出现部分失败?

答案

网络调用可能超时、丢包、重复或对端已成功但响应丢失,调用方无法仅凭超时判断真实状态。

因此需要幂等、截止时间、重试退避、状态查询和补偿。不能把远程调用当本地函数一样可靠。

  • 超时只表示调用方没收到确定结果:服务端可能未执行、执行中、已成功但响应丢失。盲目重试会把一次操作放大成重复副作用。
  • 因此远程调用要带截止时间和幂等键,重试使用指数退避与抖动,并提供状态查询或补偿;Trace 用来还原跨服务真实路径。

Q13: CAP 应该怎么解释?

答案

在发生网络分区时,分布式系统不能同时保证所有请求都获得最新一致结果和所有请求都持续成功,只能按业务选择一致性或可用性倾向。

CAP 讨论的是分区发生时的选择,不代表系统平时只能固定选两个字母。不同数据和操作可以采用不同策略。

  • CAP 的前提是网络分区已经发生,此时系统要么拒绝/等待部分请求以保持一致,要么继续响应但允许读到旧值。
  • 选择应落到具体操作:余额扣减偏一致,商品推荐偏可用。平时还要讨论延迟与一致性的 PACELC 取舍,而不是只背 CP/AP。

Q14: 定时任务如何避免重复执行?

答案

多实例环境用分布式锁、数据库抢占或队列调度确保同一任务被一个执行者领取,同时任务本身仍要幂等。

记录计划时间、执行状态、重试和结果,处理机器时钟、超时、补跑和锁过期,不能只依赖单机 cron。

  • 调度器可以用数据库 SELECT ... FOR UPDATE SKIP LOCKED、带租约的分布式锁或队列,让同一计划时间只被一个执行者领取。
  • 任务本身仍需幂等,并持久化计划时间、尝试次数和结果。租约要续期,失败要可补跑,长任务要防锁过期后被第二个实例重复执行。

Q15: 文件为什么常直接上传对象存储?

答案

服务端生成短期、受限的签名,客户端直传对象存储,可减少应用服务器带宽和内存压力,并利用分片与 CDN。

服务端仍要验证文件元数据、权限和最终状态,签名限制路径、大小、类型和有效期,上传后进行安全扫描。

  • 应用服务只生成限定对象 key、大小、类型和有效期的短时签名,浏览器分片直传对象存储,完成后再通知服务端确认。
  • 服务端不能仅相信客户端“上传成功”,应检查对象元数据并做病毒/内容扫描;未完成分片和孤儿对象还要有生命周期清理。

Q16: WebSocket 服务如何横向扩展?

答案

连接驻留在具体实例,跨实例广播和用户路由需要 Redis Pub/Sub、消息系统或专用网关协调,并处理连接注册、心跳和离线状态。

负载均衡、重连、消息顺序、背压和慢消费者都要设计,不能只把 HTTP 服务端口升级成 WebSocket。

Q17: gRPC 相比 REST 有什么特点?

答案

gRPC 基于强类型 IDL 和 HTTP/2,支持代码生成、双向流和高效二进制传输,适合内部服务通信;REST/JSON 对浏览器和外部生态更直观。

选择要考虑调试、网关、浏览器支持、版本兼容和团队语言,不是只比较包体。

  • gRPC 用 Protobuf IDL 提供强类型契约和代码生成,支持 unary、服务端流、客户端流和双向流;HTTP/2 多路复用适合内部高频调用。
  • 外部浏览器生态、人工调试和 CDN 缓存方面 REST/JSON 更方便。浏览器要用 gRPC-Web 或网关,并单独处理错误码和版本兼容。

Q18: Webhook 应如何可靠设计?

答案

发送方签名并带事件 ID/时间戳,接收方验证来源、防重放并快速确认,把实际处理放异步队列。

发送方按退避重试并提供投递日志;接收方幂等。密钥轮换、事件版本和人工重放也要支持。

  • 发送方对原始请求体、时间戳和事件 ID 签名;接收方先验证签名与时间窗口,再幂等入队并快速返回 2xx。
  • 发送失败按指数退避重试并进入死信,双方都保留投递日志和人工重放能力;事件 Schema 要版本化,密钥支持轮换。

Q19: 健康检查和优雅停机如何配合?

答案

存活检查判断进程是否需要重启,就绪检查判断是否能接流量。停机时先置为未就绪并停止新请求,再等待进行中工作和资源关闭。

检查不能依赖过多不必要下游,否则会引发级联重启;但关键依赖不可用时也不能错误报告就绪。

  • liveness 只判断进程是否僵死,readiness 判断当前实例能否接流量,startup probe 保护慢启动;三者职责混淆会导致无意义重启。
  • 停机时先 readiness 失败并从负载均衡摘除,再停止接收新请求、等待在途任务、关闭资源,最后在超时上限内退出。

Q20: 配置中心和密钥管理有什么原则?

答案

配置要分环境、版本化、Schema 校验并支持审计;Secrets 使用专门系统加密存储、最小权限、短期凭据和轮换,不进入代码、镜像或日志。

动态配置变更要灰度和可回滚,应用应明确哪些配置可热更新、哪些必须重启。

  • 普通配置可以版本化并做 Schema 校验、灰度和审计;密钥应存专用 Secret Manager,使用工作负载身份、最小权限和短期凭据。
  • 日志和错误页面必须脱敏。动态更新要明确原子性和回滚方式,不能让半数实例长期运行不同且不可追踪的配置。

Q21: Nginx 和 Node.js 如何配合部署?

答案

典型架构:Nginx 在前处理静态资源和 SSL,动态请求反向代理到 Node.js:

  • Nginx 监听 80/443 端口
  • 静态资源由 Nginx 直接返回(性能远高于 Node.js)
  • API 请求通过 proxy_pass 转发到 Node.js(通常是 3000 等内部端口)
  • Node.js 不直接暴露给公网

Q22: 线上服务出问题了,你怎么排查?

答案

# 1. 先看日志
tail -100 /var/log/app/error.log
journalctl -u app --since "5 minutes ago"

# 2. 检查进程是否存活
ps aux | grep node
systemctl status app

# 3. 检查端口是否监听
ss -tlnp | grep 3000

# 4. 检查系统资源
top # CPU 和内存
df -h # 磁盘
free -h # 内存详情

# 5. 检查网络连通性
curl localhost:3000/health

Q23: Serverless 适合什么场景?不适合什么?

答案

适合不适合
API 接口长时间运行的任务
SSR/SSGWebSocket 长连接
定时任务需要大量本地状态
图片处理高性能计算
Webhook需要持久化内存的服务

Q24: 服务发现和服务注册分别解决什么问题?

答案

服务注册让实例在启动、续约和下线时把地址、端口和元数据写入注册中心;服务发现让调用方或代理根据逻辑服务名找到当前健康实例。它解决弹性伸缩后实例地址动态变化的问题。

常见模式:

  • 客户端发现:客户端查询注册中心并负载均衡,调用链短,但每种语言都要实现治理逻辑。
  • 服务端发现:客户端请求负载均衡器或 Sidecar,由基础设施选择实例,客户端更简单。
  • Kubernetes 中通常由 Service、EndpointSlice 和 DNS 等能力协作完成。

注册成功不等于服务可用,还要结合健康检查、租约过期、优雅下线、缓存和故障实例摘除;注册中心本身也必须高可用。

答案

没有脱离威胁模型的唯一答案。浏览器会话常把 Refresh Token 或 Session ID 放在 HttpOnly + Secure + SameSite Cookie 中,JavaScript 不能直接读取,可降低 Token 被 XSS 窃取的风险;但 Cookie 会自动携带,需要正确处理 CSRF、Domain 和 Path。

localStorage 中的 Token 不会自动随请求发送,但任何同源 XSS 都能读取并外传,也没有可靠的服务端撤销能力。高价值长期凭据不应长期放在其中。

常见方案是短期 Access Token 保存在内存,Refresh Token 使用受限 Cookie,并配合轮换、重放检测和服务端撤销。最终还要看同源/跨源部署、客户端类型和 CSP。

Q26: Cache Aside 为什么常用“更新数据库后删除缓存”?它能保证强一致吗?

答案

先删缓存再更新数据库时,另一个读请求可能在数据库更新前把旧值重新写回缓存,形成较长时间脏数据。先提交数据库再删缓存,通常把主要风险缩小到“数据库已更新但删除缓存失败”这一窗口。

但它不能保证强一致:

  • 删除失败需要重试、消息或 Binlog/CDC 补偿。
  • 读写并发仍可能产生短暂旧值。
  • 多副本数据库还要考虑主从延迟。
  • 关键写入可用版本号、事务 Outbox 或绕过缓存。

延迟双删只是特定条件下的缓解技巧,延迟值难确定,不应被描述成一致性保证。

Q27: 前端错误发生后,如何通过日志快速定位问题?

答案

  1. 前端上报时携带 traceId(从响应头获取)
  2. traceId 在服务端日志系统中搜索完整链路
  3. 查看链路上每个服务的日志:请求参数 → 处理过程 → 返回结果

Q28: 如何降低消息丢失风险?

答案

要逐段定义交付保证,而不是声称“绝对不丢”:

  1. 生产者使用确认机制、稳定消息 ID和有限重试,避免发送结果不确定。
  2. Broker 配置持久化、复制和足够的确认级别,并监控不可用分区。
  3. 消费者完成业务事务后再 ACK,失败进入重试/死信。
  4. 数据库写入与发消息之间可使用 Transactional Outbox,避免双写空窗。
  5. 对账和补偿任务发现长期未完成的业务状态。

更强确认通常增加延迟,重复投递仍可能发生,所以消费者幂等与可观测性必须同时设计。

Q29: API 版本管理怎么做?

答案

三种方式:

  1. URL 路径/api/v1/users(最常用)
  2. 请求头Accept: application/vnd.api+json; version=1
  3. 查询参数/api/users?version=1

Q30: 一个接口响应很慢,怎么排查?

答案

系统化排查分五步:

第一步:确认范围

  • 是所有接口都慢,还是特定接口慢?
  • 是所有用户都慢,还是特定用户/地区?
  • 是一直慢,还是突然变慢?

第二步:检查基础设施

  • top / htop 看 CPU、内存使用率
  • iostat 看磁盘 I/O 是否瓶颈
  • ss -s / netstat 看 TCP 连接状态(是否连接池打满)
  • 检查数据库连接数是否达到上限

第三步:定位慢操作

  • 查看请求链路追踪(traceId),找出耗时最长的环节
  • 检查数据库慢查询日志(SHOW PROCESSLIST、慢查询日志)
  • 检查外部服务调用耗时(是否超时、重试)

第四步:深入分析

  • 数据库慢 → EXPLAIN 分析执行计划,检查索引
  • CPU 高 → Node.js --inspect + Chrome DevTools CPU Profile
  • 内存高 → Heap Snapshot 分析内存泄漏
  • 网络慢 → 检查 DNS 解析、连接复用、响应体大小

第五步:验证修复

  • 修改后通过压测验证效果
  • 监控 P95/P99 延迟确认改善

Q31: 限流应该放在哪一层?

答案

生产环境中通常多层限流,每一层解决不同问题:

层级职责示例
CDN/WAF防 DDoS、IP 黑名单Cloudflare Rate Limiting
Nginx 网关粗粒度:IP 限流、全局 QPSlimit_req_zonelimit_conn_zone
API 网关中粒度:按路由、按租户Kong、APISIX 限流插件
应用层细粒度:按用户、按接口@nestjs/throttler、自定义 Guard
数据库层连接池本身就是限流max_connections、连接池 poolSize

原则:越靠前的层越粗粒度,越靠后的层越细粒度。粗粒度限流挡住大部分恶意流量,细粒度限流做精准控制。

Q32: 分布式系统如何选择数据一致性方案?

答案

先按业务不变量选择,而不是把系统统一归类成“强一致”或“最终一致”:

  • 余额扣减、库存唯一占用等核心不变量可放在单库事务、共识系统或明确的串行化边界内。
  • 跨服务流程常用 Saga、Outbox/Inbox、幂等消费和补偿实现可观测的最终一致。
  • 只读副本、缓存和搜索索引可以接受有界陈旧,但要向用户说明状态并支持回源。
  • 2PC 能提供特定原子性,但可用性、协调器和参与者约束较高,不是所有跨服务写入的默认答案。

需要明确一致性窗口、失败状态、重试与人工修复,而不只给方案名称。

Q33: 定时任务和消息队列的区别?

答案

  • 定时任务:基于时间触发,如每天凌晨清理数据
  • 消息队列:基于事件触发,如用户下单后发送通知

两者经常配合使用:定时任务触发 → 消息队列执行。

Q34: 私有文件下载怎样设计临时授权?

答案

常见做法是应用先完成用户与资源权限校验,再生成短时有效的签名 URL,或由 CDN/对象存储验证签名后直接返回文件,避免应用服务器长期中转大流量。

设计重点:

  • 签名绑定资源路径、过期时间,必要时绑定下载方式或用户上下文。
  • URL 有效期按实际下载时长设置;大文件要考虑 Range 请求和续传。
  • 防止路径拼接、对象 id 猜测和日志、Referer 泄露签名地址。
  • 撤销要求很高时使用短 TTL、服务端令牌校验或可撤销会话。
  • 下载审计记录授权主体与资源,不把签名参数当用户身份。

签名 URL 是临时访问凭证,不应嵌入永久公开页面或跨用户复用。

Q35: WebSocket 断线重连应该怎样设计?

答案

客户端使用指数退避加抖动重连,网络恢复或页面重新可见时可触发受控尝试,并避免所有用户同时重连形成惊群。

恢复会话要有协议支持:

  • 重新认证,不能只信旧 connection ID。
  • 客户端携带最后确认的消息序号或游标。
  • 服务端从可持久化缓冲补发,超出保留窗口则要求全量同步。
  • 消息有 ID、顺序和幂等处理,避免补发重复产生副作用。
  • 心跳用于发现半开连接,但频率按网络和代理超时决定。

纯 Redis Pub/Sub 不保存离线消息,不能单独承担可靠补发。

Q36: 邮件发送为什么要异步?

答案

SMTP 网络请求可能需要数秒,同步发送会阻塞用户请求。应该将邮件任务丢进消息队列异步处理,接口立即返回。

Q37: 如何防止 SQL 注入?

答案

核心原则是数据与代码分离

  1. 参数化查询:使用 ?$1 占位符,数据库引擎自动转义
  2. ORM:Prisma / TypeORM 等 ORM 自动处理参数绑定
  3. 输入校验:用 Zod 校验类型和格式
  4. 最小权限:数据库账号只授予必需权限(如只读账号用于查询)
// 参数化查询
await pool.query('SELECT * FROM users WHERE id = $1 AND role = $2', [id, role]);

// ORM(Prisma 自动防注入)
await prisma.user.findMany({ where: { name: { contains: input } } });

Q38: GraphQL 和 REST 怎么选?

答案

维度RESTGraphQL
数据获取固定结构按需获取
过度获取常见不存在
缓存HTTP 缓存天然支持需要额外工具
学习曲线
适用场景CRUD 为主复杂关联数据、多端

Q39: Protobuf Schema 怎样保持向后和向前兼容?

答案

核心原则是字段编号一旦发布就不要改变含义或复用。新增可选字段通常兼容;删除字段后应把编号和名称标为 reserved,避免未来重新使用。

还要注意:

  • 不随意改变字段类型,线格式看似兼容也可能改变语义或精度。
  • 枚举新增值时,旧客户端必须能处理未知值。
  • oneof、required 语义和默认值变更要评估各语言生成代码行为。
  • 做新客户端/旧服务端、旧客户端/新服务端的组合测试。
  • 破坏性变化发布新消息或服务版本,并保留迁移窗口。

Protobuf 只提供编码层兼容基础,业务语义变更仍需版本策略和灰度。

Q40: Docker 和虚拟机的区别?

答案

维度Docker虚拟机
隔离级别进程级(共享内核)系统级(独立内核)
启动速度秒级分钟级
资源开销极小大(需要完整 OS)
镜像大小MB 级GB 级

Q41: Webhook 和轮询有什么区别?

答案

维度Webhook轮询
方向推送(服务方主动通知)拉取(客户端定时查询)
实时性实时取决于间隔
资源消耗低(事件触发)高(无效请求多)
可靠性需要重试机制简单可靠

Q42: liveness 和 readiness 的区别?

答案

  • liveness:应用是否存活。失败 → K8s 重启 Pod
  • readiness:应用是否就绪。失败 → K8s 不发流量到该 Pod

典型场景:应用启动中(readiness 失败但 liveness 正常),或依赖的数据库暂时不可用。

Q43: .env 文件和系统环境变量谁优先?

答案

优先级取决于具体加载器和框架,没有跨工具统一的固定顺序。多数 dotenv 实现默认不覆盖进程中已存在的环境变量,但 .env.local.env.[mode] 的合并顺序由 Vite、Next.js、Nuxt 等各自定义。

排查时应查看当前工具官方规则和最终进程值,不凭文件名猜测。生产环境通常由部署平台注入变量,不依赖把 .env 打进镜像。

还要区分服务端变量与会被构建进客户端的公开变量;任何进入前端 Bundle 的值都不是 Secret。

Q44: 如何解决前端 History 模式路由 404?

答案

try_files $uri $uri/ /index.html; —— 当请求的路径对应不到物理文件时,回退到 index.html,由前端路由接管。

Q45: 如何查看某个端口被谁占用?

答案

lsof -i :3000
# 或
ss -tlnp | grep 3000
# 或
netstat -tlnp | grep 3000

Q46: Edge Function 和普通 Serverless Function 有什么区别?

答案

Edge Function 通常部署在更靠近用户的分布式节点,适合鉴权前置、重写、轻量个性化和低延迟读取;区域型 Serverless Function 通常拥有更完整运行时、较长执行时间和更方便的数据库/私网访问。

具体运行时因云厂商而异,不能一概说 Edge 都是 V8 Isolate、零冷启动或不支持 Node API。边缘节点访问中心数据库还可能因为跨区网络反而更慢。

选型要看运行时 API、CPU/内存/时长限制、数据位置、一致性、日志调试、成本和供应商锁定。将计算移到边缘不等于数据也自动到边缘。

Q47: BFF 层的作用是什么?

答案

BFF(Backend for Frontend)是为前端定制的后端服务层:

  1. 接口聚合:一个页面需要的数据可能来自 5 个微服务,BFF 聚合后返回一个接口
  2. 数据裁剪:后端返回全量数据,BFF 只返回前端需要的字段
  3. 格式转换:统一不同微服务的数据格式
  4. 针对端优化:Web 和 Mobile 可以有不同的 BFF

Q48: Token 无感刷新应该怎样设计?

答案

常见模型是短期 Access Token 加更受保护的 Refresh Token,但有效期应按风险和业务决定,不存在统一的 15 分钟或 7 天标准。

刷新流程要点:

  1. Refresh Token 使用 HttpOnly、Secure、合适 SameSite/Path 的 Cookie 或安全客户端存储。
  2. 每次刷新轮换 Refresh Token,并记录 Token Family;旧 Token 再次出现时视为可能重放并撤销整条链。
  3. 前端对并发 401 做 singleflight,只发一次刷新请求,其他请求等待后重放。
  4. 刷新失败立即进入重新登录,不无限循环。
  5. 服务端支持登出、设备撤销、密码修改和风险事件失效。
  6. 写请求重放前确认幂等性,不能盲目重复提交。

Q49: 延迟双删怎么实现?

答案

延迟双删是对 Cache Aside 的增强,通过两次删除缓存来降低不一致的概率:

delayed-double-delete.ts
async function updateWithDoubleDelete(
id: string,
data: Partial<User>,
): Promise<void> {
const cacheKey = `user:${id}`;

// 1. 先删除缓存(可选,增加一致性概率)
await redis.del(cacheKey);

// 2. 更新数据库
await db.user.update(id, data);

// 3. 再次删除缓存
await redis.del(cacheKey);

// 4. 延迟后第二次删除,覆盖在步骤 2-3 之间被其他读请求重建的旧缓存
// 延迟时间 = 读请求执行时间 + 几百毫秒余量
setTimeout(async () => {
await redis.del(cacheKey);
}, 500);
}

// 更可靠的实现:通过消息队列延迟删除
async function updateWithMQDoubleDelete(
id: string,
data: Partial<User>,
): Promise<void> {
const cacheKey = `user:${id}`;

await db.user.update(id, data);
await redis.del(cacheKey);

// 发送延迟消息,500ms 后再次删除缓存
await messageQueue.sendDelayed({
action: 'DELETE_CACHE',
key: cacheKey,
delay: 500,
});
}
延迟双删的局限
  1. 延迟时间不好确定:需要根据业务读请求的平均耗时估算
  2. 不能完全保证一致性:只是降低了不一致的概率
  3. 增加了复杂度:引入了延迟任务或消息队列

如果对一致性要求非常高,建议使用 Binlog 订阅方案(Canal/Debezium),从数据库变更事件驱动缓存更新。

Q50: ELK 是什么?

答案

  • Elasticsearch:日志存储和搜索引擎
  • Logstash:日志收集和处理管道
  • Kibana:可视化查询和仪表盘

日志采集流程:应用 → Filebeat → Logstash → Elasticsearch → Kibana

Q51: 消息重复消费怎么办?

答案

消费者实现幂等性

  • 用消息 ID 做去重(数据库唯一索引、Redis SetNX)
  • 业务层幂等(如支付接口根据订单号幂等)

Q52: RESTful API 的 PUT 和 PATCH 有什么区别?

答案

PUT 表示用请求中的表示创建或替换目标资源,语义上幂等;PATCH 表示按某种补丁格式对资源做部分修改,补丁是否幂等取决于具体操作。

工程中常把 PUT 约定为全量替换、PATCH 约定为部分更新,但服务端必须明确字段缺失、显式 null、数组和并发版本的含义。PATCH 还应声明 Content-Type,例如 JSON Merge Patch 或 JSON Patch,而不是任意 JSON。

两者都要做权限、校验和并发控制,可结合 ETag/If-Match 防止覆盖他人更新。

Q53: 如何应对高并发?

答案

高并发优化是一个分层防御的过程,从入口到存储层层减压:

用户请求 → CDN(静态资源)→ 负载均衡 → 限流(保护后端)→ 缓存(拦截 80%+ 读请求)
→ 异步处理(非核心逻辑)→ 数据库(读写分离 / 分库分表)

具体手段:

层级手段说明
接入层CDN + 负载均衡分散流量,就近访问
防护层限流 + 熔断 + 降级保护核心链路,防止雪崩
缓存层多级缓存拦截绝大部分读请求
应用层异步 + 消息队列削峰填谷,快速返回
数据层读写分离 / 分库分表提升存储层吞吐量
扩展水平扩展增加服务实例

Q54: 令牌桶和漏桶的区别?

答案

对比项漏桶(Leaky Bucket)令牌桶(Token Bucket)
核心思想请求进桶排队,以恒定速率流出令牌以恒定速率生成,请求需获取令牌
突发流量不允许,强制恒速输出允许,桶中积累的令牌可应对短暂峰值
处理速度恒定可变(有令牌时可瞬时处理)
桶满行为溢出的请求被丢弃多余的令牌被丢弃(不影响请求)
类比水龙头以固定速率滴水ATM 取款,有余额才能取
适用场景需要严格匀速的场景(如消息队列消费)需要允许一定突发的场景(如 API 网关)
// 漏桶:无论请求怎么来,始终匀速处理
// 10:00:00 来 100 个请求 → 以 10/s 的速度处理,需要 10 秒

// 令牌桶:桶中有积累的令牌时,可以瞬间处理
// 桶中积累了 50 个令牌 → 50 个请求可以立即处理
// 之后恢复到 10/s 的速率

面试结论:令牌桶更常用,因为真实场景中流量总是有波动的,令牌桶能在限流的同时更好地应对合理的突发请求。

Q55: 分布式锁有哪些实现方式?

答案

方式优点缺点
Redis(SET NX)高性能,简单集群下有缺陷
Redlock更可靠实现复杂,有争议
ZooKeeper强一致重量级
数据库乐观锁简单性能低

Q56: 分布式定时任务怎样做分片和故障转移?

答案

当单实例无法完成全部任务时,可按稳定业务键或任务 id 分片,让多个执行器各自处理一部分;调度器通过租约、心跳或协调存储维护分片归属,实例失联后再把分片转移给其他执行器。

设计时要同时处理:

  • 分片数量通常多于实例数,便于扩缩容和负载均衡。
  • 任务必须幂等,租约过期与网络分区可能造成短暂重复。
  • 记录游标、检查点和执行状态,故障转移后从可验证位置恢复。
  • 热分片要能拆分或动态再平衡。
  • 设置超时、重试、死信、告警和人工补偿入口。

分布式锁只能限制某一时刻的执行者,不能单独保证任务不丢和结果正确。

Q57: 如何限制上传文件的大小和类型?

答案

三层校验:

  1. 前端input accept + File API 检查(易绕过,仅做体验优化)
  2. 服务端:Content-Type + 文件头(magic number)检测
  3. OSS 策略:Bucket Policy 限制

Q58: WebSocket 服务如何处理发送端背压?

答案

当生产消息的速度高于网络发送和客户端消费速度时,服务端发送缓冲会持续增长,最终导致内存膨胀和延迟失控。不能把 send 调用成功简单理解为对端已经处理。

常见策略:

  • 监控每个连接的缓冲字节、队列长度和最老消息延迟。
  • 为队列设置上限;行情、光标等消息可合并或丢弃,订单状态等不可丢消息转到可恢复消息系统。
  • 降低发布频率、批量发送并按用户公平调度。
  • 慢连接超过阈值时降级或断开,让客户端从游标补数据。
  • 按实际 WebSocket 库等待 drain 或检查 buffered amount。

策略必须由消息语义决定,不能对所有消息统一无限重试。

Q59: 如何避免邮件进垃圾箱?

答案

  1. 配置 SPF、DKIM、DMARC 记录
  2. 使用专业邮件服务商(SendGrid、AWS SES)
  3. 维护发件人信誉,避免频繁发送
  4. 提供退订链接

Q60: SSRF 攻击是什么?怎么防御?

答案

SSRF 是攻击者控制服务端要访问的地址,借服务端网络权限探测内网、云元数据或其他受保护服务。

防御应组合:

  1. 尽量不用任意 URL;使用业务允许列表和固定协议/端口。
  2. 解析并规范化 URL,拒绝用户名段、非常规 IP 表示、危险协议和本地文件。
  3. DNS 解析后校验所有地址是否属于内网、回环、链路本地和保留网段,并让实际连接绑定到已校验结果,防 DNS Rebinding。
  4. 每次重定向都重新校验,或直接关闭重定向。
  5. 在网络层限制应用访问元数据和内部管理网段。
  6. 设置响应大小、超时和下载类型限制,避免资源耗尽。
  7. 记录目标、解析 IP 和拒绝原因用于审计。