引言:
在社交媒体营销、短信推广等场景中,短链接服务已成为互联网基础设施的关键组件。全球每天有数十亿短链接被创建,如Bitly、TinyURL等服务每天处理数十亿请求。作为一名拥有 8年经验的Java架构师,我曾主导设计过日处理千万级短链接的系统。今天我将从原理到实现,深度解析如何构建一个高性能、高可用、可扩展的短链接服务。
bit.ly/3xY8zK1
)挑战维度 | 具体问题 |
---|---|
高并发 | 瞬时生成/跳转请求峰值可达10万QPS |
低延迟 | 跳转操作需在50ms内完成 |
海量数据存储 | 百亿级URL关系存储与快速检索 |
短码碰撞 | 避免不同长URL生成相同短码 |
防恶意攻击 | 防止短码遍历、刷量等攻击行为 |
客户端 → API网关 → 生成服务 → Redis缓存 → MySQL分库
↓ ↓
→ 跳转服务 → 埋点统计 → Kafka → 数仓
方案 | 优点 | 缺点 |
---|---|---|
哈希算法 | 生成速度快 | 存在碰撞风险 |
自增ID+进制转换 | 无碰撞、长度可控 | 需分布式ID生成器 |
预生成号池 | 高性能、零延迟 | 需提前分配、浪费风险 |
推荐方案:分布式ID生成器(Snowflake变体)+ Base62编码
// 短码-长URL映射存储
public class UrlMapping {
private String shortCode; // 短码(主键)
private String originUrl; // 原始URL
private long createTime; // 创建时间
private int expireDays; // 过期天数
private int clickCount; // 点击统计
}
存储策略:
/**
* 基于Snowflake的分布式ID生成器
*/
public class ShortCodeGenerator {
// 组成:时间戳(41bit) + 机器ID(10bit) + 序列号(12bit)
private static final long EPOCH = 1672531200000L; // 2023-01-01
public String generate(String longUrl) {
long id = snowflake.nextId(); // 获取分布式ID
return base62Encode(id);
}
// Base62编码(0-9a-zA-Z)
private String base62Encode(long num) {
char[] map = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
StringBuilder sb = new StringBuilder();
while (num > 0) {
sb.append(map[(int)(num % 62)]);
num /= 62;
}
return sb.reverse().toString(); // 如 "3xY8zK1"
}
}
@RestController
public class RedirectController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private UrlMappingRepository repository;
/**
* 短链接跳转(千万级QPS核心路径)
*/
@GetMapping("/{shortCode}")
public ResponseEntity redirect(
@PathVariable String shortCode,
HttpServletRequest request) {
// 1. 从Redis查询缓存
String originUrl = redisTemplate.opsForValue().get(shortCode);
// 2. 缓存未命中查询数据库
if (originUrl == null) {
UrlMapping mapping = repository.findByShortCode(shortCode);
if (mapping == null) return ResponseEntity.notFound().build();
originUrl = mapping.getOriginUrl();
// 写入缓存(设置TTL)
redisTemplate.opsForValue().set(shortCode, originUrl, 24, TimeUnit.HOURS);
}
// 3. 异步记录访问日志(非阻塞)
CompletableFuture.runAsync(() ->
trackAccess(shortCode, request)
);
// 4. 返回302重定向
return ResponseEntity.status(HttpStatus.FOUND)
.location(URI.create(originUrl))
.build();
}
// 访问统计埋点
private void trackAccess(String shortCode, HttpServletRequest request) {
AccessLog log = new AccessLog();
log.setShortCode(shortCode);
log.setIp(request.getRemoteAddr());
log.setUserAgent(request.getHeader("User-Agent"));
log.setReferer(request.getHeader("Referer"));
log.setAccessTime(System.currentTimeMillis());
// 发送到Kafka
kafkaTemplate.send("access_log", log);
}
}
/**
* 短链安全防护
*/
@Service
public class SecurityService {
// 布隆过滤器(10亿数据,误判率0.1%)
private BloomFilter bloomFilter = BloomFilter.create(
Funnels.stringFunnel(StandardCharsets.UTF_8), 1_000_000_000, 0.001);
/**
* 校验短码合法性
*/
public boolean isValidCode(String shortCode) {
// 1. 长度校验(6-8字符)
if (shortCode.length() < 6 || shortCode.length() > 8)
return false;
// 2. 布隆过滤器校验
if (!bloomFilter.mightContain(shortCode))
return false;
// 3. 频率限制(Redis计数器)
String key = "rate_limit:" + shortCode;
Long count = redisTemplate.opsForValue().increment(key, 1);
redisTemplate.expire(key, 1, TimeUnit.MINUTES);
return count <= 100; // 每分钟最多100次访问
}
}
缓存技巧:
-- 按短码哈希分表(1024张表)
CREATE TABLE url_mapping_1023 (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
short_code VARCHAR(8) NOT NULL COMMENT '短码',
origin_url VARCHAR(2048) NOT NULL COMMENT '原始URL',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY idx_short_code (short_code) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
分片策略:
shard = CRC32(short_code) % 1024
// 基于Flink的实时统计
public class AccessStatsJob {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.addSource(new FlinkKafkaConsumer<>("access_log", new AccessLogDeserializer(), props))
.keyBy(AccessLog::getShortCode)
.window(TumblingProcessingTimeWindows.of(Time.minutes(5)))
.aggregate(new StatsAggregator())
.addSink(new RedisSink());
}
// 聚合统计
private static class StatsAggregator implements AggregateFunction {
public StatsAcc createAccumulator() {
return new StatsAcc();
}
public StatsAcc add(AccessLog log, StatsAcc acc) {
acc.count++;
// 按设备/地区/来源统计
return acc;
}
public StatsResult getResult(StatsAcc acc) {
return new StatsResult(acc);
}
}
}
API示例:
@PostMapping("/create")
public ApiResponse createUrl(@RequestBody CreateRequest request) {
// 1. URL规范化处理
String normalizedUrl = UrlUtils.normalize(request.getUrl());
// 2. 检查是否已存在(防重复生成)
String existCode = repository.findByOriginUrl(normalizedUrl);
if (existCode != null) {
return ApiResponse.success(existCode);
}
// 3. 生成短码并存储
String shortCode = generator.generate(normalizedUrl);
repository.save(new UrlMapping(shortCode, normalizedUrl));
return ApiResponse.success(shortCode);
}
指标类型 | 监控项 | 报警阈值 |
---|---|---|
性能 | 跳转P99延迟 | >100ms |
可用性 | 5xx错误率 | >0.1% |
数据一致性 | 缓存-数据库不一致率 | >0.01% |
多级备份:
降级策略:
流量调度:DNS故障转移+多AZ部署
架构师思考:
短链接服务看似简单,实则处处暗藏玄机:
算法设计:如何在碰撞概率与性能间取得平衡?
存储优化:百亿级数据如何做到毫秒级查询?
高并发挑战:如何设计无状态服务应对流量洪峰?