荪俗 发表于 2025-9-24 15:50:39

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

前言

凌晨两点,监控大屏突然飙红——短链服务QPS突破80万!
数据库连接池告急,Redis集群响应延迟突破500ms。
这不是演习,而是某电商平台大促的真实场景。
当每秒百万级请求涌向你的短链服务,你该如何设计系统?
今天这篇文章跟大家一起聊聊100万QPS短链系统要如何设计?
希望对你会有所帮助。
1 短链系统的核心挑战

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

百万QPS下的三大生死关:

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

2.1 发号器的设计

发号器是短链系统的发动机。
方案对比:

方案吞吐量缺点适用场景UUID5万/s长度长,无法排序小型系统Redis自增ID8万/s依赖缓存持久化中型系统Snowflake12万/s时钟回拨问题中大型系统分段发号50万/s需要预分配超大型系统分段发号器实现(Java版):

public class SegmentIDGen {
    private final AtomicLong currentId = new AtomicLong(0);
    private volatile long maxId;
    private final 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) throw new BusyException();
      return currentId.incrementAndGet();
    }
}关键优化:

[*]双Buffer异步加载(避免加载阻塞)
[*]监控号段使用率(动态调整步长)
[*]多实例分段隔离(biz_tag区分业务)
2.2 短链映射算法

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

2000000000 = 2×62^4 + 17×62^3 + 35×62^2 + 10×62 + 8
         = "Cdz9a"原始ID: 2000000000,转换为62进制的值为Cdz9a。
// Base62编码(0-9a-zA-Z)
public class Base62Encoder {
    private static final 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 缓存击穿解决方案

// Redis缓存击穿防护
public String getLongUrl(String shortCode) {
    // 1. 布隆过滤器预检
    if (!bloomFilter.mightContain(shortCode)) {
      return null;
    }
   
    // 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压力
[*]指数退避重试策略
4 跳转优化

4.1 Nginx层直接跳转

server {
    listen 80;
    server_name s.domain.com;
   
    location ~ ^/({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实现:
// Netty HTTP连接池配置
public class HttpConnectionPool {
    private final EventLoopGroup group = new NioEventLoopGroup();
    private final 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整体架构如下图所示:

核心组件解析:

[*]接入层

[*]CDN:缓存静态资源
[*]Nginx:处理302跳转,本地缓存热点数据

[*]缓存层

[*]Redis集群:缓存短链映射
[*]布隆过滤器:拦截非法请求

[*]服务层

[*]短链生成:分布式ID服务
[*]映射查询:高并发查询服务

[*]存储层

[*]MySQL:分库分表存储映射关系
[*]TiKV:分布式KV存储ID生成状态

6 容灾设计

6.1 限流熔断策略

基于Sentinel的熔断降级:
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 数据分片策略

基于短码分库分表:
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,00010x+Nginx直跳350,0002.9x+连接池优化780,0002.2x+布隆过滤器1,200,0001.5x压测环境:32核64G服务器 × 10台,千兆内网
总结

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

四大设计原则:

[*]无状态设计:跳转服务完全无状态,支持无限扩展
[*]读多写少优化:将读性能压榨到极致
[*]分而治之:数据分片,流量分散
[*]柔性可用:宁可部分降级,不可全线崩溃
真正的架构艺术不在于复杂,而在于在百万QPS洪流中,用最简单的路径解决问题。当你的系统能在流量风暴中优雅舞蹈,才是架构师的巅峰时刻。
最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
本文收录于我的技术网站:http://www.susan.net.cn

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 100万QPS短链系统如何设计?