6、阿里P7面试官:TinyURL怎么设计?我答了分布式ID,他却让我出门左转
“面试造火箭,工作拧螺丝”,这句调侃背后,藏着大厂面试的残酷真相:他们用“造火箭”的难题,筛选能拧好“精密螺丝”的人。
而“设计一个短链接(TinyURL)服务”,就是那道经典的“火箭题”。
它看似简单,却像一面X光,能瞬间看穿你架构能力的底裤。从初级工程师到架构师,每个人都能说上几句,但只有不到10%的人能真正答到点子上。
今天,我将带你庖丁解牛,让你不仅知道答案,更知道面试官到底想听到什么。
想象一下,你拿到一个需求:
- 生成: 用户输入一个超长的URL,系统返回一个超短的URL。
- 访问: 用户访问这个短URL,能自动跳转到原来的长URL。
这不就是个简单的“长短转换”吗?用个数据库自增ID,再转个格式不就行了?
如果你真的这么想,那这场面试可能已经走远了。面试官真正的意思是:“如果这个服务要支撑1亿用户,每天生成上亿个链接,承载百亿次访问,你该如何设计?” 这才是问题的真正起点。
当面试官抛出这个问题时,他真正想考察的,是你对互联网架构“灵魂三问”的理解:
- 唯一性与扩展性: 海量数据下,如何保证ID的唯一性与高效生成?
- 高性能: 高并发请求下,如何保证服务的读写性能与稳定?
- 高可用: 面对单点故障,如何保证系统7x24小时永不宕机?
这背后,是对分布式ID、缓存、数据库、负载均衡等一系列核心知识的综合运用。下面,我们开始“拆火箭”。
让我们用一问一答的方式,模拟真实的面试场景,逐一攻破难题。
第一关:如何生成唯一的、足够短的short code?
面试官: “说说你的思路,如何生成这个短码?”
❌ 99%的人踩的第一个坑:哈希碰撞法
一个常见的想法是:对长链接做MD5或SHA1哈希,然后截取前6位。
面试官追问: “这会有什么问题?”
你必须答: “这会产生哈希碰撞。虽然概率低,但在上亿的数据量下,碰撞是必然事件。一旦发生,就需要复杂的解决机制,这会让系统变得不可靠。所以,这个方案在工业级应用中是不可接受的。”
✅ 正确的思路:全局唯一ID + 62进制转换
正确的工业级思路分两步:
- 获取一个全局唯一的数字ID。
- 将这个数字ID转换为一个62进制的字符串。
面试官追问: “为什么要用62进制?”
你这样答,会让他眼前一亮: “因为我们的短链接字符库通常包含a-z、A-Z、0-9,共62个字符。使用62进制,能用最短的字符串表示一个巨大的数字。例如,一个6位的62进制数,可以表示 62的6次方,约等于568亿 个不同的ID。这对于绝大多数场景,在很长一段时间内都完全够用了。”
这个问题立刻引出了下一个核心。
面试官: “很好,那这个‘全局唯一的ID’怎么来?用MySQL的自增ID行吗?”
你必须指出瓶颈: “在分布式环境下,MySQL自增ID会成为性能和可用性的核心瓶颈。所有写请求都挤到同一个数据库实例上获取ID,它很快会崩溃,并且一旦宕机,整个服务就瘫痪了。”
面试官: “那你说该怎么办?”
亮出王牌——雪花算法(Snowflake): “我们会采用业界成熟的雪花算法。它能在一个分布式的环境下,高效生成趋势递增的64位long类型ID。它由时间戳 + 机器ID + 序列号组成,完美解决了分布式ID的唯一性和性能问题。”
然后,你可以优雅地甩出一段代码,证明你不是“纸上谈兵”。
/**
* 核心算法:将十进制ID转换为62进制字符串
* 面试时能写出这个,绝对是加分项
*/
public class Base62Converter {
private static final String BASE62_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final int BASE = BASE62_CHARS.length();
/**
* 将 long 类型的 ID 转换为 Base62 字符串
* @param id 全局唯一的ID,例如由雪花算法生成
* @return Base62编码的短字符串
*/
public static String fromBase10(long id) {
if (id == 0) {
return String.valueOf(BASE62_CHARS.charAt(0));
}
StringBuilder sb = new StringBuilder();
while (id > 0) {
// 核心:对62取余,找到对应字符
sb.append(BASE62_CHARS.charAt((int) (id % BASE)));
id /= BASE;
}
// ID倒序追加,所以需要反转
return sb.reverse().toString();
}
}
// 使用示例:
// long uniqueId = snowflakeIdWorker.nextId(); // 假设从雪花算法获取ID
// String shortCode = Base62Converter.fromBase10(uniqueId);
// System.out.println(shortCode); // 输出类似 "AbC123" 的字符串到这里,你已经完美解决了最核心的“唯一性”难题,并展示了你的技术深度。
第二关:如何设计“三高”的系统架构?
面试官: “很好。那当用户访问短链接时,如何保证服务快速又稳定?”
这个问题考察的是你对读多写少场景的架构能力。
你回答: “这是一个典型的读多写少的场景,99%以上的请求都是重定向(读)。因此,缓存是关键。”
面试官追问: “具体说说缓存策略。”
你娓娓道来: “我们会引入分布式缓存(如Redis)。
- 当访问一个短链接时,先查Redis。
- 如果缓存命中,直接从Redis拿到长链接,执行302重定向,流程结束。
- 如果缓存未命中,则查询数据库,然后把结果异步写回Redis(并设置过期时间),再执行重定向。”
面试官继续深挖: “如果有人用不存在的短链接来恶意攻击你呢?”
展现你的严谨: “这是典型的缓存穿透问题。每次都查不存在的key,会导致请求全部打到数据库上。我们的对策是:缓存空值。当查询一个不存在的short code时,我们同样在Redis里缓存一个特殊的空值(并设置一个较短的过期时间,如1分钟)。这样就能有效保护后端数据库。”
面试官: “数据库层面呢?”
你给出最后一击: “数据库层面,采用主从架构,读写分离。所有生成短链接的写操作走主库,所有穿透到数据库的读操作走从库。这样既分担了压力,又实现了数据备份,保证了数据层的高可用。”
经过层层递进的回答,你已经将一个看似简单的问题,演绎成了一场精彩的架构设计秀。最后,你需要给出一个清晰的总结收尾。
“面试官您好,综上所述,一个能支撑海量用户的高可用短链接服务,其核心设计如下:”
唯一Code生成: 采用分布式ID生成器(雪花算法) + Base62编码的方案。
高可用架构:
服务层: 通过负载均衡 + 无状态服务集群实现。
缓存层: 引入分布式缓存(Redis),并使用缓存空值策略防穿透。
数据层: 采用数据库主从/读写分离架构。
最后,你可以画出这张经典的架构图,彻底征服面试官。
+-----------+ 1. 用户请求 +--------------+ 2. 转发请求 +---------------+
| 用户 | ------------> | 负载均衡 | ------------> | 服务集群 |
+-----------+ +--------------+ +-------+-------+
|
+----------------------------------------------------------+
| 3. (生成)获取分布式ID
▼
+---------------------+
| 分布式ID服务(Snowflake)|
+---------------------+
| 4. (生成)写入主库
▼
+---------------------+ 5. 主从同步 +---------------------+
| MySQL主库 (写) | ------------> | MySQL从库 (读) |
+---------------------+ +----------+----------+
| ^
| 7. (访问)异步写回缓存 | 6. (访问)缓存未命中,读从库
▼ |
+---------------------+ |
| Redis缓存 (读/写) | <-------------------------------+
+---------------------+