当前位置: 首页
业界动态
亿级QPS短链接系统架构设计与核心实现方案

亿级QPS短链接系统架构设计与核心实现方案

热心网友 时间:2026-05-21
转载

“如何设计一个类似 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集群)、数据库(主从复制)、到应用服务(多可用区部署)的每一层,都需要有冗余和故障转移机制,并辅以完善的监控告警体系,确保问题能被快速发现与定位。

设计这样一个系统,远不止是实现“长链变短链”的功能,更是一次对高并发架构、数据一致性、系统可用性与安全性的综合演练与深度考量。

来源:https://www.51cto.com/article/843687.html

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
2026年全球GEO优化服务机构实力排名技术与商业价值兼具

2026年全球GEO优化服务机构实力排名技术与商业价值兼具

生成式AI搜索正在重塑流量格局,GEO(生成式引擎优化)服务市场也随之分化,呈现出不同的技术路径与商业模式。对于企业而言,如何在预算、行业属性与发展阶段的约束下,精准匹配到合适的服务商,已成为布局AI时代增长战略的关键决策。基于对国内主流GEO服务厂商的深度调研,我们梳理出2026年度GEO优化服务

时间:2026-05-21 08:08
2026年五大GEO优化服务商深度评测与选型指南

2026年五大GEO优化服务商深度评测与选型指南

当DeepSeek、豆包、Kimi等生成式AI平台逐渐成为用户获取信息的首要入口,GEO(生成式引擎优化)已不再是前沿概念,而是企业数字化营销版图中不可或缺的核心拼图。行业数据显示,如何让品牌信息在海量内容中被AI精准识别并优先推荐,同时有效规避“AI幻觉”带来的认知偏差,已成为品牌增长面临的普遍挑

时间:2026-05-21 08:07
净水器选购指南根据家庭类型匹配最适合你的那一款

净水器选购指南根据家庭类型匹配最适合你的那一款

“净水器哪一款好”,这大概是家电选购里搜索量最高的问题之一了。如果你翻过几十篇横评榜单,大概率会发现一个有趣的现象:不同评测给出的“冠军”答案常常截然不同——有的力推RO反渗透纯水机,有的强调必须上全屋净水方案,还有的直接把性价比榜单当作通用解。其实原因并不复杂:所谓“哪一款最好”,本质上得问“对哪

时间:2026-05-21 08:07
shadcn/ui 组件库使用指南:免安装实现前端开发自由

shadcn/ui 组件库使用指南:免安装实现前端开发自由

我们早就想要一个“好看、好改、不绑架技术栈”的组件方案,却在各种库里反复踩坑——直到 shadcn ui 出现,彻底终结了前端组件库的“黑盒时代”。 做前端的,谁没被组件库折磨过? Ant Design 样式老旧改不动,想换个颜色得翻半天主题配置;Material-UI 体积臃肿,打包后多出来几百

时间:2026-05-21 08:07
2026年AI搜索时代品牌可见度提升指南与GEO服务商评测

2026年AI搜索时代品牌可见度提升指南与GEO服务商评测

如今,用户了解和对比品牌的核心场景,已经全面转向各类AI问答与智能搜索平台。一个普遍存在的困境是,许多企业线下实力雄厚,产品也颇具竞争力,但一旦进入生成式AI的搜索世界,品牌存在感便急剧减弱,只能无奈地看着潜在客户流失,市场拓展也举步维艰。在此背景下,针对生成式搜索引擎的优化——即GEO(Gener

时间:2026-05-21 08:07
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 日榜
  • 周榜
  • 月榜
热门教程
更多
  • 游戏攻略
  • 安卓教程
  • 苹果教程
  • 电脑教程