100万QPS短链系统如何设计?

大家好,我是苏三,又跟大家见面。

前言

凌晨两点,监控大屏突然飙红------短链服务QPS突破80万! 数据库连接池告急,Redis集群响应延迟突破500ms。

这不是演习,而是某电商平台大促的真实场景。

当每秒百万级请求涌向你的短链服务,你该如何设计系统?

今天这篇文章跟大家一起聊聊100万QPS短链系统要如何设计?

希望对你会有所帮助。

最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、场景题、面试真题、项目实战、工作内推什么都有

1 短链系统的核心挑战

首先我们一起看看设计一个高并发的短链系统,会遇到哪些核心的挑战。

如下图所示:

百万QPS下的三大生死关:

  1. ID生成瓶颈:传统数据库自增ID撑不住百万并发
  2. 跳转性能黑洞:302重定向的TCP连接成本
  3. 缓存雪崩风险:热点短链瞬间击穿Redis

2 短链生成

2.1 发号器的设计

发号器是短链系统的发动机。

方案对比:

方案 吞吐量 缺点 适用场景
UUID 5万/s 长度长,无法排序 小型系统
Redis自增ID 8万/s 依赖缓存持久化 中型系统
Snowflake 12万/s 时钟回拨问题 中大型系统
分段发号 50万/s 需要预分配 超大型系统

分段发号器实现(Java版):

csharp 复制代码
public class SegmentIDGen {
    privatefinal AtomicLong currentId = new AtomicLong(0);
    privatevolatilelong maxId;
    privatefinal ExecutorService loader = Executors.newSingleThreadExecutor();

    public void init() {
        loadSegment();
        loader.submit(this::daemonLoad);
    }

    private void loadSegment() {
        // 从DB获取号段:SELECT max_id FROM alloc WHERE biz_tag='short_url'
        this.maxId = dbMaxId + 10000; // 每次取1万个号
        currentId.set(dbMaxId);
    }

    private void daemonLoad() {
        while (currentId.get() > maxId * 0.8) {
            loadSegment(); // 号段使用80%时异步加载
        }
    }

    public long nextId() {
        if (currentId.get() >= maxId) thrownew BusyException();
        return currentId.incrementAndGet();
    }
}

关键优化

  1. 双Buffer异步加载(避免加载阻塞)
  2. 监控号段使用率(动态调整步长)
  3. 多实例分段隔离(biz_tag区分业务)

2.2 短链映射算法

短码映射将长ID转换成62进制的字符串。

转换原理:

ini 复制代码
2000000000 = 2×62^4 + 17×62^3 + 35×62^2 + 10×62 + 8 
           = "Cdz9a"

原始ID: 2000000000,转换为62进制的值为Cdz9a。

typescript 复制代码
// Base62编码(0-9a-zA-Z)
publicclass Base62Encoder {
    privatestaticfinal String BASE62 = 
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    
    public static String encode(long id) {
        StringBuilder sb = new StringBuilder();
        while (id > 0) {
            sb.append(BASE62.charAt((int)(id % 62)));
            id /= 62;
        }
        return sb.reverse().toString();
    }
    
    // 测试:生成8位短码
    public static void main(String[] args) {
        long id = 1_000_000_000L;
        System.out.println(encode(id)); // 输出:BFp3qQ
    }
}

编码优势:

  • 6位短码可表示 62^6 ≈ 568亿种组合
  • 8位短码可表示 62^8 ≈ 218万亿种组合
  • 无意义字符串避免被猜测

3 存储架构

3.1 数据存储模型设计

3.2 缓存层级设计

3.3 缓存击穿解决方案

typescript 复制代码
// Redis缓存击穿防护
public String getLongUrl(String shortCode) {
    // 1. 布隆过滤器预检
    if (!bloomFilter.mightContain(shortCode)) {
        returnnull;
    }
    
    // 2. 查Redis
    String cacheKey = "url:" + shortCode;
    String longUrl = redis.get(cacheKey);
    if (longUrl != null) {
        return longUrl;
    }
    
    // 3. 获取分布式锁
    String lockKey = "lock:" + shortCode;
    if (redis.setnx(lockKey, "1", 10)) { // 10秒超时
        try {
            // 4. 二次检查缓存
            longUrl = redis.get(cacheKey);
            if (longUrl != null) return longUrl;
            
            // 5. 查数据库
            longUrl = db.queryLongUrl(shortCode);
            if (longUrl != null) {
                // 6. 回填Redis
                redis.setex(cacheKey, 3600, longUrl);
            }
            return longUrl;
        } finally {
            redis.del(lockKey);
        }
    } else {
        // 7. 等待重试
        Thread.sleep(50);
        return getLongUrl(shortCode);
    }
}

防护要点:

  • 布隆过滤器拦截非法短码
  • 分布式锁防止缓存击穿
  • 双重检查减少DB压力
  • 指数退避重试策略

最近建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。加苏三的微信:li_su223,备注:掘金+所在城市,即可进群。

4 跳转优化

4.1 Nginx层直接跳转

bash 复制代码
server {
    listen 80;
    server_name s.domain.com;

    location ~ ^/([a-zA-Z0-9]{6,8})$ {
        set $short_code $1;

        # 查询Redis
        redis_pass redis_cluster;
        redis_query GET url:$short_code;

        # 命中则直接302跳转
        if ($redis_value != "") {
            add_header Cache-Control "private, max-age=86400";
            return 302 $redis_value;
        }

        # 未命中转发到后端
        proxy_pass http://backend;
    }
}

性能收益:

  • 跳转延迟从100ms降至5ms
  • 节省后端服务器资源
  • 支持百万级并发连接

4.2 连接池优化

连接池优化可以用Netty实现:

csharp 复制代码
// Netty HTTP连接池配置
publicclass HttpConnectionPool {
    privatefinal EventLoopGroup group = new NioEventLoopGroup();
    privatefinal Bootstrap bootstrap = new Bootstrap();
    
    public HttpConnectionPool() {
        bootstrap.group(group)
            .channel(NioSocketChannel.class)
            .option(ChannelOption.SO_KEEPALIVE, true)
            .handler(new HttpClientInitializer());
    }
    
    public Channel getChannel(String host, int port) throws InterruptedException {
        return bootstrap.connect(host, port).sync().channel();
    }
    
    // 使用示例
    public void redirect(ChannelHandlerContext ctx, String longUrl) {
        Channel channel = getChannel("target.com", 80);
        channel.writeAndFlush(new DefaultFullHttpRequest(
            HttpVersion.HTTP_1_1, 
            HttpMethod.GET, 
            longUrl
        ));
        // 处理响应...
    }
}

优化效果:

  • TCP连接复用率提升10倍
  • 减少80%的TCP握手开销
  • QPS承载能力提升3倍

5 百万QPS整体架构

百万QPS整体架构如下图所示:

核心组件解析:

  1. 接入层
    • CDN:缓存静态资源
    • Nginx:处理302跳转,本地缓存热点数据
  2. 缓存层
    • Redis集群:缓存短链映射
    • 布隆过滤器:拦截非法请求
  3. 服务层
    • 短链生成:分布式ID服务
    • 映射查询:高并发查询服务
  4. 存储层
    • MySQL:分库分表存储映射关系
    • TiKV:分布式KV存储ID生成状态

6 容灾设计

6.1 限流熔断策略

基于Sentinel的熔断降级:

less 复制代码
public class RedirectController {
    @GetMapping("/{shortCode}")
    @SentinelResource(
        value = "redirectService", 
        fallback = "fallbackRedirect",
        blockHandler = "blockRedirect"
    )
    public ResponseEntity redirect(@PathVariable String shortCode) {
        // 跳转逻辑...
    }
    
    // 熔断降级方法
    public ResponseEntity fallbackRedirect(String shortCode, Throwable ex) {
        return ResponseEntity.status(503)
            .body("服务暂时不可用");
    }
    
    // 限流处理方法
    public ResponseEntity blockRedirect(String shortCode, BlockException ex) {
        return ResponseEntity.status(429)
            .body("请求过于频繁");
    }
}

6.2 多级降级方案

使用多级降级方案:保证服务的高可用。

6.3 数据分片策略

基于短码分库分表:

arduino 复制代码
public int determineDbShard(String shortCode) {
    // 取短码首字母的ASCII值
    int ascii = (int) shortCode.charAt(0);
    // 分16个库
    return ascii % 16;
}

public int determineTableShard(String shortCode) {
    // 取短码的CRC32值
    CRC32 crc32 = new CRC32();
    crc32.update(shortCode.getBytes());
    // 每库1024张表
    return (int) (crc32.getValue() % 1024);
}

这里成了16个库,每个库有1024张表。

7 性能压测数据对比

优化点 优化前QPS 优化后QPS 提升倍数
原始方案 12,000 - 1x
+Redis缓存 120,000 10x
+Nginx直跳 350,000 2.9x
+连接池优化 780,000 2.2x
+布隆过滤器 1,200,000 1.5x

压测环境:32核64G服务器 × 10台,千兆内网

总结

百万QPS短链架构核心要点如图所示:

四大设计原则:

  1. 无状态设计:跳转服务完全无状态,支持无限扩展
  2. 读多写少优化:将读性能压榨到极致
  3. 分而治之:数据分片,流量分散
  4. 柔性可用:宁可部分降级,不可全线崩溃

真正的架构艺术不在于复杂,而在于在百万QPS洪流中,用最简单的路径解决问题。当你的系统能在流量风暴中优雅舞蹈,才是架构师的巅峰时刻。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

相关推荐
罗超驿2 分钟前
SpringBoot 快速入门
java·spring boot·后端
卷无止境44 分钟前
除了开发api,FastAPI其实也可以配合jinja2模板写页面
后端·python·fastapi
newerp1 小时前
Redis 操作与缓存策略
后端·程序员·go
newerp1 小时前
GORM ORM 基础
后端·程序员·go
小岛前端1 小时前
AI Skills 已经封神,但新的问题却越来越严重!
前端·后端·github
拖孩1 小时前
一个人 + AI 做的小程序,上线 15 天赚了 10 块 5
前端·后端·微信小程序
newerp1 小时前
CRUD 操作与预处理语句
后端·程序员·go
wei_shuo1 小时前
KES 数据同步与ETL实战:数据集成、转换与实时同步方案
后端
乒乓狂魔14786739970001 小时前
LangGraph:用一张图,编排一群 AI 助手
后端
Gopher_HBo1 小时前
请求绑定 binding(binding/ 包)
后端