题干与适用场景
设计一个服务:接收 HTTP 或 HTTPS URL,返回短链接;用户访问短链接时,服务跳转到已保存的 目标地址。基础方案支持可选的自定义别名和过期时间。点击分析、自定义域名、账号管理和链接预览 作为追问,不进入核心需求。
采用以下案例假设,确保每项容量结论都能复算:
- 每天创建 100 万条短链接,发生 1 亿次跳转;
- 峰值流量是日均值的 10 倍;
- 除非过期或禁用,链接保留 5 年;
- 跳转链路目标为月可用性 99.99%,服务端 p99 延迟低于 100 ms;
- 索引和副本之前,每条映射平均占 500 字节。
这些数字只是面试案例假设,不是某个产品的实测数据。它们说明系统读多写少,但设计仍要保证并发 创建时短码唯一;创建请求发生不确定超时后能可靠返回同一个结果;过期或恶意链接能在明确的传播 窗口内停止服务。
面试官考察点
第一,看需求是否收敛。好的回答会把创建和跳转与分析、账号功能分开,明确目标地址是否可修改, 并询问过期和自定义别名的语义。需求未定义就先加入队列、搜索引擎或图数据库,会削弱设计依据。
第二,看容量估算是否真正影响选择。假设流量的日均约为每秒创建 12 条、跳转 1200 次,峰值约为 每秒创建 120 条、跳转 12000 次。5 年约保留 18 亿条映射,原始映射数据约 0.9 TB;加上副本、 索引、存储开销和余量,实际配置会是数 TB。这些数量支持使用可分区持久化存储和缓存,但不能为 所有分布式组件提供理由。
第三,看标识符的正确性。8 位 Base62 短码有 62^8、约 218 万亿种取值。保留 18 亿条时, 空间占用率低于 0.001%。单次随机抽样发生碰撞的概率很小,但抽样次数足够多后,系统曾经发生过 某次碰撞的概率会很大。随机性降低了可预测性,并不保证唯一;持久化写入必须原子判断短码不存在, 碰撞后重新生成。
第四,看读链路和故障推导。缓存只是优化,不是真源。过期必须在读取时检查,不能依赖清理任务。 热点短码、缓存故障、数据库超时、重复 POST、跨地域复制延迟、分析积压和紧急封禁都需要明确行为。
最后,安全是跳转服务的核心需求。短链接可能隐藏钓鱼目标,可预测短码也可能被枚举。URL 解析、 协议限制、限流、信誉检查、滥用举报、快速禁用和非连续公开短码,都应进入主设计。
回答前需要澄清的问题
- 创建后允许修改目标地址吗? 基础映射不可变,这会简化缓存与审计。若要编辑,增加版本控制和
严格的缓存失效 SLO。
- 相同长链接是否共用一个短码? 不共用。不同所有者、活动、过期时间和策略可能需要不同链接;
去重应是显式选项,不能成为对目标 URL 做哈希的意外结果。
- 必须支持自定义别名吗? 可选,并且在选定域名内唯一。冲突返回
409,服务不能悄悄修改别名。 - 过期时返回什么? 已知过期或禁用返回
410,从未存在返回404。读取时检查
expires_at,异步删除只负责回收存储。
- 采用哪种跳转状态码? 默认
302,因为映射可能被禁用,策略和分析也可能需要接收每次请求。
只有不可变链接且所有者接受客户端与中间缓存长期保存时,才提供 301。
- 需要何种一致性? 短码预留和自定义别名创建需要强唯一性。已有链接跳转优先可用性,但成功创建
后必须通过填充缓存或读己之写路径立即可读。
- 分析事件必须零丢失吗? 基础方案不包含分析。加入后要单独定义丢失率和新鲜度,分析延迟不能
阻塞跳转。
- 服务会抓取目标内容吗? 跳转链路不会。预览或恶意内容扫描若要抓取 URL,应在具备 SSRF 防护的
隔离异步服务中运行。
30 秒回答框架
“我会把创建和跳转作为两条核心链路。每天创建 100 万条、跳转 1 亿次,对应日均每秒约 12 次写、 1200 次读,10 倍峰值约为 120 和 12000。短码使用密码学安全随机数生成 8 位 Base62,并通过 原子 insert-if-absent 预留;自定义别名采用同一条件。持久化映射按短码哈希分区并作为真源。 跳转服务使用 cache-aside,检查状态和过期时间后返回 302 Location。创建接口具备幂等性,缓存 条目不晚于链接本身过期,修改或封禁会主动推送失效。我会独立扩展热点读链路,防止缓存未命中形成 击穿,把分析放在异步链路。最后用别名竞争、响应丢失重试、热点、缓存与数据库故障、过期边界和封禁 传播来验证明确的 SLO。”
分步骤深入解答
先列出一份简短的容量账本:
创建:1,000,000 / 86,400 ≈ 12/s 日均,10 倍峰值约 120/s
跳转:100,000,000 / 86,400 ≈ 1,200/s 日均,10 倍峰值约 12,000/s
映射:1,000,000 × 365 × 5 = 18.25 亿条
原始数据:18.25 亿 × 500 字节 ≈ 0.9 TB,不含额外开销与副本
短码空间:62^8 = 218,340,105,584,896,占用率仍低于 0.001%核心 API 可以保持很窄:
POST /v1/links
Idempotency-Key: client-generated-key
{ "url": "https://example.com/a", "customAlias": null, "expiresAt": null }
-> 201 { "code": "aZ3kP9qR", "shortUrl": "https://sho.rt/aZ3kP9qR" }
GET /{code}
-> 302 Location: https://example.com/a
-> 404:短码从未存在
-> 410:短码已过期或禁用核心访问模式是按短码点查,因此一条持久化记录只保留创建、跳转和生命周期策略所需字段:
links
code 主键
long_url
owner_id
status ACTIVE | DISABLED
created_at
expires_at 可空
version
create_requests
owner_id + idempotency_key 唯一键
request_fingerprint
code
status
expires_at使用密码学安全随机数生成 8 个 Base62 字符。7 位空间已经约有 3.5 万亿个值,第 8 位用很小的 URL 长度代价换来更大余量,也提高在线枚举成本。随机方案不需要中央数字分配器,也不会公开连续序列, 但仍必须原子条件写入:只有 code 不存在时才插入映射。生成短码冲突时,在有界次数内重新抽样; 自定义别名冲突则返回 409,因为改名会违反调用方合同。
把递增计数器转成 Base62 也是合法方案。只要分配器正确,它能得到唯一紧凑的值,并能通过号段租赁 减少协调;代价是分配器恢复、号段浪费、地域归属和可预测枚举。对长链接做哈希也没有免费解决问题: 截断会碰撞,同一目标可能需要不同链接,处理碰撞最终仍要存储。应先说明所需性质,再在随机码、 租赁计数器和哈希之间选择。
创建链路按以下顺序执行:
- 按需鉴权并限流,解析 URL,只允许
http和https,限制长度和策略,规范化自定义别名。 - 检查幂等键。相同键配不同请求指纹属于冲突;相同请求应返回原结果。
- 生成或接收短码。在同一事务中条件预留短码并保存幂等记录,避免两个创建者同时赢得同一别名,
也避免响应丢失后的重试创建另一条链接。
- 持久化提交后再填充或失效缓存,并投递异步信誉扫描。不能返回只存在于缓存中的短码。
若写请求超时,客户端用同一幂等键重试。服务先读取请求记录,已提交就返回原结果。盲目生成新短码会 把一次不确定响应变成重复持久化状态。如果存储暂时不能证明事务是否提交,返回处理中或可重试结果, 不能先宣告失败再创建新映射。
跳转时,边缘或无状态跳转服务先检查快速传播的封禁清单,再从分布式缓存查询 code。命中后仍要 检查 status 和 expires_at;未命中则从持久化存储点查,执行相同的生命周期检查后写入缓存。 缓存 TTL 不能晚于 expires_at,普通 TTL 加少量抖动,避免大量条目同时过期。不存在的短码可以 短暂负缓存来吸收扫描,但创建同名自定义别名时必须失效对应负缓存。
默认返回带 Location 的 302。HTTP 语义把 302 定义为临时位置,客户端后续仍使用短链接。 301 表示永久新 URI,并可被启发式缓存;它能减少服务流量,也会拖慢撤销、目标修改和逐请求分析。 跳转状态码与 Cache-Control 是产品合同,不能只是服务内部隐藏的性能开关。
持久化层可以是键值数据库,也可以是按 code 哈希分区的关系数据库。核心条件是原子 create-if-absent、持久副本、点查、备份和经过演练的恢复。随机短码能自然分散普通流量,但一个爆款 链接仍会成为热点键。应复制缓存,为极端热点增加小型本地缓存,并合并并发未命中,避免一次过期把 数千个相同请求同时送到数据库。
故障策略必须守住真源边界:
- 缓存不可用时,使用熔断、有界数据库直读、本地热点项和准入控制。无限绕过缓存会把缓存事故升级成
数据库事故。
- 数据库读取不可用时,只有产品接受风险才提供有界的陈旧正缓存;不能延长已过期链接,也不能绕过
封禁清单。
- 持久化写入无法保证唯一时,创建失败。可用性不能成为同一短码对应两个目标的理由。
- 分析延迟时,跳转继续;点击事件按单独的分析合同缓存、采样或丢弃。
- 清理任务停止时,读取仍执行过期判断。存储会增长,但不会继续服务过期链接。
安全校验从创建开始,并持续到创建之后。用真正的 URL 解析器拒绝非 HTTP 协议和畸形 URL;按账号、 网络和风险信号限流;异步扫描目标;提供举报、申诉和快速封禁。短码不能承担私有内容的授权职责。 若目标内容需要鉴权,应由目标系统执行;短码难猜并不等于访问控制。
验证要覆盖性质和故障。让大量创建者争抢同一自定义别名,确认只有一个成功;丢掉第一次 POST 响应, 确认幂等重试得到同一短码;测试过期前一秒、过期时刻和过期后一秒;在负缓存查询后立即创建同名短码; 人工缩小短码空间来触发条件冲突。分别以宽工作集和单一热点短码压到 10 倍峰值,再关闭缓存节点、 限制数据库、延迟失效、停止清理和分析 worker。验收应观察跳转成功率、p99、缓存命中率、数据库 未命中负载、条件冲突、陈旧链接和封禁传播,而不只报告平均吞吐。
高质量示范回答
“我会把基础系统限定为创建和解析短链接,支持可选的自定义别名和过期时间。我先确认目标不可修改、 相同长链接可以得到不同短码、已知过期返回 410,而且分析不会阻塞跳转。
按案例假设,创建日均约每秒 12 次,峰值约 120;跳转日均约每秒 1200 次,峰值约 12000。5 年 约保留 18 亿条映射,按每条 500 字节计算原始数据约 0.9 TB。因此我选择支持点查、分区、副本和 原子条件插入的持久化存储,以 code 作为分区键。
生成链接时,我用密码学随机数生成 8 位 Base62。空间约 218 万亿,因此本规模下单次碰撞概率很低, 但随机不等于唯一。我会用 insert-if-absent 预留短码,随机冲突就重试;自定义别名冲突返回 409。 POST 同时携带幂等键,映射和请求记录一起提交,响应丢失后仍能返回同一短码。
跳转服务先检查封禁清单,再查询缓存。未命中时执行点查,检查启用状态和过期时间,以不超过剩余 有效期的 TTL 缓存,并返回带 Location 的 302。默认采用 302,是因为映射可能被禁用,服务也可能 需要逐请求策略或分析;不可变链接可以选择 301 和更强缓存。负缓存只保留很短时间,创建自定义别名 时主动失效。
持久化映射是真源。缓存故障时只能有界直读数据库并做准入控制;数据库故障时是否提供有界陈旧正缓存 由产品策略决定;无法保证唯一时创建直接失败。热点使用分层缓存和未命中合并。分析与信誉扫描异步, 确认恶意后通过快速控制链路禁用。
我会用并发别名竞争、响应丢失重试、过期边界、负缓存失效、热点和宽工作集压力、缓存故障、数据库 限流、清理故障及封禁传播来证明设计。验收条件是在既定峰值下跳转可用性达到 99.99%、p99 低于 100 ms、没有重复短码赢家、不服务过期链接,并测出明确的禁用传播窗口。”
常见错误
- 对长链接做哈希就认为唯一 → 截断会碰撞,相同 URL 也可能需要不同策略 → **原子预留短码,
并先定义是否去重。**
- 使用随机短码却没有条件写 → 把概率当成保证 → 只在短码不存在时插入,并重试生成冲突。
- 写超时后返回新短码 → 一次客户端操作生成多条链接 → 把重试绑定到持久化幂等键并恢复原结果。
- 先写缓存再写持久化 → 看似成功的链接会随缓存淘汰消失 → 先提交真源,再填充缓存。
- 依赖删除任务处理过期 → 任务延迟就继续服务过期链接 → **所有解析链路都检查
expires_at,
清理只负责回收空间。**
- 把 301 说成“更快”、302 说成“不缓存” → 忽略可变性和显式缓存控制 → **根据产品合同选择
跳转语义和 Cache-Control。**
- 永久缓存 404 → 新建自定义别名仍不可达 → 负缓存使用短 TTL,创建时主动失效。
- 所有缓存未命中都直达数据库 → 热点过期形成击穿 → 使用请求合并、TTL 抖动和分层热点缓存。
- 同步写分析 → 非核心管道故障拖垮跳转 → 解析后再发事件,单独定义分析丢失与时效。
- 把短码当成授权 → 枚举或分享会暴露私有内容 → 目标系统独立鉴权,短码只作为定位符。
追问及应对
追问 1:如何增加近实时点击分析?
跳转决策后发送点击事件,包含 code、事件时间、请求 ID 和隐私政策允许的维度。按短码分区可保持 单链接聚合顺序,极端热点则要拆分。消费者以幂等方式更新分钟和日聚合。选择确认机制前先定义允许的 丢失、重复、新鲜度、保留、机器人过滤和用户同意;跳转不能等待分析存储。
追问 2:如何跨地域 active-active 部署?
通过地域缓存和副本让读取就近。创建仍需全局唯一:使用全局条件写存储、为地域分配不相交的随机或 数字命名空间,或把创建路由到主地域。复制完成前,成功创建需要读己之写路径。封禁元数据应使用比 普通映射复制更快、单独度量的传播链路。
追问 3:若目标地址允许编辑,需要改变什么?
增加带版本条件的更新、审计记录、所有者授权和按短码及版本失效缓存。要明确已被客户端缓存的 301 是否允许陈旧;若快速修改或撤销很重要,默认使用 302 和有界缓存新鲜度。并发编辑要携带预期版本, 避免一个编辑者静默覆盖另一个。
追问 4:单条链接每秒有数百万次请求怎么办?
通过 CDN 或边缘缓存、地域缓存和小型进程内缓存提供服务,同时保持一致的封禁检查。应复制热点值, 不能试图按短码拆分同一个键。合并刷新、提前刷新,并隔离热点流量,避免它耗尽全部缓存或数据库连接。 压测必须覆盖缓存失效,因为最热门的链接也最难快速撤销。
追问 5:自定义域名如何改变键和路由?
唯一范围变为 (domain, code)。需要验证域名所有权、签发证书、按 Host 路由,并保留租户配额和 滥用策略。缓存键和数据库分区键都要包含域名。同一别名存在于两个域名时,不能互相覆盖或失效。
追问 6:法律删除要求立即抹除目标怎么办?
把停止服务和物理删除拆开。先标记禁用、更新封禁清单、失效缓存,并验证每个地域在封禁 SLO 内都 返回 410;之后按保留政策删除或加密擦除持久记录、备份、分析维度、搜索和扫描副本。单靠异步删除 任务无法证明链接已停止解析。
追问 7:如何从 8 位短码迁移到更长短码?
先让解析器接受带版本的多种长度,再改变写入端。新写入可以使用更长短码,旧映射继续原样解析。 分区逻辑不能依赖新格式取消的固定字符位置。监控解析错误和缓存键解析后再淘汰旧写入版本;不能为了 统一长度而重写已公开短码。