想象你走进一家餐厅。菜单潦草混乱:菜品没有编号,名称随意,招牌菜只写着“那啥汤”,服务员点单全凭默契与喊话。这顿饭,你吃得放心吗?API设计,就是为机器世界编写这样一份服务手册——服务对象不是食客,而是成千上万的开发者与AI应用。RESTful原则规定如何给“菜品”(资源)命名与编号;请求与响应定义了点单与上菜的传话流程;分页过滤确保服务员不会一次端上整桌菜;版本控制处理菜单改版时老顾客的怀旧情绪;错误处理是一套成熟的道歉与补救话术;GraphQL则代表自助餐与单点菜的路线之争。系统设计面试中,面试官想看到的不是你把某个框架背得滚瓜烂熟,而是你能否像一位资深的餐厅运营总监,把数据世界的“菜单”设计得清晰、稳定、可扩展。本文用一个贯穿始终的“数字餐厅”比喻,带你从头设计一份让面试官点头的API方案。

岗位关联

对AI从业者而言,API设计能力决定了你的模型与系统能否被外部世界干净地接入。系统设计面试里,这几乎是必考的能力底色——一份优雅的API设计,就是你留给面试官的第一张名片。

目录
  1. 可视化演示
  2. RESTful设计:给资源立规矩
  3. 请求响应:点单与上菜的标准话术
  4. 分页过滤:别一次端上整桌菜
  5. 版本控制:菜单改版的艺术
  6. 错误处理:出错的道歉艺术
  7. GraphQL对比:自助餐与单点菜
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示RESTful资源建模(GET/POST/PUT/DELETE)、GraphQL按需查询对比(避免过度获取)、分页策略(偏移vs游标)与版本控制,交互切换三大API设计核心主题。

本节配有交互动效(进入视口后自动加载,约 3KB)。

API设计面试三要点 RESTful:资源用名词、HTTP方法表达操作、无状态通信 GraphQL:客户端按需查询字段,解决过度获取与获取不足 分页:偏移分页简单但大offset慢,游标分页稳定高性能 REST: /api/v1/users?offset=20&limit=10 | GraphQL: query{user{id,name}} 版本控制:URL路径/Header/查询参数三种策略

RESTful设计:给资源立规矩

餐厅想高效运转,第一件事是设计菜单规范。服务员不会说“那个谁谁谁家的大厨做过的一道很好喝的汤”,而是报出编号:“菜单A3,西湖牛肉羹”。在API世界里,RESTful设计做的正是这件事:把用户、订单、菜品这些“东西”抽象成资源,给每个资源一个唯一、可读、有规律的“门牌号”(URI),让客户端能准确找到它、操作它。

REST(Representational State Transfer)不是协议,而是一种架构风格,源自Roy Fielding的博士论文。它强调“资源导向”:一切业务对象都是名词,而动作统一交给HTTP的标准方法。看见 /api/v1/users/42,任何开发者都立刻明白这是一个“用户集合中的编号42”——想读用GET,想删用DELETE,语义完全由方法决定,无需额外暗号手册。

RESTful设计有几条黄金约定。第一,URI用名词复数,写 /orders 而非 /getOrderInfo,动词必须待在HTTP方法里;第二,层级关系用路径嵌套表达,/api/v1/users/42/favorites 表示“用户42的收藏列表”;第三,避免暴露实现细节,如 .php 后缀或 ?action=delete 这类参数。这些约定让API像一份格式统一的菜单,客人拿到就能点菜。

回到数字餐厅:服务员听到“菜单A3”就知道该去后厨让厨师现做(POST),而不是去冷藏柜拿成品(GET)。方法语义一旦清晰,点单效率自然翻倍——面试官最看重的,正是这种“说到哪做到哪”的确定性。

R=资源集合,M↦CRUD:POST→C, GET→R, PUT/PATCH→U, DELETE→D\mathcal{R} = \text{资源集合},\quad M \mapsto \text{CRUD}: \text{POST}\to C,\ \text{GET}\to R,\ \text{PUT}/\text{PATCH}\to U,\ \text{DELETE}\to D
这条公式是RESTful的“总纲”:资源(名词)由URI承载,操作(动词)由HTTP方法承载,两者拼起来就是一次完整的API调用。
  1. 资源集合$\mathcal{R}$ 表示所有被管理的资源,如用户、菜品、订单,它是整个API设计的起点,决定了URI的路径前缀。
    R=资源集合\mathcal{R} = \text{资源集合}
  2. 创建映射$\text{POST}\to C$ 表示POST方法对应Create(创建),好比让后厨“现做一道新菜”,每次点单都会产生一个新资源。
    POST→C\text{POST}\to C
  3. 读取映射$\text{GET}\to R$ 对应Read(读取),这是唯一保证无副作用的查询型操作——问服务员“这道菜是什么”,他不会因此改变菜单。
    GET→R\text{GET}\to R
  4. 更新与删除$\text{PUT}$ 整包替换、$\text{PATCH}$ 局部修改都属于Update,$\text{DELETE}$ 对应Delete;分别像“重做整盘菜”“加个辣”和“退掉这道菜”。
    PUT/PATCH→U, DELETE→D\text{PUT}/\text{PATCH}\to U,\ \text{DELETE}\to D

请求响应:点单与上菜的标准话术

顾客坐下后,点单不是随便喊一嗓子。得告诉服务员“我要什么菜、什么做法、不放香菜”;服务员的回应也有规矩:“马上来”“对不起卖完了”“后厨着火了,稍候”。这套标准话术,就是API的请求与响应。

一次HTTP请求包含四要素:方法(做什么)、URI(对谁做)、请求头(元信息,如认证令牌、内容类型)、请求体(具体数据)。响应则以状态码(100-599)概括结果,配合响应体返回数据或错误说明。状态码是客户端判断下一步动作的“信号灯”:绿灯通行,黄灯变道,红灯停车。

状态码按百位数归族:2xx成功,3xx重定向,4xx客户端错误(你传的东西有问题),5xx服务端错误(服务器自己的锅)。面试最常考的一组:401未认证、403无权限、404不存在、409冲突、429限流、500服务器错误、503服务不可用。能脱口而出401与403的区别,是API方向的基本功——前者是“你是谁我不知道”,后者是“我知道你是谁,但你不许进”。

优秀的服务员会把话说全:结论(状态码)+ 原因(响应体)。面试中描述响应设计时,记得把三层结构讲完整,这比单背几个数字有说服力得多。

S=⌊c100⌋×100,c∈{200,201,400,401,403,404,500,503}S = \left\lfloor \frac{c}{100} \right\rfloor \times 100,\quad c \in \{200, 201, 400, 401, 403, 404, 500, 503\}
状态码家族:用百位数把HTTP状态码归类,客户端只需判断“2还是4还是5”,就能决定重试、改参还是报警。
  1. 取百位归类$\left\lfloor \frac{c}{100} \right\rfloor$ 是向下取整,把状态码压缩成家族编号,例如404取整后得到4。
    ⌊404100⌋=4\left\lfloor \frac{404}{100} \right\rfloor = 4
  2. 族号定语义$S=400$ 表示客户端错误,$S=500$ 表示服务端错误,$S=200$ 表示成功——这是客户端处理逻辑的最粗粒度分流。
    S∈{200,400,500}S \in \{200, 400, 500\}
  3. 响应三件套一次完整响应等于状态码 $S$ 加上响应头(Headers)与响应体(Body),状态码是结论,头是附注,体是细节。
    Response=(S, Headers, Body)\text{Response} = (S,\ \text{Headers},\ \text{Body})

分页过滤:别一次端上整桌菜

如果餐厅有100道菜,服务员不可能一次全端上桌;客人往往只要“四个凉菜、一道汤”。“分页”是分批上菜,“过滤”是按口味挑菜。高明的服务员会问:“您先吃着,后面的菜我按顺序慢慢上。”API的分页与过滤,就是服务员的这套功夫。

偏移分页(offset/limit)实现简单直觉:?page=3&limit=20 表示第3页每页20条,数据库跳过前40条。但它有两个软肋:一是深翻页时(第10000页)数据库要扫描并丢弃海量行,性能断崖下跌;二是并发写入导致数据位移时,用户会看到重复或遗漏。游标分页因此登场:客户端拿着服务器签发的“书签”(cursor),服务端从书签位置继续取数,稳如老狗。

游标通常编码为排序字段(如 created_at 加 id)的Base64值,本质是“记住你看到哪了”,而非“你翻到第几页”。过滤与排序应在服务端完成:客户端传 ?category=hot&sort=price_asc,服务端先过滤再分页,绝不把全量数据倒给客户端自己筛。

分页方案是面试的高频考察点,遇到“数据量百万级、实时写入”场景,直接答游标分页并解释退化问题,能明显拉开与背答案考生的差距。

offset=(page−1)×limit\text{offset} = (\text{page} - 1) \times \text{limit}
cursor=Base64(created_at∥id)\text{cursor} = \text{Base64}\left(\text{created\_at} \parallel \text{id}\right)
偏移分页算“页码→跳行数”,游标分页算“书签→定位行”。一个面向页,一个面向位置,后者在深翻页时性能稳定。
  1. 偏移计算$\text{offset} = (\text{page} - 1) \times \text{limit}$ 第3页每页20条,偏移量就是40条,意思是“跳过前40行后开始取”。
    offset=(3−1)×20=40\text{offset} = (3 - 1) \times 20 = 40
  2. 页码反推$\text{page} = \left\lfloor \frac{\text{offset}}{\text{limit}} \right\rfloor + 1$ 已知偏移量也能反推页码,但深翻页时这个方案会退化。
    page=⌊4020⌋+1=3\text{page} = \left\lfloor \frac{40}{20} \right\rfloor + 1 = 3
  3. 游标编码$\text{Base64}\left(\text{created\_at} \parallel \text{id}\right)$ 把“时间+唯一ID”两列拼起来编码成不透明字符串,服务端解码即回到当时位置——书签上写的是“你吃到第几盘”,而不是“你吃了饭的百分之几”。
    cursor=Base64(时间戳∥ID)\text{cursor} = \text{Base64}(\text{时间戳} \parallel \text{ID})

版本控制:菜单改版的艺术

餐厅的招牌菜每个季度会改良配方:换更好的食材、调整摆盘。但总不能因为改版就把老顾客熟悉的菜全撤了。聪明的餐厅会保留一个“经典系列”(v1),同时推出“新派系列”(v2),提前告知大家:“经典系列将在年底下架,请新老顾客知悉。”API版本控制,就是菜单改版的艺术。

一个被外部依赖的接口,任何不兼容改动——改字段名、删字段、改响应格式——都可能让下游应用瞬间崩溃。语义化版本号(SemVer)约定三段式:主版本Major、次版本Minor、补丁Patch。主版本号增加意味着不兼容的破坏性更新;次版本号是向后兼容的新功能;补丁号是向后兼容的bug修复。API路径里的 v1、v2 正是取自主版本号。

实践中最常见的是URI路径版本化:/api/v1/、/api/v2/,直观、可缓存、便于多版本同时部署。另有Header或参数版本化,但可缓存性差,用得少。推荐的演进策略是“只增不改”:新需求优先以新增字段、新增端点来满足,避免改动现有语义;确需破坏性修改时,新版本至少并存一个弃用周期(如6个月),并给客户端明确的迁移窗口与文档。

面试里聊到版本控制,别只甩出“加个v2”——把Major/Minor/Patch的兼容性契约说透,再补上“先并存、再通知、后下线”的弃用节奏,专业感立现。

vapi=Major.Minor.Patch,ΔMajor⇒Breaking Changev_{\text{api}} = \text{Major}.\text{Minor}.\text{Patch},\quad \Delta\text{Major} \Rightarrow \text{Breaking Change}
版本号不是装饰:主版本变了就是“菜单大改版”,老顾客需要重新学点菜;次版本和补丁则必须保证旧菜谱依然有效。
  1. 三段编号$\text{Major}.\text{Minor}.\text{Patch}$ 例如2.3.1,主版本2、次版本3、补丁1,每位含义不同。
    vapi=2.3.1v_{\text{api}} = 2.3.1
  2. 破坏性变更$\Delta\text{Major}$ 表示主版本号增加,意味着不兼容改动,客户端必须适配才能继续工作——新版菜单撤掉了旧招牌菜。
    ΔMajor⇒Breaking Change\Delta\text{Major} \Rightarrow \text{Breaking Change}
  3. 向后兼容$\Delta\text{Minor}, \Delta\text{Patch} \Rightarrow \text{Compatible}$ 次版本与补丁的变动不破坏既有调用,如新增字段、修复bug,老客户无感。
    ΔMinor, ΔPatch⇒Compatible\Delta\text{Minor},\ \Delta\text{Patch} \Rightarrow \text{Compatible}

错误处理:出错的道歉艺术

服务员上错菜时的表现,最能体现一家餐厅的素养。差劲的餐厅只会说“出错了”,顾客不知道错在哪、怎么补救;出色的餐厅会说:“对不起,这是我们的问题(错误码),您点的红烧肉卖完了(message),建议换招牌烤鸭或退款(details),您看行吗?”好的错误响应,就是这套道歉艺术。

规范的API错误响应不是一句冰冷的 500 Internal Server Error,而是一个结构化对象,通常包含四件套:code 业务错误码(如 ORDER_PAID),message 人类可读的提示,details 具体是哪个字段出错,trace_id 用于后端日志追踪的唯一ID。同时,错误码的语义必须与HTTP状态码一致:4xx是客户端的错,5xx是服务端的锅,绝不能颠倒。

错误处理还与重试策略强相关。客户端收到5xx可以尝试重试,但要用指数退避(1秒、2秒、4秒……),而不是疯狂轰炸;收到4xx则不该盲目重试,应修正请求。对创建类请求(如创建订单、提交训练任务),为避免重复下单,API应支持幂等:客户端携带 Idempotency-Key,服务端记录该key对应的处理结果,重复提交时直接返回第一次的结果。

答辩时主动说出“错误响应要有 trace_id”,面试官会眼前一亮——这体现你不是只写过玩具API的人,而是被线上问题教育过的工程师。

Error={code:str, message:str, details:obj, trace_id:str}\text{Error} = \{\text{code}: \text{str},\ \text{message}: \text{str},\ \text{details}: \text{obj},\ \text{trace\_id}: \text{str}\}
错误结构是“服务员的道歉话术模板”:先给错误码(什么类型的错),再给句话(人话解释),再给细节(哪里错),最后给追踪ID(方便查账)。
  1. 业务错误码$\text{code}$ 是可编程处理的短字符串,如 USER_NOT_FOUND,比HTTP状态码更细粒度,客户端可据此做分支逻辑。
    code∈{USER_NOT_FOUND, ORDER_PAID, CONTENT_FILTERED}\text{code} \in \{\text{USER\_NOT\_FOUND},\ \text{ORDER\_PAID},\ \text{CONTENT\_FILTERED}\}
  2. 人类可读信息$\text{message}$ 是给人看的提示,应当说明“发生了什么+该怎么办”,避免纯技术黑话。
    message:str\text{message}: \text{str}
  3. 明细与追踪$\text{details}$ 存字段级错误(如“price不能为负”),$\text{trace\_id}$ 则是贯穿前后端日志的“案件编号”,出问题可一键串起排查链路。
    trace_id:UUID\text{trace\_id}: \text{UUID}

GraphQL对比:自助餐与单点菜

同样是吃饭,有人喜欢单点菜的仪式感:一道一道喊服务员(REST),请求多但指向明确;有人喜欢自助餐的自由:端着餐盘走一圈,想吃什么夹什么(GraphQL),一次搞定、不多不少。这两种模式的较量,就是REST与GraphQL之争。

REST的痛点在于过度获取(overfetch)与欠获取(underfetch):移动端想展示“用户+其订单+订单里的商品”,可能要连发三次请求 /users、/users/42/orders、/orders/42/items,把一堆用不上的字段一起下载。GraphQL让客户端在单个请求里声明所需字段与嵌套结构,服务端精确装配返回,相当于把“点菜权”交给了食客。

但GraphQL不是银弹。它把数据装配压力移给了服务端,容易踩N+1查询坑;单一端点牺牲了HTTP缓存,得自建CDN与持久化缓存;schema演进也依赖团队纪律。选型铁律是:公开、简单、缓存友好的场景选REST;客户端多样、聚合复杂、移动端流量敏感的场景选GraphQL。数字餐厅里,单点菜仪式感足、后厨压力小;自助餐一次吃爽、后厨备菜压力大。你作为“运营总监”,做出的是因地制宜的选择——面试官想看的,正是这份权衡的智慧。

Overfetch Rate=∑i=1nRi−AA\text{Overfetch Rate} = \frac{\sum_{i=1}^{n} R_i - A}{A}
过度获取率量化了REST的浪费:分母是客户端真正需要的数据量,分子是n次请求实际传输量减去需求量。这个比值越高,接口设计越不匹配调用方。
  1. 多次响应求和$\sum_{i=1}^{n} R_i$ 表示REST下客户端要凑齐数据得发 $n$ 个请求,把所有响应大小加起来,就是总的传输量。
    ∑i=1nRi=R1+R2+⋯+Rn\sum_{i=1}^{n} R_i = R_1 + R_2 + \cdots + R_n
  2. 减去实际需求$- A$ 中 $A$ 是客户端真实需要的最小数据量,两者相减就是白传输的“货物”——自助餐里你吃不掉却堆在盘里的菜。
    ∑i=1nRi−A\sum_{i=1}^{n} R_i - A
  3. 浪费比例除以 $A$ 得到归一化的浪费率,$n$ 越大、$R_i$ 越臃肿,这个值越高,说明接口越需要GraphQL这类按需方案。
    浪费量实际需求量=Overfetch Rate\frac{\text{浪费量}}{\text{实际需求量}} = \text{Overfetch Rate}

AI 应用场景

智能推荐服务的API设计

阶段:模型服务化与接入

解决的问题:传统方式下每个客户端各写一套数据拉取逻辑,推荐列表的分页、排序、字段格式全凭各端自由发挥,模型一升级,接口就崩。AI平台用RESTful设计统一暴露接口,游标分页支撑用户无限下滑,版本控制保障模型升级时老客户端不受影响。

具体案例:某电商“猜你喜欢”接口从 /getRec?page=1 重构为 POST /api/v1/recommendations/feed,请求体携带 cursor 与 channel 字段,响应统一为 {code, data, next_cursor}。推荐模型从v1升级到v2(算法全面更换)时,/api/v1/ 与 /api/v2/ 并存三个月,AB实验平滑切换,客户端发版节奏完全不受影响。

LLM应用的流式响应与错误处理

阶段:模型推理与交互

解决的问题:大语言模型的Token是流式生成的,若用传统请求-响应模式,用户要等几十秒才能看到第一个字,体验极差。同时模型可能触发内容审核、超时、限流等异常,错误处理必须颗粒化。

具体案例:某智能客服机器人,用户提问后通过SSE(Server-Sent Events)逐Token返回回答,同时在响应头携带 trace_id 串联模型调用链。当内容审核命中风险时,返回结构化业务错误码 CONTENT_FILTERED 并触发兜底话术;限流时返回429并附 Retry-After 头,客户端按退避策略重试。

企业AI平台的多租户API治理

阶段:平台工程与治理

解决的问题:企业内部多个团队共享训练与推理集群,API滥用会拖垮整体服务。需要API网关统一鉴权、限流、幂等控制,让“租户”之间互不干扰。

具体案例:某公司“模型训练平台”开放 POST /api/v1/jobs 提交训练任务,要求客户端携带 Idempotency-Key;重复提交同一key直接返回上次任务结果,避免重复扣费。下游对接方收到5xx后按指数退避重试,队列积压时返回503并提示稍后重试。

Python 演示

python51 行
class RestaurantAPI:    """数字餐厅API模拟:演示偏移分页与游标分页"""     def __init__(self):        self.dishes = [            {"id": i, "name": f"菜品{i:02d}", "price": i * 3,             "category": "热菜" if i % 2 else "凉菜"}            for i in range(1, 41)        ]     def list_offset(self, page=1, page_size=5, category=None):        """偏移分页:客户端传页码,服务端计算偏移量"""        filtered = [d for d in self.dishes                    if category is None or d["category"] == category]        start = (page - 1) * page_size        page_data = filtered[start:start + page_size]        total = len(filtered)        return page_data, total     def list_cursor(self, cursor=None, page_size=5):        """游标分页:服务器签发下一页书签"""        start = 0 if cursor is None else cursor        chunk = self.dishes[start:start + page_size]        next_cursor = chunk[-1]["id"] if chunk else None        return chunk, next_cursor     def make_error(self, code, message, trace_id):        """结构化错误响应"""        return {"error": {"code": code, "message": message,                          "trace_id": trace_id}}  if __name__ == "__main__":    api = RestaurantAPI()    print("=== 偏移分页: 第3页, 每页5道菜 ===")    page, total = api.list_offset(page=3, page_size=5)    print(f"本页菜品ID: {[d['id'] for d in page]}, 共{total}道")     print("\n=== 游标分页: 逐页翻菜单 ===")    cursor = None    for page_no in range(1, 5):        chunk, cursor = api.list_cursor(cursor, page_size=6)        ids = [d["id"] for d in chunk]        print(f"第{page_no}页: {ids}, 下一个游标={cursor}")        if cursor is None:            break     print("\n=== 过滤: 只看凉菜 ===")    cold, total_cold = api.list_offset(page=1, page_size=5,                                       category="凉菜")    print(f"第1页凉菜: {[d['name'] for d in cold]}, 共{total_cold}道")
目的:用“数字餐厅”的菜品数据模拟API服务端,对比偏移分页与游标分页的实现差异,并演示服务端过滤与结构化错误返回。
  1. filtered = [d for d in self.dishes if ...]服务端先按 category 过滤,再分页,绝不把全量菜单倒给客户端。
  2. start = (page - 1) * page_size偏移分页核心:把页码换算成“跳过的行数”。
  3. chunk[-1]["id"] if chunk else None把本页最后一项的ID当作新的游标,作为下一页的书签。
  4. next_cursor = chunk[-1]["id"] if chunk else None当没有更多数据时返回 None,客户端据此停止翻页。
  5. def make_error(self, code, message, trace_id)返回结构化错误体,正是正文第5节“道歉四件套”的代码形态。
  6. if cursor is None: break游标为 None 表示菜单翻完了,结束循环。

常见误区

误区“我觉得 `GET /api/v1/getUserInfo?id=42` 已经是RESTful风格了,毕竟动词写在URL里,多清楚呀。”初学REST时,很难理解为什么看起来“更清晰”的写法反而是反模式。

正解动词应由HTTP方法承载:读取用GET,创建用POST,整体更新用PUT,局部更新用PATCH,删除用DELETE。URL只表达“对哪个名词资源操作”。getUserInfo 的问题在于把动作编码进路径,破坏了资源的统一抽象,客户端无法凭路径把它识别为一个“用户资源”。

为何常见:很多早期框架和示例都这么写,且短期看“可读性好”,容易形成路径惯性。

误区“不就是在路径里加个 `/v1/` 吗?以后改了加 `/v2/`,旧的别删就行。”把版本控制当成URL命名游戏,忽略了兼容性契约与弃用管理。

正解版本化的核心是承诺:v1一旦发布就冻结语义,只做bug修复;新的不兼容变更进v2。同时必须维护弃用周期:提前通知、灰度流量、给出迁移文档,最后才下线。没有这些纪律,/v1/ 与 /v2/ 会演变成两个行为漂移的孪生接口,让客户端无所适从。

为何常见:大家讨论版本控制时第一反应是URL的写法,而“通知-并存-迁移-下线”这套软治理,需要踩过线上事故的坑才能真正理解。

误区“GraphQL按需查询那么强大,面试里是不是应该无脑推荐GraphQL?”认为新技术必然替代旧技术。

正解GraphQL在缓存、服务端性能(N+1问题)、上传下载监控、安全控制上都比REST复杂。公开API、以CDN缓存为主的场景,REST依然是更优选择。面试答题要展示权衡思维:客户端多样性、数据聚合复杂度、流量敏感性、团队维护成本,缺一不可。

为何常见:GraphQL的宣传语太有吸引力,初学者容易只看到优点而忽视代价,这在面试中恰好是扣分项。

误区“为了前端统一处理,不管成不成功状态码都返200,用业务码区分逻辑错误。”部分公司内部确实这么约定,让面试者误以为这是最佳实践。

正解这会摧毁HTTP中间件、代理、监控的语义。CDN、网关、监控系统都依赖状态码判断服务健康度;200加错误码会让真实的故障淹没在“假成功”里。正确做法是状态码表达“结果类别”(4xx/5xx),业务码表达“具体原因”,两者配合。

为何常见:前端“只处理200”能少写分支,且部分BFF架构确实在网关层做了状态码归一化,被误当成了通用方案。

误区“数据量不大时offset挺好用,没必要学什么游标。”忽视了数据增长后的深翻页灾难。

正解offset深翻页要扫描并丢弃大量行,在MySQL里 page=1000000&limit=20 会引发严重的延迟与IO。游标分页基于有序索引定位,复杂度为对数级,在大数据量、实时写入场景是唯一可靠方案。代价是失去直接跳页能力,产品交互要从“第N页”改为“加载更多”。

为何常见:多数教程从单表小数据出发,offset实现只有两行代码,而游标分页必须依赖业务排序字段,初学成本更高。

面试题

Q1. RESTful架构中,GET /api/v1/users/42 最恰当的含义是?

单选题 · easy

  • A.创建一个编号42的新用户
  • B.读取编号42用户的资料← 正确答案
  • C.删除编号42的用户账号
  • D.更新编号42用户的全部字段

答案:B。GET对应读取(R),所以是读取编号42的用户资料。POST才对应创建,DELETE对应删除,PUT对应全量更新。类比服务员报菜名:GET是“问问这道菜是啥”,不会改变菜单。

Q2. 下列哪个状态码表示“资源不存在”?

单选题 · easy

  • A.400 请求参数有误
  • B.403 服务器明确拒绝
  • C.404 未找到该资源← 正确答案
  • D.500 服务器内部出错

答案:C。404表示“目标资源根本没有”,好比顾客点了一道不在菜单上的菜。400是请求格式错,403是没权限,500是服务器自己出错了。

Q3. 下列哪个不是标准的HTTP方法?

单选题 · easy

  • A.GET 获取资源数据
  • B.POST 创建新资源
  • C.FETCH 远程获取数据← 正确答案
  • D.PATCH 部分更新资源

答案:C。HTTP标准方法有GET、POST、PUT、PATCH、DELETE等。FETCH是浏览器提供的JavaScript API,不属于HTTP方法。这题考察对HTTP标准方法的边界是否清晰。

Q4. 游标分页相比偏移分页的最大优势是?

单选题 · mid

  • A.支持用户跳转任意页码
  • B.大数据量下稳定且高效← 正确答案
  • C.过滤条件实现更简单
  • D.对数据库索引要求更低

答案:B。游标分页基于有序索引定位,深翻页时不会像offset那样扫描海量行再丢弃。代价是失去直接跳页能力——它记住了“看到哪”而非“第几页”,而过滤条件和索引要求反而更高。

Q5. 语义化版本号 2.3.1 中,哪个位置变化表示不兼容的破坏性变更?

单选题 · mid

  • A.主版本号 2 发生变化← 正确答案
  • B.次版本号 3 发生变化
  • C.修订号 1 发生变化
  • D.版本号内容全部变化

答案:A。主版本号(Major)变化代表破坏性变更,好比餐厅撤掉招牌菜;次版本(Minor)是向后兼容的新功能,补丁(Patch)是向后兼容的修复。三者语义不同,不能混为一谈。

Q6. 以下哪些HTTP方法是幂等的?

多选题 · mid

  • A.GET 读取资源数据← 正确答案
  • B.POST 创建新资源
  • C.DELETE 删除目标资源← 正确答案
  • D.PUT 全量更新资源← 正确答案

答案:ACD。幂等指“执行一次和执行多次结果一致”。GET重复读取结果相同;DELETE删一个不存在的资源依然返回同样的结果;PUT全量替换后结果稳定。POST每次都会创建新资源,多执行一次就多一份,不幂等——好比每次喊“再来一份红烧肉”,后厨就真做一份。

Q7. API返回4xx客户端错误时,客户端首选的做法是?

单选题 · mid

  • A.立即无限重试直到成功
  • B.检查错误码修正请求← 正确答案
  • C.强制用户重新登录
  • D.更换服务器地址重试

答案:B。4xx代表客户端的请求有问题,重试只会重复犯错。正确做法是读错误码,修正参数、鉴权或业务逻辑后再发。只有5xx(服务端错)才适合退避重试。这就像点了一道已下架的菜,再喊十遍服务员也不会凭空变出来。

Q8. 部署新版本的API时,下列哪些是推荐的兼容性策略?

多选题 · hard

  • A.只新增字段不删除旧字段← 正确答案
  • B.新老版本并存一段过渡期← 正确答案
  • C.立即强制切换并删除旧版
  • D.提前发布弃用通知再下线← 正确答案

答案:ABD。“只增不改”保持向后兼容;新老版本并存给客户端迁移时间;弃用通知让调用方有预期。强制切换删除旧版是灾难性的——相当于餐厅突然把经典菜单全撕了,老顾客只能饿肚子。

Q9. 深分页时offset很大的主要性能问题是?

单选题 · hard

  • A.返回的数据结构不够稳定
  • B.数据库扫描丢弃效率极低← 正确答案
  • C.游标解码消耗大量CPU
  • D.更频繁触发限流保护

答案:B。offset深翻页时,数据库要先扫描并丢弃前面的所有行,再取目标页,代价随页码线性增长。游标分页通过有序索引直接定位,避免大量无效扫描。这好比找一本1000页的书,offset是每次从第1页翻起,游标是直接翻到书签处。

Q10. GraphQL相比REST的优势体现在哪些方面?

多选题 · hard

  • A.客户端可按需指定字段← 正确答案
  • B.单请求可聚合多个资源← 正确答案
  • C.天然兼容HTTP缓存机制
  • D.类型系统支持自省文档← 正确答案

答案:ABD。A和B是GraphQL的核心卖点:按需取字段、一次请求聚合多资源,解决过度获取与多次往返。D也是优势:GraphQL的强类型schema支持自省(Introspection),可自动生成文档。C是错误项——GraphQL通常走单一POST端点,难以利用HTTP缓存,这正是它的痛点。