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

MySQLNoSQL数据库

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

前言

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

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

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

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

希望对你会有所帮助。

最近建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。

扫码加苏三的微信:li_su223,备注:所在城市,即可进群。

picture.image

1 短链系统的核心挑战

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

如下图所示:

picture.image

百万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版):

  
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进制的字符串。

转换原理:

  
2000000000 = 2×62^4 + 17×62^3 + 35×62^2 + 10×62 + 8   
           = "Cdz9a"  

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

  
// 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 数据存储模型设计

picture.image

3.2 缓存层级设计

picture.image

3.3 缓存击穿解决方案

  
// 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压力
  • 指数退避重试策略

4 跳转优化

4.1 Nginx层直接跳转

  
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实现:

  
// 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整体架构如下图所示:picture.image

核心组件解析:

  1. 接入层
  • 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 多级降级方案

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

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,000 | 10x |

| | +Nginx直跳 | 350,000 | 2.9x |

| | +连接池优化 | 780,000 | 2.2x |

| | +布隆过滤器 | 1,200,000 | 1.5x |

|

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

总结

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

四大设计原则:

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

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

最后欢迎加入苏三的星球,你将获得:商城微服务实战、AI开发项目课程、苏三AI项目、秒杀系统实战、商城系统实战、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等多个优质专栏。

还有1V1答疑、修改简历、职业规划、送书活动、技术交流。

扫描下方二维码,即可加入星球(非常正确的一次决定):

picture.image

目前星球已经更新了5200+篇优质内容,还在持续爆肝中.....

星球已经被官方推荐了3次,收到了小伙伴们的一致好评。戳我加入学习,已有1700+小伙伴加入学习。

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
云原生数据库 veDB 核心技术剖析与展望
veDB 是一款分布式数据库,采用了云原生计算存储分离架构。本次演讲将为大家介绍火山引擎这款云原生数据库的核心技术原理,并对未来进行展望。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论