如何设计一个高并发短链接服务(如 bit.ly)?

如何设计一个高并发短链接服务(如 bit.ly)?

引言:

在社交媒体营销、短信推广等场景中,短链接服务已成为互联网基础设施的关键组件。全球每天有数十亿短链接被创建,如Bitly、TinyURL等服务每天处理数十亿请求。

作为一名拥有 8年经验的Java架构师 ,我曾主导设计过日处理千万级短链接的系统。今天我将从原理到实现,深度解析如何构建一个高性能、高可用、可扩展的短链接服务。


一、业务场景与技术挑战

1.1 核心业务需求

  • 短链接生成 :将长URL转换为短码(如 bit.ly/3xY8zK1
  • 短链接跳转:通过短码重定向到原始URL
  • 访问统计:记录点击量、来源、设备等信息
  • 链接管理:设置有效期、访问密码、API接入等

1.2 技术挑战

挑战维度 具体问题
高并发 瞬时生成/跳转请求峰值可达10万QPS
低延迟 跳转操作需在50ms内完成
海量数据存储 百亿级URL关系存储与快速检索
短码碰撞 避免不同长URL生成相同短码
防恶意攻击 防止短码遍历、刷量等攻击行为

二、架构设计核心思路

2.1 整体架构

markdown 复制代码
 客户端 → API网关 → 生成服务 → Redis缓存 → MySQL分库
          ↓         ↓
          → 跳转服务 → 埋点统计 → Kafka → 数仓

2.2 核心组件设计

✅ 短码生成方案对比
方案 优点 缺点
哈希算法 生成速度快 存在碰撞风险
自增ID+进制转换 无碰撞、长度可控 需分布式ID生成器
预生成号池 高性能、零延迟 需提前分配、浪费风险

推荐方案:分布式ID生成器(Snowflake变体)+ Base62编码

✅ 存储方案选型
arduino 复制代码
// 短码-长URL映射存储
public class UrlMapping {
    private String shortCode;   // 短码(主键)
    private String originUrl;  // 原始URL
    private long createTime;   // 创建时间
    private int expireDays;    // 过期天数
    private int clickCount;    // 点击统计
}

存储策略:

  • Redis:存储热点映射(TTL自动过期)
  • MySQL:持久化全量数据(分库分表)
  • 布隆过滤器:快速判断短码是否存在

三、核心实现细节

3.1 短码生成算法

arduino 复制代码
/**
 * 基于Snowflake的分布式ID生成器
 */
public class ShortCodeGenerator {
    // 组成:时间戳(41bit) + 机器ID(10bit) + 序列号(12bit)
    private static final long EPOCH = 1672531200000L; // 2023-01-01
    
    public String generate(String longUrl) {
        long id = snowflake.nextId(); // 获取分布式ID
        return base62Encode(id);
    }
    
    // Base62编码(0-9a-zA-Z)
    private String base62Encode(long num) {
        char[] map = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
        StringBuilder sb = new StringBuilder();
        while (num > 0) {
            sb.append(map[(int)(num % 62)]);
            num /= 62;
        }
        return sb.reverse().toString(); // 如 "3xY8zK1"
    }
}

3.2 跳转服务核心逻辑

scss 复制代码
@RestController
public class RedirectController {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private UrlMappingRepository repository;
    
    /**
     * 短链接跳转(千万级QPS核心路径)
     */
    @GetMapping("/{shortCode}")
    public ResponseEntity<Void> redirect(
            @PathVariable String shortCode,
            HttpServletRequest request) {
        
        // 1. 从Redis查询缓存
        String originUrl = redisTemplate.opsForValue().get(shortCode);
        
        // 2. 缓存未命中查询数据库
        if (originUrl == null) {
            UrlMapping mapping = repository.findByShortCode(shortCode);
            if (mapping == null) return ResponseEntity.notFound().build();
            originUrl = mapping.getOriginUrl();
            
            // 写入缓存(设置TTL)
            redisTemplate.opsForValue().set(shortCode, originUrl, 24, TimeUnit.HOURS);
        }
        
        // 3. 异步记录访问日志(非阻塞)
        CompletableFuture.runAsync(() -> 
            trackAccess(shortCode, request)
        );
        
        // 4. 返回302重定向
        return ResponseEntity.status(HttpStatus.FOUND)
                .location(URI.create(originUrl))
                .build();
    }
    
    // 访问统计埋点
    private void trackAccess(String shortCode, HttpServletRequest request) {
        AccessLog log = new AccessLog();
        log.setShortCode(shortCode);
        log.setIp(request.getRemoteAddr());
        log.setUserAgent(request.getHeader("User-Agent"));
        log.setReferer(request.getHeader("Referer"));
        log.setAccessTime(System.currentTimeMillis());
        
        // 发送到Kafka
        kafkaTemplate.send("access_log", log);
    }
}

3.3 防攻击策略

arduino 复制代码
/**
 * 短链安全防护
 */
@Service
public class SecurityService {
    
    // 布隆过滤器(10亿数据,误判率0.1%)
    private BloomFilter<String> bloomFilter = BloomFilter.create(
        Funnels.stringFunnel(StandardCharsets.UTF_8), 1_000_000_000, 0.001);
    
    /**
     * 校验短码合法性
     */
    public boolean isValidCode(String shortCode) {
        // 1. 长度校验(6-8字符)
        if (shortCode.length() < 6 || shortCode.length() > 8) 
            return false;
        
        // 2. 布隆过滤器校验
        if (!bloomFilter.mightContain(shortCode)) 
            return false;
        
        // 3. 频率限制(Redis计数器)
        String key = "rate_limit:" + shortCode;
        Long count = redisTemplate.opsForValue().increment(key, 1);
        redisTemplate.expire(key, 1, TimeUnit.MINUTES);
        
        return count <= 100; // 每分钟最多100次访问
    }
}

四、性能优化关键点

4.1 缓存策略优化

缓存技巧:

  • 热点数据:二级缓存(Caffeine+Redis)
  • 缓存穿透:布隆过滤器+空值缓存
  • 缓存雪崩:随机TTL(24h ± 2h)

4.2 MySQL分库分表

sql 复制代码
-- 按短码哈希分表(1024张表)
CREATE TABLE url_mapping_1023 (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  short_code VARCHAR(8) NOT NULL COMMENT '短码',
  origin_url VARCHAR(2048) NOT NULL COMMENT '原始URL',
  create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  UNIQUE KEY idx_short_code (short_code) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

分片策略:
shard = CRC32(short_code) % 1024

4.3 跳转服务优化

  1. 零拷贝响应:直接返回HTTP 302避免序列化开销
  2. 异步日志:访问日志通过Disruptor队列异步处理
  3. 连接池优化:使用HikariCP配置千级数据库连接池
  4. CDN边缘计算:全球部署跳转节点减少延迟

五、扩展功能实现

5.1 访问统计看板

typescript 复制代码
// 基于Flink的实时统计
public class AccessStatsJob {
    public static void main(String[] args) {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        env.addSource(new FlinkKafkaConsumer<>("access_log", new AccessLogDeserializer(), props))
            .keyBy(AccessLog::getShortCode)
            .window(TumblingProcessingTimeWindows.of(Time.minutes(5)))
            .aggregate(new StatsAggregator())
            .addSink(new RedisSink());
    }
    
    // 聚合统计
    private static class StatsAggregator implements AggregateFunction<AccessLog, StatsAcc, StatsResult> {
        public StatsAcc createAccumulator() {
            return new StatsAcc();
        }
        
        public StatsAcc add(AccessLog log, StatsAcc acc) {
            acc.count++;
            // 按设备/地区/来源统计
            return acc;
        }
        
        public StatsResult getResult(StatsAcc acc) {
            return new StatsResult(acc);
        }
    }
}

5.2 管理功能实现

API示例:

less 复制代码
@PostMapping("/create")
public ApiResponse createUrl(@RequestBody CreateRequest request) {
    // 1. URL规范化处理
    String normalizedUrl = UrlUtils.normalize(request.getUrl());
    
    // 2. 检查是否已存在(防重复生成)
    String existCode = repository.findByOriginUrl(normalizedUrl);
    if (existCode != null) {
        return ApiResponse.success(existCode);
    }
    
    // 3. 生成短码并存储
    String shortCode = generator.generate(normalizedUrl);
    repository.save(new UrlMapping(shortCode, normalizedUrl));
    
    return ApiResponse.success(shortCode);
}

六、生产环境最佳实践

6.1 监控指标

指标类型 监控项 报警阈值
性能 跳转P99延迟 >100ms
可用性 5xx错误率 >0.1%
数据一致性 缓存-数据库不一致率 >0.01%

6.2 灾难恢复方案

  1. 多级备份

    • Redis:AOF持久化+跨机房复制
    • MySQL:每日全量备份+Binlog增量
  2. 降级策略

    • 缓存故障时直接读数据库
    • 数据库故障时返回静态页
  3. 流量调度:DNS故障转移+多AZ部署


架构师思考:

短链接服务看似简单,实则处处暗藏玄机:

  • 算法设计:如何在碰撞概率与性能间取得平衡?

  • 存储优化:百亿级数据如何做到毫秒级查询?

  • 高并发挑战:如何设计无状态服务应对流量洪峰?

相关推荐
2301_14725836940 分钟前
7月2日作业
java·linux·服务器
香饽饽~、42 分钟前
【第十一篇】SpringBoot缓存技术
java·开发语言·spring boot·后端·缓存·intellij-idea
小莫分享1 小时前
移除 Java 列表中的所有空值
java
程序员爱钓鱼2 小时前
Go语言实战指南 —— Go中的反射机制:reflect 包使用
后端·google·go
ℳ₯㎕ddzོꦿ࿐2 小时前
Spring Boot 集成 MinIO 实现分布式文件存储与管理
spring boot·分布式·后端
2301_803554523 小时前
c++中类的前置声明
java·开发语言·c++
不想写bug呀6 小时前
多线程案例——单例模式
java·开发语言·单例模式
心平愈三千疾6 小时前
通俗理解JVM细节-面试篇
java·jvm·数据库·面试
我不会写代码njdjnssj6 小时前
网络编程 TCP UDP
java·开发语言·jvm
第1缕阳光6 小时前
Java垃圾回收机制和三色标记算法
java·jvm