服务端面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 02026年5月30日 02:24

Promise.allSettled() 有什么作用?和 Promise.all 有什么区别?

Promise.allSettled() 会等待一组 Promise 全部结束,不管成功还是失败,最后返回每个任务的状态和结果;Promise.all() 则要求全部成功,只要一个 reject 就立刻 reject。面试里一句话区分:all 适合“缺一个都不行”,allSettled 适合“尽量都跑完,再分别处理结果”。例如批量请求、批量上传、页面多个独立模块加载,更适合 allSettled。追问allSettled 返回值长什么样?每一项都有 status。成功是 { status: 'fulfilled', value },失败是 { status: 'rejected', reason }。allSettled 会吞掉错误吗?不会吞,只是把错误包装进结果数组。你仍然要检查 rejected 项,否则失败会被业务层忽略。什么时候不能用 allSettled?任务之间有强依赖时不适合。比如用户信息失败后,后续请求必须停止,这时用 all 或串行 await 更清楚。和给每个 Promise 单独 catch 有什么区别?单独 catch 可以兼容更老环境,也能自定义返回结构;allSettled 是标准化写法,语义更明确。写段代码const results = await Promise.allSettled([fetchUser(), fetchPosts()]);const ok = results.filter(r => r.status === 'fulfilled').map(r => r.value);const failed = results.filter(r => r.status === 'rejected').map(r => r.reason);
服务端阅读 02026年5月30日 02:24

Promise 和回调函数有什么区别?为什么能解决回调地狱?

Promise 和回调函数都能处理异步,但抽象层次不同。回调是“异步完成后调用你给的函数”,容易出现多层嵌套、错误分散、控制流难追踪;Promise 把异步结果封装成一个有状态对象,可以链式调用、统一 catch、组合 all/race/any/allSettled。面试里可以说:Promise 不是让异步变同步,而是让异步流程更可组合、错误更集中、代码更容易维护。追问Promise 解决了回调地狱吗?解决了一部分。链式 then 可以拉平嵌套,async/await 又进一步接近同步写法。但如果业务拆分不好,Promise 链也会写得很乱。Promise 的错误处理强在哪里?回调通常要每层传 err;Promise 的错误会沿链传播,最后一个 catch 可以兜底处理。回调还有用吗?有。事件监听、流、Node 风格 API、低层库里仍常见回调。Promise 更适合“一次性成功或失败”的异步结果。Promise 有什么代价?它会引入微任务调度和对象创建,不适合滥用在纯同步逻辑里。性能敏感代码要避免无意义包装。写段代码readFile('a.txt') .then(parse) .then(save) .catch(handleError);
服务端阅读 02026年5月30日 02:24

Promise.any() 有什么作用?和 Promise.race 有什么区别?

Promise.any() 的作用是:一组 Promise 里只要有一个成功,就立刻返回这个成功结果;只有全部失败时,才会 reject,并抛出 AggregateError。它适合“多个候选源,谁先成功用谁”的场景,比如多 CDN 拉资源、多个镜像接口兜底。面试里要强调:它忽略失败,只关心第一个成功;这和 Promise.race() 谁先 settled 就返回完全不同。追问Promise.any 和 Promise.race 最大区别是什么?race 看第一个完成,不管成功还是失败;any 看第一个成功,失败会被暂时忽略,除非全部失败。全部失败时会发生什么?会 reject 一个 AggregateError,里面的 errors 保存所有失败原因。不要只 catch 后打印 message,最好把 errors 也记录下来。它和 Promise.all 有什么区别?all 要全部成功才成功,一个失败就失败;any 只要一个成功就成功。一个适合“都要”,一个适合“有一个可用就行”。实际项目里怎么用?适合容灾兜底,不适合支付、写入、下单这类不能重复尝试的操作。并行请求多个源时也要考虑取消慢请求或控制成本。写段代码try { const data = await Promise.any([ fetch('/cdn-a/config.json'), fetch('/cdn-b/config.json') ]); console.log(await data.json());} catch (e) { console.error(e.errors);}
服务端阅读 02026年5月30日 02:24

RPC 和 RESTful API 有什么区别?什么时候选 RPC?

RPC 和 RESTful API 的核心区别是抽象不同:RPC 像调用远程函数,关注方法、参数和返回值;REST 更像操作资源,关注 URL、HTTP 方法和状态码。内部微服务、低延迟、高吞吐、强类型契约、双向流式通信,通常选 RPC,比如 gRPC、Dubbo。对外开放接口、浏览器直接访问、需要易调试和缓存语义,REST 更合适。面试里不要说谁替代谁,关键是边界:内部效率优先选 RPC,对外通用性优先选 REST。追问RPC 为什么通常性能更好?很多 RPC 框架使用二进制序列化和长连接,协议开销更小,也更容易做连接复用、流式传输和代码生成。REST 的优势是什么?它基于 HTTP 语义,curl、Postman、浏览器都好调试;URL、状态码、缓存头也更适合开放平台和前后端协作。gRPC 能直接给浏览器用吗?原生 gRPC 对浏览器不友好,通常需要 gRPC-Web 或网关转换。面向公网用户时,很多团队会在外层提供 REST。项目里怎么组合两者?常见做法是外部 REST 网关,内部服务之间用 RPC。网关负责鉴权、限流、协议转换,内部服务专注高效调用。
服务端阅读 02026年5月30日 02:24

RPC 常见序列化协议有哪些?各自怎么选?

RPC 常见序列化协议有 Protobuf、Thrift、JSON、Hessian、MessagePack 和 Avro。面试先给结论:内部高性能微服务优先 Protobuf 或 Thrift;需要可读、易调试、对外兼容用 JSON;Java 旧系统可能见到 Hessian;需要类 JSON 但更小的体积可考虑 MessagePack;大数据和日志链路常用 Avro。选择时看四件事:体积、速度、跨语言、schema 演进能力。追问Protobuf 为什么常用于 RPC?它是二进制格式,体积小、解析快,靠 .proto 定义字段和类型,跨语言代码生成成熟。缺点是调试不如 JSON 直观。Thrift 和 Protobuf 有什么区别?Thrift 更像一整套 RPC 方案,包含 IDL、协议和传输;Protobuf 更专注数据序列化,常和 gRPC 搭配使用。JSON 为什么还没被淘汰?因为它人类可读、生态通用、排查方便。对外 API、管理接口、低频调用里,调试成本往往比极致性能更重要。协议升级最容易出什么问题?字段删除、类型变更、枚举兼容最容易翻车。新增字段通常安全,变更字段语义要同时考虑新老客户端。写段代码message UserReq { int64 id = 1; string name = 2;}
服务端阅读 02026年5月30日 02:24

如何优化 RPC 调用性能并降低网络延迟?

优化 RPC 性能先看调用链路:连接、序列化、网络传输、线程模型、服务端处理和观测。面试可以先答:复用长连接,开启连接池和预热;选 Protobuf、Thrift 这类二进制协议,减少字段和大对象;小包低延迟场景开启 TCP_NODELAY;用异步调用、批量请求、就近路由和客户端缓存降低等待时间。最后用 P95/P99、错误率、QPS、线程池队列和链路追踪定位瓶颈,不要只凭感觉调参数。追问TCP_NODELAY 一定要开吗?不一定。它能减少小包等待,但可能增加包数量。低延迟 RPC 常开,吞吐优先的批量传输要压测后决定。序列化为什么影响延迟?序列化影响 CPU、对象分配和网络包大小。JSON 好调试但体积大;Protobuf 体积小、速度快,更适合内部高频调用。异步调用能让单次 RPC 更快吗?不一定降低单次网络耗时,但能减少线程阻塞,提高并发吞吐。真正耗时的服务端逻辑仍要单独优化。项目里怎么排查慢 RPC?先看 P99 和超时分布,再用 Trace 拆成客户端排队、网络、服务端处理、序列化几段。定位后再调连接池、线程池、负载均衡或缓存。写段代码bootstrap.option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.SO_KEEPALIVE, true) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 300);
服务端阅读 02026年5月30日 02:24

RPC 负载均衡算法有哪些?如何选择合适策略?

RPC 负载均衡常见算法有随机、轮询、加权随机/轮询、最少连接、最少活跃、最短响应时间、一致性哈希和 IP Hash。面试里先说选择原则:实例差不多用随机或轮询;机器配置不同用加权;请求耗时差异大用最少连接/最少活跃;需要会话保持或本地缓存命中用一致性哈希。真正落地还要配健康检查、熔断、权重动态调整,否则算法再好也会把流量打到故障节点。追问随机和轮询有什么区别?随机实现最简单,长期看分布均匀;轮询更可预测,但如果某台机器变慢,仍会按顺序分流。两者都适合实例能力接近的场景。为什么 Dubbo 默认常用加权随机?它简单、开销低,配合权重能表达机器能力差异。比普通轮询更不容易在短时间内形成固定流量节奏。一致性哈希解决什么问题?它让同一个 key 尽量落到同一台实例,适合会话、缓存、分片类场景。节点增减时只迁移少量 key,但要用虚拟节点缓解倾斜。实际项目里会踩什么坑?只看请求数不看耗时会误判负载;服务注册中心实例下线不及时,会出现短时间错误流量。通常要把负载均衡和健康检查、超时、重试一起设计。写段代码ServiceInstance select(List<ServiceInstance> list) { return list.stream() .filter(ServiceInstance::isHealthy) .min(Comparator.comparingInt(ServiceInstance::getActive)) .orElseThrow();}
服务端阅读 02026年5月30日 01:39

什么是分布式链路追踪?OpenTelemetry、Jaeger 和 SkyWalking 怎么选?

分布式链路追踪就是给一次请求打上 Trace ID,把它经过的网关、服务、数据库、消息队列调用都串起来。面试里先答核心:Trace 表示一次完整请求,Span 表示其中一次操作,Span 之间用 parentId 形成调用树;上下文通常通过 HTTP Header、RPC Metadata 传播;数据由 SDK 或 Agent 采集,再异步上报到 Jaeger、SkyWalking、Zipkin 等后端。现在更推荐用 OpenTelemetry 做统一采集标准,后端再按团队习惯选择 Jaeger、SkyWalking 或商业 APM。追问Trace、Span、Trace ID 有什么区别?Trace 是整条调用链,Span 是链路上的一个节点,比如一次 HTTP 调用或 SQL 查询。Trace ID 贯穿全链路,Span ID 标识当前节点,Parent Span ID 用来还原父子关系。OpenTelemetry 和 Jaeger 是什么关系?OpenTelemetry 主要解决“怎么埋点、怎么采集、怎么传输”的标准化问题;Jaeger 更像存储、查询和展示链路的后端。实际项目里常见组合是 OTel SDK/Collector + Jaeger。Jaeger、SkyWalking、Zipkin 怎么选?Java 微服务、想要 APM 能力更全,可以选 SkyWalking;多语言、高并发链路追踪,Jaeger 更常见;Zipkin 简单稳定,适合轻量场景。新项目优先保证采集侧接 OpenTelemetry,避免后续迁移被某个后端绑死。项目里最容易踩什么坑?第一是异步线程、消息队列、定时任务没传上下文,链路会断。第二是采样率过高拖慢系统,过低又抓不到问题;线上通常按流量、错误率和核心接口分层采样。链路追踪和日志、监控有什么区别?监控告诉你“哪里慢了”,日志告诉你“发生了什么”,链路追踪告诉你“一次请求到底卡在哪个调用”。排障时三者结合,Trace ID 要能在日志里直接检索。写段代码Span span = tracer.spanBuilder("queryUser").startSpan();try (Scope scope = span.makeCurrent()) { return userClient.getUser(id);} catch (Exception e) { span.recordException(e); span.setStatus(StatusCode.ERROR); throw e;} finally { span.end();}
服务端阅读 02026年5月30日 01:39

React 中如何正确使用 MobX 和 observer?

在 React 中用 MobX,核心是让读取 observable 的组件被 observer 包住。store 可以通过 Context、props 或模块变量传入;实际项目更推荐 Context,测试和多实例更好控。observer 会追踪组件渲染时真正读到的 observable,相关字段变化才重渲染,所以不要在外层提前把 observable 解成普通值再传下去。函数组件优先用 mobx-react-lite,类组件或旧项目才考虑 mobx-react。追问observer 应该包父组件还是子组件?谁读取 observable 就包谁。把整个 App 包起来不等于所有子组件都响应,细粒度 observer 反而更容易减少无关渲染。Context 里的 store 要不要经常替换?通常不要。Provider 的 value 保持同一个 store 实例,更新 observable 字段即可;频繁替换 store 会让依赖关系和测试都变复杂。组件为什么没有更新?优先查三件事:组件是否用了 observer,读取的对象是否真是 observable,是否在 observer 组件外提前解构成普通值。第三方组件能直接吃 observable 吗?不建议。第三方组件不是 observer,传入前最好转成普通数据,或只传它需要的字段。写段代码const StoreContext = createContext(null)export const useStore = () => useContext(StoreContext)const TodoList = observer(() => { const store = useStore() return store.todos.map(todo => <span key={todo.id}>{todo.text}</span>)})
服务端阅读 02026年5月30日 01:39

如何优化 Zustand 状态更新性能?

Zustand 性能优化先看订阅粒度:组件只订阅自己需要的字段,不要 useStore() 拿整个 store。多个字段一起取时用 shallow 或拆成多个 selector;状态太大时按领域拆 store;异步更新用函数式 set 或 get() 避免旧值。真正的瓶颈通常不是 Zustand,而是选择器返回新对象、组件订阅过宽、列表渲染太重。追问为什么 useStore() 容易造成重渲染?它订阅整个 store,任何字段变化都会让组件重新渲染。字段越多,误伤越明显。shallow 能解决什么问题?selector 返回对象或数组时,每次都是新引用。shallow 会比较第一层字段,字段没变就不触发更新。拆 store 一定更好吗?不一定。强相关状态放一起更好维护;变化频率差异很大、业务边界清楚时再拆,否则会增加同步成本。批量更新要手动处理吗?React 18 下大多数场景会自动批处理。更重要的是把相关字段放在一次 set 里,避免中间状态被订阅者看到。写段代码import { shallow } from 'zustand/shallow';const count = useStore((s) => s.count);const inc = useStore((s) => s.inc);const userView = useStore( (s) => ({ name: s.user.name, role: s.user.role }), shallow);const useStore = create((set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 }))}));