亿级QPS短链接系统架构设计与核心实现方案
“如何设计一个类似 t.cn/abc123 的短链接系统?”
当面试官提出这个问题时,许多人的初步想法可能是:将长链接存入数据库,生成一个短码,访问时查询并重定向。听起来似乎技术难度不高。
然而,如果面试官进一步要求:系统需支持每日生成1亿个短链接,处理10亿次访问,保证高可用性,且每次跳转延迟必须低于100毫秒——挑战立刻升级。这不再是一个简单的CRUD应用,而是一个典型的高并发、高可用、低延迟的系统架构设计考题。
需求分析:明确核心与边界
功能需求
核心功能聚焦于两点:
一是短链接生成:用户提交原始长URL,系统返回一个对应的短链接。
二是短链接跳转:用户访问短链接,系统需快速、准确地重定向至原长URL。
在此基础上,可扩展增强功能,例如:支持自定义短码、设置链接有效期、提供访问数据统计(访问者、时间、频次等)。
非功能需求:性能与规模的挑战
真正体现设计深度的,是非功能性指标。我们来量化分析:
每日1亿的生成量,平均每秒约1200次写入请求。考虑到业务高峰(如电商大促),峰值写入QPS需按平均值的5倍估算,即需支撑约5000/秒。
访问量更大,每日10亿次,平均读QPS约12000/秒,峰值可能高达50000/秒。这是典型的读多写少场景,读写比例接近10:1。
存储方面,假设每条记录(含长URL、短码、创建时间等)占用1KB,每日新增存储约100GB,一年累积约36TB。这尚未计入索引和访问日志的存储开销。
最后是硬性指标:跳转延迟必须低于100毫秒,系统可用性需达到99.9%以上。短链接服务一旦故障,所有依赖它的营销活动、推广内容将瞬间失效,影响巨大。
架构设计:从核心到全局
核心问题:如何生成短码?
短码生成是系统的基石。主流方案有三种:
方案一:哈希算法
对原始长URL进行MD5或SHA256等哈希运算,截取结果前几位作为短码。
优点是实现简单、计算高效。但致命缺陷是存在哈希冲突风险,在海量数据下虽概率极低,但无法保证绝对唯一,需引入额外的冲突检测与处理机制。
方案二:分布式ID + 进制编码
利用一个全局唯一的自增ID(如分布式ID生成器产生),将该十进制ID转换为62进制(字符集:0-9, A-Z, a-z),所得字符串即为短码。
此方案能保证短码全局唯一,且长度可控(6位62进制可表示超过560亿个ID)。关键在于,分布式ID生成器本身需具备高可用与高性能,不能成为系统瓶颈。
方案三:预生成短码池
系统预先批量生成大量短码,存入“号码池”。当用户请求生成短链时,直接从池中分配一个可用短码。
优点是生成与分配解耦,响应极快。缺点是需要维护池状态(已用/可用),实现复杂度较高,且存在号码浪费的可能。
综合考量唯一性、可扩展性与实现复杂度,方案二(分布式ID + 62进制编码)通常是更优选择。其逻辑清晰,短码长度固定,且易于集成成熟的分布式ID方案(如Snowflake、号段模式)。
整体架构设计
面对十亿级日流量,单体应用直连数据库的方案绝不可行。必须采用分层、分布式、读写分离的架构。
首先,用户请求经过CDN和负载均衡器(如Nginx/LVS)进行流量分发与防护。
核心服务层将“写服务”(生成短链)与“读服务”(跳转查询)物理分离。鉴于读请求量远高于写,读服务可独立进行水平扩容,以应对高并发访问压力。
数据层是核心。持久化存储采用分库分表的MySQL集群,根据短码进行哈希分片,避免单表瓶颈。在数据库之上,构建多级缓存体系:使用Redis集群缓存热点链接数据,并在应用本地使用Caffeine或Guava Cache构建一层极热数据缓存,将绝大多数请求拦截在数据库之外。
此外,需要一个独立、高可用的分布式ID生成服务(发号器),负责产生全局唯一的ID。
核心代码实现
1. 短链接生成服务
服务层核心逻辑,需妥善处理并发、缓存与幂等性。
@Service
@Slf4j
public class ShortUrlService {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private UrlMapper urlMapper;
@Autowired
private IdGeneratorService idGeneratorService;
private static final String SHORT_URL_PREFIX = "short:url:";
private static final String LONG_URL_PREFIX = "long:url:";
// 62进制字符集
private static final String CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
public String createShortUrl(String longUrl) {
// 1. 幂等性处理:相同长URL返回已有短链,节约存储
String longUrlKey = LONG_URL_PREFIX + DigestUtils.md5Hex(longUrl);
String existShortUrl = redisTemplate.opsForValue().get(longUrlKey);
if (existShortUrl != null) {
return existShortUrl;
}
// 2. 获取全局唯一ID
Long id = idGeneratorService.nextId();
// 3. ID转62进制短码
String shortCode = encodeBase62(id);
String shortUrl = "t.cn/" + shortCode;
// 4. 数据持久化
UrlMapping mapping = new UrlMapping();
mapping.setId(id);
mapping.setShortCode(shortCode);
mapping.setLongUrl(longUrl);
mapping.setCreateTime(new Date());
urlMapper.insert(mapping);
// 5. 双写缓存(短码->长链,长链MD5->短链)
redisTemplate.opsForValue().set(SHORT_URL_PREFIX + shortCode, longUrl, 7, TimeUnit.DAYS);
redisTemplate.opsForValue().set(longUrlKey, shortUrl, 7, TimeUnit.DAYS);
return shortUrl;
}
public String getLongUrl(String shortCode) {
// 1. 优先查询缓存
String cacheKey = SHORT_URL_PREFIX + shortCode;
String longUrl = redisTemplate.opsForValue().get(cacheKey);
if (longUrl != null) {
// 异步更新访问统计,不影响主流程性能
asyncUpdateVisitCount(shortCode);
return longUrl;
}
// 2. 缓存未命中,查数据库(含防缓存击穿逻辑)
return getFromDatabaseWithLock(shortCode);
}
private String getFromDatabaseWithLock(String shortCode) {
String lockKey = "lock:" + shortCode;
String longUrl = null;
try {
// 尝试获取分布式锁,防止缓存击穿时大量请求穿透到数据库
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
UrlMapping mapping = urlMapper.selectByShortCode(shortCode);
if (mapping == null) {
// 缓存空值,防止恶意查询持续穿透数据库
redisTemplate.opsForValue().set(SHORT_URL_PREFIX + shortCode, "", 5, TimeUnit.MINUTES);
return null;
}
longUrl = mapping.getLongUrl();
// 回写缓存
redisTemplate.opsForValue().set(SHORT_URL_PREFIX + shortCode, longUrl, 7, TimeUnit.DAYS);
} else {
// 未抢到锁,短暂等待后重试
Thread.sleep(50);
return getLongUrl(shortCode);
}
} catch (Exception e) {
log.error("查询数据库异常", e);
} finally {
redisTemplate.delete(lockKey);
}
return longUrl;
}
// 十进制ID转62进制短码
private String encodeBase62(Long id) {
StringBuilder sb = new StringBuilder();
while (id > 0) {
sb.append(CHARS.charAt((int)(id % 62)));
id /= 62;
}
// 固定补齐到6位长度
while (sb.length() < 6) {
sb.append('0');
}
return sb.reverse().toString();
}
}
2. 控制器实现
控制器负责对外暴露API,逻辑应保持简洁。
@RestController
@Slf4j
public class ShortUrlController {
@Autowired
private ShortUrlService shortUrlService;
@PostMapping("/api/shorten")
public Result createShortUrl(@RequestBody CreateShortUrlRequest request) {
String shortUrl = shortUrlService.createShortUrl(request.getLongUrl());
ShortUrlVO vo = new ShortUrlVO();
vo.setShortUrl(shortUrl);
vo.setLongUrl(request.getLongUrl());
return Result.success(vo);
}
@GetMapping("/{shortCode}")
public void redirect(@PathVariable String shortCode, HttpServletResponse response) throws IOException {
String longUrl = shortUrlService.getLongUrl(shortCode);
if (longUrl == null || longUrl.isEmpty()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "链接不存在或已过期");
return;
}
// 使用302临时重定向,便于后续更新目标地址
response.sendRedirect(longUrl);
}
}
3. 发号器服务(简化版)
发号器核心是生成全局唯一、趋势递增的ID。此处展示基于Redis原子操作的简化版,生产环境建议采用更健壮的方案。
@Service
public class IdGeneratorService {
@Autowired
private RedisTemplate redisTemplate;
private static final String ID_KEY = "short:url:id";
public Long nextId() {
// 生产环境建议使用Snowflake算法或数据库号段模式
return redisTemplate.opsForValue().increment(ID_KEY);
}
}
深度追问:应对面试官的连环提问
Q1:如何防止短链接生成接口被恶意刷取?
这是实际运营中关键的安全与风控问题。恶意刷接口会导致存储激增、ID耗尽、系统负载过高。
防御需要多层次策略:
// 1. 用户维度限流(基于用户ID或Token)
@Component
public class RateLimitAspect {
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint point) throws Throwable {
String userId = getCurrentUserId();
String key = "rate:limit:shorten:" + userId;
// 例如:每小时最多生成100次
Integer count = redisTemplate.opsForValue().get(key);
if (count != null && count >= 100) {
throw new RateLimitException("操作太频繁,请稍后再试");
}
redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, 1, TimeUnit.HOURS);
return point.proceed();
}
}
// 2. IP维度限流(针对未登录用户)
// 3. 验证码机制(对可疑高频请求触发)
// 4. 业务黑名单(自动封禁异常IP或用户)
Q2:高并发下,如何保证缓存与数据库的数据一致性?
这是分布式系统的经典难题。对于短链接这种“写少读多”且数据几乎不更新的场景,策略可以相对明确。
读策略遵循标准缓存模式:先查缓存,命中则返回;未命中则查数据库,并回填缓存。
写策略采用“先更新数据库,再删除缓存”。为何删除而非更新?因为短链接创建后极少被修改,更新缓存可能因并发导致脏数据。删除缓存,让下次读请求时从数据库加载并回填,是更简单有效的最终一致性策略。
更进一步,可采用“延迟双删”应对极端并发场景:
public void updateUrlMapping(UrlMapping mapping) {
// 1. 更新数据库
urlMapper.updateById(mapping);
// 2. 删除缓存
String cacheKey = SHORT_URL_PREFIX + mapping.getShortCode();
redisTemplate.delete(cacheKey);
// 3. 延迟一段时间后,再次删除(清理可能存在的脏缓存)
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500);
redisTemplate.delete(cacheKey);
} catch (Exception e) {
log.error("延迟双删失败", e);
}
});
}
Q3:如果Redis缓存集群故障怎么办?
缓存是性能核心,必须具备高可用性。单一Redis实例是巨大的单点故障风险。
方案一是采用Redis主从复制加Sentinel哨兵机制,实现自动故障转移。需注意主从切换期间可能有少量数据丢失。
方案二是引入本地缓存作为兜底。当Redis不可用时,系统可降级至查询本地缓存和数据库,保证核心跳转功能不中断。
@Service
public class ShortUrlService {
// 使用Caffeine作为本地缓存
private final Cache localCache = Caffeine.newBuilder()
.maximumSize(10000) // 缓存1万条热点数据
.expireAfterWrite(5, TimeUnit.MINUTES) // 5分钟过期
.build();
public String getLongUrl(String shortCode) {
// 1. 查本地缓存
String longUrl = localCache.getIfPresent(shortCode);
if (longUrl != null) {
return longUrl;
}
// 2. 查Redis(异常时自动降级)
try {
longUrl = redisTemplate.opsForValue().get(SHORT_URL_PREFIX + shortCode);
if (longUrl != null) {
localCache.put(shortCode, longUrl);
return longUrl;
}
} catch (Exception e) {
log.warn("Redis访问异常,降级到数据库查询", e);
}
// 3. 查数据库
return getFromDatabase(shortCode);
}
}
方案三是配置Redis的持久化策略,如AOF(每秒刷盘)或混合持久化,确保进程重启后能快速恢复数据,减少损失。
大厂特色:这些深度追问你如何回答?
追问1:发号器服务本身如何实现高可用?
发号器是短码生成的源头,必须绝对可靠。常见高可用方案有几种:
Snowflake算法是Twitter开源的分布式ID生成方案,性能高、不依赖数据库,但需要妥善处理时钟回拨问题。
数据库号段模式是另一种思路:服务每次从数据库申请一个号段(如1-10000),在内存中分配。用尽后再次申请。此方式对数据库压力小,但需处理好号段耗尽和服务重启时的状态恢复。
在大厂内部,通常会有一套完善的中台化发号器服务,可能结合多机房部署(每个机房分配不同的ID段)、使用Raft等一致性协议保证高可用,并能支撑千万级QPS。
追问2:如何构建有效的监控与告警体系?
没有监控的系统如同“盲人摸象”。对于短链接系统,以下核心指标至关重要:
@Component
public class ShortUrlMonitor {
@Autowired
private MeterRegistry meterRegistry;
// 1. 业务量监控
public void recordCreateUrl() {
meterRegistry.counter("short.url.create.count").increment();
}
// 2. 性能监控
public void recordAccessLatency(long latencyMs) {
meterRegistry.timer("short.url.access.latency").record(latencyMs, TimeUnit.MILLISECONDS);
}
// 3. 缓存有效性监控
public void recordCacheHit(boolean hit) {
meterRegistry.counter("short.url.cache." + (hit ? "hit" : "miss")).increment();
}
// 4. 错误监控
public void recordError(String errorType) {
meterRegistry.counter("short.url.error", "type", errorType).increment();
}
}
基于这些指标,设置相应的告警规则:生成失败率超过1%、平均访问延迟突破200毫秒、Redis连接数使用率超过80%、数据库慢查询数量激增等,都需及时通知运维与开发人员。
追问3:短链接系统有哪些安全风险?如何防护?
短链接系统面临多种安全威胁:攻击者可能枚举短码遍历所有链接;中间人可能篡改重定向目标;系统可能被用于生成钓鱼链接。
防护需多层面入手:
// 1. 短码不可预测:避免使用连续、可猜测的ID
private String generateUnpredictableCode(Long id) {
// 对ID进行混淆后再编码,增加猜测难度
Long encryptedId = id ^ SECRET_KEY;
return encodeBase62(encryptedId);
}
// 2. 访问鉴权:对敏感链接进行保护
public String getLongUrl(String shortCode, String token) {
UrlMapping mapping = urlMapper.selectByShortCode(shortCode);
if (mapping.isPrivate()) {
if (!validateToken(shortCode, token)) {
throw new UnauthorizedException("无权访问此链接");
}
}
return mapping.getLongUrl();
}
// 3. 添加安全响应头,防止点击劫持等攻击
@GetMapping("/{shortCode}")
public void redirect(@PathVariable String shortCode, HttpServletResponse response) {
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("X-Content-Type-Options", "nosniff");
// ... 重定向逻辑
}
总结:高并发短链接系统设计核心要点
回顾整个设计,一个能承载十亿级流量的短链接系统,其核心要点可归纳如下:
在架构层面,必须坚持读写分离、多级缓存、数据分片和核心服务(如发号器)高可用的设计原则。
在性能优化上,要追求极高的缓存命中率,贯彻异步化思想(如访问统计更新),并准备好缓存失效时的降级与熔断方案。
安全防护不容忽视,需要通过限流防刷、分布式锁防缓存击穿、不可预测短码防遍历、以及安全响应头等多重手段加固系统。
最后,高可用是生命线。这意味着从缓存(Redis集群)、数据库(主从复制)、到应用服务(多可用区部署)的每一层,都需要有冗余和故障转移机制,并辅以完善的监控告警体系,确保问题能被快速发现与定位。
设计这样一个系统,远不止是实现“长链变短链”的功能,更是一次对高并发架构、数据一致性、系统可用性与安全性的综合演练与深度考量。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
2026年全球GEO优化服务机构实力排名技术与商业价值兼具
生成式AI搜索正在重塑流量格局,GEO(生成式引擎优化)服务市场也随之分化,呈现出不同的技术路径与商业模式。对于企业而言,如何在预算、行业属性与发展阶段的约束下,精准匹配到合适的服务商,已成为布局AI时代增长战略的关键决策。基于对国内主流GEO服务厂商的深度调研,我们梳理出2026年度GEO优化服务
2026年五大GEO优化服务商深度评测与选型指南
当DeepSeek、豆包、Kimi等生成式AI平台逐渐成为用户获取信息的首要入口,GEO(生成式引擎优化)已不再是前沿概念,而是企业数字化营销版图中不可或缺的核心拼图。行业数据显示,如何让品牌信息在海量内容中被AI精准识别并优先推荐,同时有效规避“AI幻觉”带来的认知偏差,已成为品牌增长面临的普遍挑
净水器选购指南根据家庭类型匹配最适合你的那一款
“净水器哪一款好”,这大概是家电选购里搜索量最高的问题之一了。如果你翻过几十篇横评榜单,大概率会发现一个有趣的现象:不同评测给出的“冠军”答案常常截然不同——有的力推RO反渗透纯水机,有的强调必须上全屋净水方案,还有的直接把性价比榜单当作通用解。其实原因并不复杂:所谓“哪一款最好”,本质上得问“对哪
shadcn/ui 组件库使用指南:免安装实现前端开发自由
我们早就想要一个“好看、好改、不绑架技术栈”的组件方案,却在各种库里反复踩坑——直到 shadcn ui 出现,彻底终结了前端组件库的“黑盒时代”。 做前端的,谁没被组件库折磨过? Ant Design 样式老旧改不动,想换个颜色得翻半天主题配置;Material-UI 体积臃肿,打包后多出来几百
2026年AI搜索时代品牌可见度提升指南与GEO服务商评测
如今,用户了解和对比品牌的核心场景,已经全面转向各类AI问答与智能搜索平台。一个普遍存在的困境是,许多企业线下实力雄厚,产品也颇具竞争力,但一旦进入生成式AI的搜索世界,品牌存在感便急剧减弱,只能无奈地看着潜在客户流失,市场拓展也举步维艰。在此背景下,针对生成式搜索引擎的优化——即GEO(Gener
- 日榜
- 周榜
- 月榜
1
2
3
4
5
6
7
8
9
10
相关攻略
2015-03-10 11:25
2015-03-10 11:05
2021-08-04 13:30
2015-03-10 11:22
2015-03-10 12:39
2022-05-16 18:57
2025-05-23 13:43
2025-05-23 14:01
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程
热门话题

