从 0 设计一个高并发短链接服务要考虑什么?
一句话结论:核心是“长链接进来换短码、短码访问 302 跳回去”,要把发号、存储、缓存、跳转四件事分开设计:发号保证全局唯一且难枚举(号段/发号器加 base62);存储用 DB 持久化加 Redis 缓存(读多写少,cache-aside);跳转用 302 便于统计与失效控制;再叠加限流、过期清理与监控,就是一个能支撑高并发的方案。
关键设计点
| 模块 | 方案要点 |
|---|---|
| 发号 | 分布式发号器(号段模式/雪花)生成唯一 ID,转 base62 成 6~8 位短码 |
| 去重 | 长链接哈希后查表,命中直接复用旧短码;DB 唯一索引兜底并发 |
| 存储 | MySQL 存短码、长链、创建时间与过期时间;短码建索引 |
| 缓存 | Redis 存短码到长链;未命中回源 DB;防穿透用空值短缓存、防击穿用单飞/互斥 |
| 跳转 | 命中返回 302 Location;不直接用 301,避免客户端缓存导致无法统计与下线 |
| 治理 | 写接口限流、黑白名单、过期任务清理、监控 QPS 与命中率 |
写入与读取主链路
// 读链路:缓存优先
func redirect(short string) (string, error) {
long, err := cache.Get(ctx, short)
if errors.Is(err, cache.Miss) {
long, err = db.FindLong(short) // 回源
if err == nil {
cache.Set(ctx, short, long, ttl) // 回填
}
}
return long, err
}
// 写链路:先发号再落库(库内唯一索引防重复)
id := allocator.NextID() // 全局唯一
code := base62.Encode(id) // 短码
if err := db.Insert(code, longURL); err != nil {
// 冲突则查重并复用已有短码
}
高并发注意事项
- 读多写少:缓存命中率是生命线,热点短链可再加进程内本地缓存挡峰值。
- 发号器要预取号段:一次取一批(如 1 万个)缓存在内存,避免每写一次打一次 DB。
- 统计异步化:点击日志走消息队列或异步落库,别让打点卡在重定向主链路上。
- 过期策略:懒删除(访问时发现过期即删)加定期扫描兜底。
- 防御:短码随机化/加长、限流与风控,防止被恶意遍历枚举。
常见追问 / 记忆点
- 追问:为什么用 302 不用 301?——301 会被浏览器缓存,后续访问不再打服务端,统计与“下架链接”都会失效。
- 追问:短码能直接用长链接的哈希吗?——可以但需处理冲突与碰撞,正规做法仍是发号器保证唯一性。
- 记忆点:发号唯一加 base62;DB 持久化、Redis 缓存;302 跳转便于统计;防穿透击穿并限流。