短链接背后的技术细节:生成、冲突与高并发设计
作者:Nicky2026.11.07 02:06浏览量:0简介:一条只有几个字符的短链接,背后串联着重定向原理、短码生成算法、哈希冲突处理与亿级高并发缓存架构。本文系统拆解短链接的完整技术链路:为什么不能直接截断长链接、哈希与发号器两条路线如何取舍、冲突如何检测与规避、一亿条短链接如何稳定支撑高频跳转,帮你把这道 Java 后端高频面试题彻底吃透。
一、从一条“超长链接”说起
下午,运营同学把一条准备投放的链接发了过来:域名后面拖着长长的路径,还挂着一串查询参数,整个 URL 上百个字符,复制到短信里显得笨重又昂贵。她问我:能不能给链接“办张瘦身卡”?
这个需求看起来只是“把字符串变短”,但真要落地,它是一道完整的后端系统设计题,至少包含四个追问:
- 短链接和长链接到底差在哪?跳转流程是怎样的?
- 短码怎么生成,才能又短、又唯一、又不容易被猜到?
- 如果两个不同的长链接生成了相同的短码(冲突),怎么发现、怎么解决?
- 数据量到一亿条、跳转请求量很高时,系统靠什么扛住?
下面按照“原理 → 生成 → 冲突 → 存储 → 高并发 → 工程细节”的顺序,把这些问题一次讲透。
二、短链接的本质:编号登记 + HTTP 重定向
先看一组示例(域名为示意占位):
- 长链接:
https://<业务域名>/product/detail?id=10086&source=sms&campaign=summer - 短链接:
https://<短链域名>/a8K2p
两者最终指向同一个页面。区别在于:短链接用一串极短的标识符(短码)代替了原始 URL。用户访问短链接时,短链服务根据短码查出原始地址,再通过 HTTP 重定向把用户带到目标页面。
完整流程只有五步:
- 用户点击短链接,请求到达短链服务;
- 服务从路径中解析出短码(如
a8K2p); - 先查缓存、再查存储,取出短码与长 URL 的映射记录;
- 服务返回 302 响应,
Location响应头指向原始长链接; - 浏览器对
Location中的地址再次发起请求,到达真正的业务页面。
这个过程就像酒店前台:客人只需要报一个简短的房间编号,前台翻一下登记簿,就能告诉他去几楼。短码是房间编号,数据库是登记簿,短链服务就是前台。
有三个非常容易混淆、也常被面试官追问的点,必须先澄清:
- 长短链接说的是 URL 的长度,与 TCP 连接的寿命无关。“长连接/短连接”描述的是连接是否复用,属于传输层话题;“长链接/短链接”是一次业务层面的重定向流程。两者只是名字相近,没有因果关系,这是面试中的高频混淆点。
- 短链接不一定比长链接访问更快。如果长链接可以直达业务服务器,而短链服务后面串着网关、缓存、数据库和一次额外的重定向,链路反而更长,访问延迟通常更高。短链接的核心价值不在于“快”,而在于“短”:字符少、好传播、好排版,在短信这类按字符计费的场景里还能直接省成本。
- 短链接的另一个核心价值是“统一管控”。所有流量都经过短链服务这个统一入口,天然具备点击统计、A/B 分流、灰度控制、风控拦截的能力,这是裸长链接给不了的。
三、302 还是 301:一个必须想清楚的取舍
重定向状态码有两个候选:
- 301(永久重定向):浏览器会缓存这个映射,之后用户再点同一个短链,请求根本不会到达短链服务。服务端压力最小,但也统计不到后续点击了。
- 302(临时重定向):每次点击都会回到短链服务,服务端能完整记录每一次访问。
绝大多数业务型短链系统选择 302:跳转服务的读压力主要靠缓存消化,而点击数据是运营的命根子。301 只适合“确定不需要统计”的静态场景。
四、短码怎么生成:截断不可行,两条主流路线
先排除一个错误答案:直接截断。
假设原始 URL 是 https://<业务域名>/product?id=10086,从前往后截取若干字符,得到的可能只是一段残缺的协议前缀——它既不能唯一定位目标页面,甚至都不是一个合法 URL。截断破坏的是信息本身,而短链接需要的是“生成一个短标识符,再把标识符映射回原始 URL”。
业界主流有两条生成路线。
路线一:哈希 + Base62 编码
对原始 URL 计算哈希值(业界常用的 MD5、Murmur 等通用哈希算法均可),取散列值的一部分,再映射到 62 字符表(0-9、a-z、A-Z)上,得到 6~7 位短码。
编码空间是足够的:62 的 6 次方约 568 亿,62 的 7 次方约 3.5 万亿,仅 6 位短码的理论容量就能支撑亿级数据。
Base62 编码的示意代码:
public class Base62 {private static final String ALPHABET ="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";public static String encode(long value) {StringBuilder sb = new StringBuilder();while (value > 0) {sb.append(ALPHABET.charAt((int) (value % 62)));value /= 62;}return sb.reverse().toString();}}
- 优点:实现简单,不依赖任何中心组件;相同输入得到相同短码,天然具备幂等性。
- 缺点:存在哈希冲突的可能,必须配合查重与兜底逻辑。
路线二:发号器(全局唯一 ID)
不从“内容”推导短码,而是为每个新链接分配一个全局唯一号码,再把号码转成 Base62:
- 数据库自增主键:最简单的发号方式,可靠;但短码连续、可预测,存在被外部遍历猜测的风险,写压力也集中在单库。
- 分布式 ID 生成器:以雪花算法为代表的“时间戳 + 机器位 + 序列号”方案,趋势递增、全局唯一、不依赖单点,性能高;号码不完全连续,安全性更好。
预生成号池:离线批量生成一批合法短码放入号池(可借助消息队列缓冲),线上创建链接时直接取号。延迟低、削峰填谷,但要维护号池的供给速度与存量监控。
优点:号码天然唯一,完全不需要查重流程。
- 缺点:引入发号组件,复杂度略高;同一长链接多次提交会得到不同短码,若需要幂等,得额外按长链接查重。
两条路线的对比:
| 维度 | 哈希方案 | 发号器方案 |
|---|---|---|
| 唯一性 | 可能冲突,需检测兜底 | 天然唯一 |
| 幂等性 | 相同 URL 得到相同短码 | 需额外查重实现 |
| 组件依赖 | 无 | 依赖发号服务/号池 |
| 可预测性 | 短码随机,不易被遍历 | 号码有规律,需混淆 |
| 实现成本 | 低 | 中 |
工程实践中常见的组合拳是:发号器保证唯一性,Base62 压缩长度,再对号码做一次混淆映射(如位运算打乱),消除“连号可猜”的问题。
五、冲突怎么办:检测、规避与兜底
如果选择了哈希路线,冲突处理就是必答题,可以分三层来解。
第一层,标准检测流程:
- 计算候选短码;
- 到存储中查询该短码是否已存在;
- 不存在,直接写入映射;
- 存在,比对已存的长链接与本次是否相同:相同则直接复用已有短码(顺便实现了幂等);不同则确认冲突,追加随机盐或更换散列截取位数后重新计算,循环直到不冲突为止。
第二层,用布隆过滤器为判重提速:把全量短码灌入布隆过滤器。它的特性是“判不存在必然准确,判存在可能误判”——对不存在的新短码,直接写入,省掉一次数据库查询;对“可能存在”的短码,再回源精确比对。亿级短码在布隆过滤器中的内存占用依然可控,判重开销能从毫秒级降到微秒级。
第三层,数据库唯一索引做最后防线:短码列建唯一索引,即使并发场景下两个请求同时算出同一个短码,也只有一个能写入成功,另一个触发唯一约束冲突后走重试逻辑。缓存和布隆过滤器都可能失误,唯一索引不会。
六、存储设计:表结构与分片
核心映射表的示意结构:
CREATE TABLE url_mapping (id BIGINT PRIMARY KEY,short_code VARCHAR(16) NOT NULL,long_url TEXT NOT NULL,status TINYINT NOT NULL DEFAULT 1,expire_time DATETIME,create_time DATETIME NOT NULL,UNIQUE KEY uk_short_code (short_code));
几个设计要点:
- short_code 加唯一索引:它既是查询的唯一入口,也是并发写入冲突的最终防线;
- 查询路径只依赖短码:热点数据完全可以被缓存挡住,冷数据才回库;
- 亿级数据不宜单库单表:通常按 short_code 哈希做分库分表,让读写压力均匀打散;
- 大字段独立考虑:long_url 用大字段存储,如果统计字段、备注字段不断膨胀,可以垂直拆分成“映射主表 + 扩展表”,避免查询放大。
七、一亿条短链,如何扛住高并发
先给系统定性:短链系统是典型的读多写少——创建短链是低频操作,跳转是海量高频读。所以设计主旋律只有一句话:尽一切可能把读请求挡在数据库之外。
1. 多级缓存架构
- 第一级:进程内本地缓存,拦截最热的头部短码,读取延迟纳秒级;
- 第二级:分布式缓存(内存型缓存中间件),承载绝大部分读流量;
- 第三级:数据库,只承接缓存未命中的长尾请求。
跳转查询的示意代码:
public String resolve(String code) {// 1. 本地缓存String url = localCache.getIfPresent(code);if (url != null) {return url;}// 2. 分布式缓存url = distributedCache.get("url:" + code);if (url != null) {localCache.put(code, url);return url;}// 3. 布隆过滤器前置拦截,防穿透if (!bloomFilter.mightContain(code)) {return null; // 一定不存在,无需回源}// 4. 查库并回填缓存UrlMapping mapping = urlMapper.findByCode(code);if (mapping == null) {distributedCache.set("url:" + code, "", shortTtl); // 空值缓存return null;}distributedCache.set("url:" + code, mapping.getLongUrl(), ttl);localCache.put(code, mapping.getLongUrl());return mapping.getLongUrl();}
2. 缓存穿透的三道防线
恶意攻击或大量失效短码会反复“查不存在的短码”,试图打穿缓存直击数据库:
- 布隆过滤器:不存在的短码在入口就被拦下;
- 空值缓存:对确认不存在的短码缓存一个短 TTL 的空值,同一攻击反复查也打不到库;
- 入口校验:短码的长度、字符集不合法的请求直接拒绝,连布隆过滤器都不必查。
3. 一致性与生命周期
- 映射创建成功后立即写入缓存,避免“刚创建就被访问”的未命中抖动;
- 短链支持
expire_time,缓存 TTL 与过期时间取较小值,跳转前还要校验状态位; - 缓存整体故障时降级限流、直查数据库,保住可用性,牺牲部分性能。
4. 扩展性与热点
- 跳转服务本身无状态,可以随意横向扩容;
- 读压力持续增长时,为数据库增加只读副本,分担长尾查询;
- 大促活动的爆款短码会形成热点 key:本地缓存天然能消化大部分热点;必要时对热点 key 做探测与多副本打散,防止单个缓存节点被打满。
八、跳转之外的工程细节
- 安全风控:短链可能被滥用于传播恶意页面。创建时对目标链接做内容安全检测;跳转时校验状态位,违规短码返回拦截页;敏感场景还可以增加“即将离开当前站点”的中间提示页。
- 点击统计:302 跳转的瞬间可以异步记录一条点击日志(先投递到消息队列,再流入后续分析链路),这是短链系统“顺便”获得的数据增值能力,也是运营最看重的能力之一。
- 幂等创建:如果业务要求“同一长链接始终得到同一短码”,可以按长链接的哈希建唯一键,重复创建时直接返回已有记录,既省存储又省号段。
- 监控告警:缓存命中率、布隆过滤器误判率、跳转成功率、平均重定向耗时,都是短链服务的核心健康指标,需要纳入统一监控。
九、面试要点速记
- 短链接 = 短码映射 + HTTP 重定向(一般用 302),“酒店前台与房间号”是标准类比;
- 长短链接不等于长短连接,前者是 URL 长度,后者是 TCP 连接复用;
- 短链接不一定更快,价值在“短”与“统一管控”;
- 生成两条路线:哈希 + Base62(幂等但要处理冲突)对比发号器(唯一但要注意可预测性);
- 冲突三板斧:查重比对 + 加盐重试,布隆过滤器提速,数据库唯一索引兜底;
- 高并发主旋律:读多写少,多级缓存 + 布隆防穿透 + 空值缓存 + 分库分表 + 无状态扩容;
- 加分项:301/302 取舍、安全风控、过期管理、点击统计、热点 key 处理。
把短链接这道题答完整,展示的不是背诵能力,而是“从一个朴素需求推导出一整套架构”的系统设计思维——这正是面试官真正想看到的东西。

登录后可评论,请前往 登录 或 注册