1. 短URL系统的核心挑战与设计目标
当我们需要设计一个百亿级短URL系统时,最直观的问题就是:如何在极短的字符串长度内(通常6-8个字符)确保海量链接的唯一性?这就像要在有限的停车位上停放无限增长的车辆——看似不可能完成的任务。
短URL系统的核心指标有三个维度:
- 唯一性:每个长链接必须对应唯一的短码,且不同长链接绝不能产生相同短码
- 抗冲突性:系统需要预防或处理哈希碰撞的情况
- 可扩展性:当短码耗尽时需要能平滑扩容
传统方案如自增ID(1,2,3...)直接编码会暴露业务量,且长度不可控。而随机字符串方案虽然隐藏了序列信息,但面临严重的碰撞概率问题——根据生日悖论,当短码数量达到√N量级时(N为可能组合数),碰撞概率就会急剧上升。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 短码生成的核心算法选型
2.1 Base62编码的原理与实现
主流短码采用Base62编码(A-Z,a-z,0-9共62个字符),相比Base64少了"+""/"两个特殊字符。其核心是将长整型ID转换为62进制表示:
python复制BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
def encode(num):
if num == 0:
return BASE62[0]
arr = []
while num:
num, rem = divmod(num, 62)
arr.append(BASE62[rem])
return ''.join(reversed(arr))
6位Base62可表示62^6≈568亿种组合,满足百亿级需求。但直接使用自增ID编码会暴露业务规模,需要引入混淆机制。
2.2 分布式唯一ID生成方案
确保唯一性的核心在于生成全局唯一的原始ID。常见方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 数据库自增ID | 简单可靠 | 单点瓶颈,扩展性差 |
| UUID | 去中心化生成 | 无序且长度过长 |
| Snowflake算法 | 趋势递增,分布式友好 | 依赖时钟同步 |
| 号段模式 | 性能极高 | 需要维护号段状态 |
推荐组合方案:改良版Snowflake(64位)= 1位符号位 + 41位时间戳 + 10位机器ID + 12位序列号。通过时间戳保证趋势递增,机器ID避免集群冲突,序列号应对单机并发。
3. 防冲突的工程实践
3.1 预生成与缓冲池机制
为避免实时生成时的竞争,可采用预生成策略:
- 后台服务批量生成ID并编码为短码
- 将短码存入Redis缓冲池(SET结构)
- 应用层直接从缓冲池取用短码
java复制// 预生成短码示例
public void preGenerateCodes(int batchSize) {
List<Long> ids = idGenerator.batchGenerate(batchSize);
List<String> codes = ids.stream().map(Base62::encode).collect(Collectors.toList());
redisTemplate.opsForSet().add("short_code_pool", codes.toArray());
}
3.2 实时生成的重试机制
当用户请求短链时:
- 生成短码并查询数据库是否存在
- 若存在则重新生成(最多重试3次)
- 仍冲突则启用扩展字符(如增至7位)
python复制def generate_short_code(retry=3):
for _ in range(retry):
code = base62_encode(generate_id())
if not db.exists(code):
return code
return base62_encode(generate_id(), length=7) # 扩展位数
4. 存储层的唯一性保障
4.1 数据库唯一索引
必须在短码字段建立唯一索引,这是最后防线:
sql复制ALTER TABLE short_urls ADD UNIQUE INDEX idx_code (short_code);
4.2 布隆过滤器的应用
在查询数据库前先用布隆过滤器判断是否存在:
- 如果布隆过滤器返回不存在,则肯定不存在
- 如果返回存在,则可能误判(需二次查库)
java复制// 使用Guava的布隆过滤器
BloomFilter<String> filter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1_000_000, // 预期元素数量
0.01 // 误判率
);
// 查询时先检查过滤器
if (!filter.mightContain(code)) {
return false; // 绝对不存在
}
5. 性能优化与监控
5.1 缓存热点短链
使用两级缓存策略:
- 本地缓存(Caffeine):存储高频访问的短链
- 分布式缓存(Redis):存储全量映射关系
yaml复制# Spring Cache配置示例
caffeine:
spec: maximumSize=10_000,expireAfterWrite=1h
redis:
timeToLive: 24h
5.2 监控关键指标
需要持续监控的核心指标包括:
- 短码生成速率(/秒)
- 缓冲池剩余量
- 冲突发生率
- 平均重定向延迟
推荐使用Prometheus+Grafana搭建监控看板,设置以下告警规则:
- 缓冲池剩余量低于10%
- 冲突率连续5分钟>0.1%
- P99重定向延迟>100ms
6. 扩展性与容灾设计
6.1 字符集扩容方案
当62字符不够时,可以:
- 增加字符集(如Base64)
- 增加短码长度(6→7位)
- 引入校验位(如Luhn算法)
python复制def upgrade_encoding(original_code):
# 示例:6位Base62转7位Base36
num = base62_decode(original_code)
return base36_encode(num, length=7)
6.2 多活架构设计
对于金融级系统,建议:
- 在不同地域部署生成节点
- 每个节点配置不同的机器ID段
- 通过GTM实现流量调度
code复制[北京机房]
机器ID范围:0-511
[上海机房]
机器ID范围:512-1023
我在实际项目中遇到过因NTP时钟回拨导致的ID重复问题。最终解决方案是在检测到时钟回拨时,短暂拒绝服务并报警,而不是冒险生成可能重复的ID。这提醒我们:分布式系统没有银弹,每个方案都需要配套的异常处理机制。
