每天一道面试题之架构篇|如何设计一个百万级短链系统架构

44 阅读5分钟

面试官:"刷微博时点开的 t.cn 短链,背后是怎么实现的?能简单说说吗?"

这道题在大厂架构师面试中出现率高达80%!今天我们就来彻底拆解这个经典系统设计题

一、开篇:理解问题本质

面对百万级用户,系统需要解决四大核心挑战:

  • 高并发生成:每秒处理数万次生成请求
  • 全局唯一性:保证数十亿短码绝不重复
  • 低延迟跳转:毫秒级完成重定向
  • 水平扩展:支持业务持续增长

这就像建造一个数字高速公路系统,既要保证每辆车有唯一车牌,又要让所有车辆快速通行

二、核心架构设计

2.1 短链生成算法

三种主流方案对比

方案优点缺点适用场景
自增ID+Base62无冲突、有序需要中心化ID生成器推荐使用
雪花算法分布式、高性能可能时钟回拨分布式环境
哈希算法去重能力强需要解决冲突小规模场景

推荐方案:Redis自增ID + Base62编码

@Service
public class ShortUrlGenerator {
    @Autowired
    private RedisTemplate<String, Long> redisTemplate;
    
    // Base62字符集:数字+大写字母+小写字母
    private static final String BASE62 = 
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    public String generate(String longUrl) {
        // 使用Redis原子操作生成全局唯一ID
        Long id = redisTemplate.opsForValue()
            .increment("short_url_id_counter");
        
        return encodeBase62(id);
    }
    
    // Base62编码实现
    private String encodeBase62(long num) {
        StringBuilder sb = new StringBuilder();
        do {
            int remainder = (int)(num % 62);
            sb.append(BASE62.charAt(remainder));
            num /= 62;
        } while (num > 0);
        
        return sb.reverse().toString();
    }
}

2.2 存储架构设计

MySQL分库分表策略

-- 按short_code的hash值分1024张表
CREATE TABLE short_url_% (
    id BIGINT PRIMARY KEY COMMENT '雪花算法ID',
    short_code VARCHAR(8) NOT NULL COMMENT '短码',
    original_url VARCHAR(2048) NOT NULL COMMENT '原始URL',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    expire_time DATETIME COMMENT '过期时间',
    status TINYINT DEFAULT 1 COMMENT '状态',
    UNIQUE KEY uk_code(short_code),
    KEY idx_create_time(create_time)
) ENGINE=InnoDB CHARSET=utf8mb4;

多级缓存设计

# application.yml配置
spring:
  redis:
    short-url:
      timeout: 7200    # 2小时缓存过期
      max-size: 1000000 # 缓存100万条热点数据
  cache:
    type: caffeine    # 本地缓存
    caffeine:
      spec: maximumSize=10000,expireAfterWrite=10m

2.3 高性能重定向实现

Spring Boot重定向接口

@RestController
public class RedirectController {
    
    @GetMapping("/s/{shortCode}")
    public void redirect(@PathVariable String shortCode, 
                       HttpServletResponse response) throws IOException {
        String originalUrl = urlService.getOriginalUrl(shortCode);
        
        if (originalUrl == null) {
            response.sendError(404, "短链接不存在");
            return;
        }
        
        // 302临时重定向
        response.setStatus(HttpStatus.FOUND.value());
        response.setHeader("Location", originalUrl);
        
        // 异步记录访问日志
        logService.asyncRecordAccess(shortCode);
    }
}

多级查询策略

@Service
public class UrlService {
    @Cacheable(value = "shortUrl", key = "#shortCode")
    public String getOriginalUrl(String shortCode) {
        // 1. 查询本地缓存(Caffeine)
        // 2. 查询Redis集群
        // 3. 查询MySQL分表
        // 4. 使用布隆过滤器防穿透
        return findInStorage(shortCode);
    }
}

2.4 安全防护体系

Sentinel限流配置

@PostMapping("/api/shorten")
@SentinelResource(value = "createShortUrl", 
                 blockHandler = "blockHandler",
                 fallback = "fallbackHandler")
public Result<String> createShortUrl(@Valid @RequestBody UrlRequest request) {
    // 业务逻辑处理
    String shortUrl = urlService.createShortUrl(request.getUrl());
    return Result.success(shortUrl);
}

// 限流处理
public Result<String> blockHandler(UrlRequest request, BlockException ex) {
    log.warn("触发限流保护: {}", request.getUrl());
    return Result.error("请求过于频繁,请稍后再试");
}

// 降级处理
public Result<String> fallbackHandler(UrlRequest request, Throwable ex) {
    return Result.error("系统繁忙,请重试");
}

综合防刷策略

  1. IP限流:每个IP每分钟最多100次生成
  2. 用户认证:重要操作要求登录验证
  3. 内容安全:URL恶意检测和过滤
  4. 验证码:高频操作需要图形验证码

三、扩展功能设计

3.1 自定义短链功能

@Service
public class CustomUrlService {
    
    public boolean createCustomUrl(String customCode, String originalUrl) {
        // 检查自定义码格式
        if (!isValidCustomCode(customCode)) {
            throw new IllegalArgumentException("自定义码格式错误");
        }
        
        // 使用Redis分布式锁防止并发冲突
        String lockKey = "lock:custom:" + customCode;
        try {
            Boolean locked = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, "1", Duration.ofSeconds(5));
            
            if (Boolean.TRUE.equals(locked)) {
                // 检查是否已存在
                if (isCodeExists(customCode)) {
                    return false;
                }
                
                // 保存到数据库
                saveCustomUrl(customCode, originalUrl);
                return true;
            }
            return false;
        } finally {
            redisTemplate.delete(lockKey);
        }
    }
}

3.2 点击统计与分析

// 使用RocketMQ异步处理点击统计
@Component
public class ClickStatisticService {
    
    @Resource
    private RocketMQTemplate rocketMQTemplate;
    
    public void recordClick(String shortCode, HttpServletRequest request) {
        ClickEvent event = new ClickEvent();
        event.setShortCode(shortCode);
        event.setIp(request.getRemoteAddr());
        event.setUserAgent(request.getHeader("User-Agent"));
        event.setReferer(request.getHeader("Referer"));
        event.setTimestamp(System.currentTimeMillis());
        
        // 异步发送到消息队列
        rocketMQTemplate.sendOneWay("short_url_clicks", event);
    }
}

// 点击事件消费者
@RocketMQMessageListener(consumerGroup = "click-consumer", topic = "short_url_clicks")
public class ClickConsumer implements RocketMQListener<ClickEvent> {
    
    @Override
    public void onMessage(ClickEvent event) {
        // 批量入库或更新统计信息
        statisticService.updateClickStatistic(event);
    }
}

四、面试陷阱与加分项

4.1 常见陷阱问题

问题1:"DB主从延迟导致新生成的短链查不到怎么办?"

参考答案

  • 写操作后一段时间内强制读主库
  • 新生成的短码在Redis设置短期缓存
  • 使用数据库读写分离中间件

问题2:"如何避免哈希冲突?"

参考答案

  • 自增ID方案天然无冲突
  • 哈希方案需要重试机制和盐值
  • 布隆过滤器快速判断是否存在

问题3:"短链过期机制怎么设计?"

参考答案

  • Redis设置TTL自动过期
  • 定时任务清理过期数据
  • 软删除+物理删除结合

4.2 面试加分项

  1. 业界实践参考

    • Bitly:NSQ消息队列 + Cassandra存储
    • 微博t.cn:多层缓存 + 异地多活部署
  2. 容量预估

    • 每亿条数据约需要500GB存储空间
    • 性能指标:QPS 10万+,跳转延迟<50ms
  3. 监控体系

    • 生成成功率监控
    • 跳转延迟监控
    • 错误率告警

五、总结与互动

核心设计哲学:用空间换时间,用缓存抗流量,用异步保性能

记住这个架构公式:自增ID + Base62 + 多级缓存 + 分库分表 + 异步化 = 高性能短链系统


思考题:如果是你,会怎么优化这个系统?欢迎在评论区分享你的架构思路!

关注我,每天搞懂一道面试题,助你轻松拿下Offer!