面试官:"刷微博时点开的 t.cn 短链,背后是怎么实现的?能简单说说吗?"
这道题在大厂架构师面试中出现率高达80%!今天我们就来彻底拆解这个经典系统设计题
一、开篇:理解问题本质
面对百万级用户,系统需要解决四大核心挑战:
- 高并发生成:每秒处理数万次生成请求
- 全局唯一性:保证数十亿短码绝不重复
- 低延迟跳转:毫秒级完成重定向
- 水平扩展:支持业务持续增长
这就像建造一个数字高速公路系统,既要保证每辆车有唯一车牌,又要让所有车辆快速通行
二、核心架构设计
2.1 短链生成算法
三种主流方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 自增ID+Base62 | 无冲突、有序 | 需要中心化ID生成器 | 推荐使用 |
| 雪花算法 | 分布式、高性能 | 可能时钟回拨 | 分布式环境 |
| 哈希算法 | 去重能力强 | 需要解决冲突 | 小规模场景 |
推荐方案:Redis自增ID + Base62编码
@Service
public class ShortUrlGenerator {
@Autowired
private RedisTemplate<String, Long> redisTemplate;
// Base62字符集:数字+大写字母+小写字母
private static final String BASE62 =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
public String generate(String longUrl) {
// 使用Redis原子操作生成全局唯一ID
Long id = redisTemplate.opsForValue()
.increment("short_url_id_counter");
return encodeBase62(id);
}
// Base62编码实现
private String encodeBase62(long num) {
StringBuilder sb = new StringBuilder();
do {
int remainder = (int)(num % 62);
sb.append(BASE62.charAt(remainder));
num /= 62;
} while (num > 0);
return sb.reverse().toString();
}
}
2.2 存储架构设计
MySQL分库分表策略:
-- 按short_code的hash值分1024张表
CREATE TABLE short_url_% (
id BIGINT PRIMARY KEY COMMENT '雪花算法ID',
short_code VARCHAR(8) NOT NULL COMMENT '短码',
original_url VARCHAR(2048) NOT NULL COMMENT '原始URL',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
expire_time DATETIME COMMENT '过期时间',
status TINYINT DEFAULT 1 COMMENT '状态',
UNIQUE KEY uk_code(short_code),
KEY idx_create_time(create_time)
) ENGINE=InnoDB CHARSET=utf8mb4;
多级缓存设计:
# application.yml配置
spring:
redis:
short-url:
timeout: 7200 # 2小时缓存过期
max-size: 1000000 # 缓存100万条热点数据
cache:
type: caffeine # 本地缓存
caffeine:
spec: maximumSize=10000,expireAfterWrite=10m
2.3 高性能重定向实现
Spring Boot重定向接口:
@RestController
public class RedirectController {
@GetMapping("/s/{shortCode}")
public void redirect(@PathVariable String shortCode,
HttpServletResponse response) throws IOException {
String originalUrl = urlService.getOriginalUrl(shortCode);
if (originalUrl == null) {
response.sendError(404, "短链接不存在");
return;
}
// 302临时重定向
response.setStatus(HttpStatus.FOUND.value());
response.setHeader("Location", originalUrl);
// 异步记录访问日志
logService.asyncRecordAccess(shortCode);
}
}
多级查询策略:
@Service
public class UrlService {
@Cacheable(value = "shortUrl", key = "#shortCode")
public String getOriginalUrl(String shortCode) {
// 1. 查询本地缓存(Caffeine)
// 2. 查询Redis集群
// 3. 查询MySQL分表
// 4. 使用布隆过滤器防穿透
return findInStorage(shortCode);
}
}
2.4 安全防护体系
Sentinel限流配置:
@PostMapping("/api/shorten")
@SentinelResource(value = "createShortUrl",
blockHandler = "blockHandler",
fallback = "fallbackHandler")
public Result<String> createShortUrl(@Valid @RequestBody UrlRequest request) {
// 业务逻辑处理
String shortUrl = urlService.createShortUrl(request.getUrl());
return Result.success(shortUrl);
}
// 限流处理
public Result<String> blockHandler(UrlRequest request, BlockException ex) {
log.warn("触发限流保护: {}", request.getUrl());
return Result.error("请求过于频繁,请稍后再试");
}
// 降级处理
public Result<String> fallbackHandler(UrlRequest request, Throwable ex) {
return Result.error("系统繁忙,请重试");
}
综合防刷策略:
- IP限流:每个IP每分钟最多100次生成
- 用户认证:重要操作要求登录验证
- 内容安全:URL恶意检测和过滤
- 验证码:高频操作需要图形验证码
三、扩展功能设计
3.1 自定义短链功能
@Service
public class CustomUrlService {
public boolean createCustomUrl(String customCode, String originalUrl) {
// 检查自定义码格式
if (!isValidCustomCode(customCode)) {
throw new IllegalArgumentException("自定义码格式错误");
}
// 使用Redis分布式锁防止并发冲突
String lockKey = "lock:custom:" + customCode;
try {
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(5));
if (Boolean.TRUE.equals(locked)) {
// 检查是否已存在
if (isCodeExists(customCode)) {
return false;
}
// 保存到数据库
saveCustomUrl(customCode, originalUrl);
return true;
}
return false;
} finally {
redisTemplate.delete(lockKey);
}
}
}
3.2 点击统计与分析
// 使用RocketMQ异步处理点击统计
@Component
public class ClickStatisticService {
@Resource
private RocketMQTemplate rocketMQTemplate;
public void recordClick(String shortCode, HttpServletRequest request) {
ClickEvent event = new ClickEvent();
event.setShortCode(shortCode);
event.setIp(request.getRemoteAddr());
event.setUserAgent(request.getHeader("User-Agent"));
event.setReferer(request.getHeader("Referer"));
event.setTimestamp(System.currentTimeMillis());
// 异步发送到消息队列
rocketMQTemplate.sendOneWay("short_url_clicks", event);
}
}
// 点击事件消费者
@RocketMQMessageListener(consumerGroup = "click-consumer", topic = "short_url_clicks")
public class ClickConsumer implements RocketMQListener<ClickEvent> {
@Override
public void onMessage(ClickEvent event) {
// 批量入库或更新统计信息
statisticService.updateClickStatistic(event);
}
}
四、面试陷阱与加分项
4.1 常见陷阱问题
问题1:"DB主从延迟导致新生成的短链查不到怎么办?"
参考答案:
- 写操作后一段时间内强制读主库
- 新生成的短码在Redis设置短期缓存
- 使用数据库读写分离中间件
问题2:"如何避免哈希冲突?"
参考答案:
- 自增ID方案天然无冲突
- 哈希方案需要重试机制和盐值
- 布隆过滤器快速判断是否存在
问题3:"短链过期机制怎么设计?"
参考答案:
- Redis设置TTL自动过期
- 定时任务清理过期数据
- 软删除+物理删除结合
4.2 面试加分项
-
业界实践参考:
- Bitly:NSQ消息队列 + Cassandra存储
- 微博t.cn:多层缓存 + 异地多活部署
-
容量预估:
- 每亿条数据约需要500GB存储空间
- 性能指标:QPS 10万+,跳转延迟<50ms
-
监控体系:
- 生成成功率监控
- 跳转延迟监控
- 错误率告警
五、总结与互动
核心设计哲学:用空间换时间,用缓存抗流量,用异步保性能
记住这个架构公式:自增ID + Base62 + 多级缓存 + 分库分表 + 异步化 = 高性能短链系统
思考题:如果是你,会怎么优化这个系统?欢迎在评论区分享你的架构思路!
关注我,每天搞懂一道面试题,助你轻松拿下Offer!