
最近在准备秋招面试发现“短链系统设计”几乎是后端岗位必考的高频题目。很多同学虽然知道短链的基本原理但被问到“如何从零设计”时往往只能说出“生成短码、存映射、重定向”这几个关键词对于背后的技术选型、架构演进、性能瓶颈和工程细节却语焉不详。本文将从面试官视角出发结合实战经验为你拆解一套可落地的短链系统设计方案涵盖从核心原理到高可用架构的完整思考路径让你不仅能回答面试更能真正理解系统设计的精髓。1. 短链系统不只是“长链接变短”在深入设计之前我们必须明确短链系统要解决的核心问题及其价值。1.1 什么是短链系统短链系统顾名思义是一个将冗长的原始URL长链接转换成一个简短、易记、易传播的短链接的服务。用户访问短链接后系统会将其重定向到对应的原始长链接。其核心价值在于节省空间与美化在字符数受限的场景如微博、短信短链至关重要。便于传播与记忆t.cn/abc123远比一个包含复杂参数的原始URL更友好。数据追踪与分析这是商业化的关键。通过短链可以收集点击量、用户设备、地域、时间等维度数据用于营销效果分析、用户行为洞察。1.2 系统设计目标与挑战设计一个短链系统我们需要达成以下目标并应对相应挑战功能性目标核心生成短链、解析跳转。扩展链接管理创建、禁用、生效时间、数据统计。非功能性目标面试重点高并发与低延迟跳转是读多写少的场景必须承受极高的QPS每秒查询率响应时间要极短毫秒级。高可用服务必须7x24小时可用跳转失败直接影响用户体验。高容量需要支持海量数十亿甚至更多的短链映射存储。唯一性与防冲突生成的短码必须全局唯一。安全性防止短码被恶意遍历、爆破防止生成恶意跳转链接。理解了这些我们的设计就有了明确的导向一个读多写少、需要极高性能和可靠性的键值映射服务。2. 核心流程与数据结构设计让我们从最简单的单机版本开始理解数据是如何流转的。2.1 核心业务流程生成短链用户提交一个长链接https://www.example.com/product?id123sourceweibo。系统生成一个全局唯一的短码如7sUx9K。将映射关系7sUx9K - 原始长链接持久化存储。返回给用户完整的短链接如https://s.cn/7sUx9K。访问跳转用户点击或访问https://s.cn/7sUx9K。短链服务解析出短码7sUx9K。查询存储获取对应的原始长链接。返回302 Found或301 Moved Permanently重定向响应引导浏览器跳转。2.2 数据模型设计我们需要至少两张核心表短链映射表 (short_url_map)这是最核心的表存储短码与长链接的映射。CREATE TABLE short_url_map ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键自增ID, short_code VARCHAR(10) NOT NULL UNIQUE COMMENT 短码唯一索引, original_url VARCHAR(2048) NOT NULL COMMENT 原始长链接, hash_key CHAR(32) COMMENT 原始URL的MD5用于判重和索引, status TINYINT DEFAULT 1 COMMENT 状态1-启用0-禁用, expire_time DATETIME COMMENT 过期时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, INDEX idx_short_code (short_code), INDEX idx_hash_key (hash_key) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT短链映射表;short_code必须建立唯一索引这是跳转查询的关键。hash_key对原始URL取MD5等哈希用于实现幂等创建。同一长链接多次请求可返回相同的短码节省存储。expire_time支持链接过期功能。访问统计表 (short_url_access_log)用于数据统计分析。CREATE TABLE short_url_access_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, short_code VARCHAR(10) NOT NULL COMMENT 短码, access_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 访问时间, user_agent TEXT COMMENT 用户代理, referer VARCHAR(512) COMMENT 来源页, client_ip VARCHAR(64) COMMENT 客户端IP, country VARCHAR(100) COMMENT 国家, region VARCHAR(100) COMMENT 地区, device_type VARCHAR(50) COMMENT 设备类型 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT短链访问日志表;这张表的数据量会飞速增长需要考虑分库分表或使用时序数据库。3. 短码生成算法系统的基石如何生成简短、唯一、高并发的短码是首要技术挑战。常见方案有以下几种3.1 方案一哈希算法如 MurmurHash 冲突处理对长链接进行哈希如MurmurHash一种非加密型快速哈希函数得到一个64位或128位的整数。将此整数通过62进制编码A-Z, a-z, 0-9共62个字符转换为短字符串。冲突处理哈希必然存在冲突不同长链接生成相同短码。解决方法是生成短码后查询数据库是否已存在。若存在则在原长链接后追加一个随机盐值或自增序号重新哈希直到生成唯一短码。优点生成速度快短码长度相对固定。缺点存在冲突重试逻辑在极高并发下可能成为瓶颈短码无顺序性。3.2 方案二发号器 进制转换推荐这是更主流的方案也是面试中期望你详细阐述的。使用一个全局发号器为每个长链接分配一个全局唯一、趋势递增的ID。将十进制ID转换为62进制字符串作为短码。关键点在于发号器的设计数据库自增ID最简单利用AUTO_INCREMENT。但单库有性能上限且不利于分库分表。Redis INCR利用Redis的原子递增命令性能极高。SET short_url_id_counter 10000000然后通过INCR获取ID。需考虑Redis持久化问题。雪花算法Snowflake分布式ID生成算法生成的是带有时间戳、机器ID、序列号的64位整数。无需中心化发号器性能高。但生成的ID较长转换为62进制后短码长度不固定但依然很短。Leaf/美团分布式ID生成器开源解决方案提供了基于数据库号段和雪花算法的优化方案适合大规模生产环境。示例62进制转换代码public class Base62Encoder { private static final String BASE62_CHARS 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz; public static String encode(long num) { StringBuilder sb new StringBuilder(); while (num 0) { int remainder (int)(num % 62); sb.append(BASE62_CHARS.charAt(remainder)); num num / 62; } // 反转字符串保证短码顺序性可选取决于发号器特性 return sb.reverse().toString(); } public static long decode(String shortCode) { long num 0; for (int i 0; i shortCode.length(); i) { num num * 62 BASE62_CHARS.indexOf(shortCode.charAt(i)); } return num; } // 测试 public static void main(String[] args) { long id 123456789L; String shortCode encode(id); // 输出类似 8M0kX System.out.println(短码: shortCode); long decodedId decode(shortCode); System.out.println(还原ID: decodedId); } }3.3 方案对比与选型建议方案优点缺点适用场景哈希冲突处理实现简单不依赖中心化服务有冲突风险重试逻辑复杂短码无规律中小流量对短码无顺序要求发号器进制转换绝对唯一无冲突短码有顺序利于数据库索引需要维护发号器额外复杂度高并发、大规模系统的首选方案在面试中明确选择发号器方案并深入讨论发号器如Redis或雪花算法的选型理由能极大提升回答深度。4. 系统架构演进从单机到分布式系统设计题的核心是展现你应对规模增长的设计能力。我们可以分三个阶段来阐述4.1 第一阶段简易原型单机技术栈Spring Boot MySQL单实例。流程生成短码使用数据库自增ID作为发号器转62进制。存储直接插入short_url_map表。跳转根据短码查询数据库返回重定向。瓶颈数据库成为绝对瓶颈无论是写入生成还是读取跳转都无法支撑高并发。4.2 第二阶段引入缓存与读写分离这是应对读高并发的关键一步。引入Redis作用作为热点数据的缓存。跳转请求读首先查询Redis命中则直接返回长链接未命中则查数据库并回填Redis。缓存策略Key设计short:code:{shortCode}值为原始URL。过期时间设置合理的TTL如7天防止冷数据常驻内存。可与数据库的expire_time结合。发号器使用Redis INCR命令作为高性能发号器。数据库读写分离主库负责写操作创建短链、写入映射。一个或多个从库负责读操作缓存未命中时的查询。通过数据库中间件或Spring动态数据源实现。架构图此时用户 - Nginx/网关 - 短链服务集群 | |--- (写/发号) Redis (INCR) |--- (读缓存) Redis (Key-Value) | |--- (写) MySQL Master |--- (读) MySQL Slave4.3 第三阶段全面分布式与高可用应对百亿级映射和每秒数十万QPS的跳转请求。数据库分库分表分片键以short_code或id作为分片键。策略例如对short_code进行一致性哈希或者直接按id的范围进行分片。使用ShardingSphere或MyCat等中间件。缓存集群与高可用Redis采用Cluster集群模式实现数据分片和高可用。考虑使用多级缓存如本地缓存Caffeine Redis集群进一步降低Redis压力和访问延迟。服务无状态化与弹性伸缩短链服务本身设计为无状态方便通过Kubernetes或云服务进行水平扩容。前端通过负载均衡器如Nginx, SLB将流量分发到多个服务实例。异步化与削峰填谷访问日志异步落盘跳转时访问日志的写入不能阻塞重定向响应。使用消息队列如Kafka, RocketMQ将日志数据异步发送到后端消费者再批量写入数据库或数据仓库如HBase, ClickHouse。创建请求异步化对于批量生成等场景也可以采用异步处理快速响应“受理成功”后台任务处理完成后通知用户。最终架构示意图[负载均衡器] | [短链服务集群 - 无状态] / | \ / | \ (发号)Redis Cluster | (缓存)Redis Cluster | | [消息队列 Kafka/RocketMQ] | | [数据分析服务] --- [大数据平台] --- [MySQL集群 (分库分表)]5. 关键工程细节与面试加分点除了宏观架构面试官很喜欢追问细节。以下问题需要提前准备。5.1 如何实现 301 与 302 重定向301 (Moved Permanently)永久重定向。浏览器和搜索引擎会缓存此映射后续请求直接访问长链接不再经过短链服务。优点减轻服务端压力。缺点无法统计后续点击数据且长链接失效后无法更新。302 (Found)临时重定向。每次访问都会经过短链服务。优点可以准确统计每次点击便于控制链接状态如禁用、修改。缺点服务端压力大。选择绝大多数短链系统如t.cn使用302因为数据统计和链路控制是核心商业需求。只有在明确需要永久转移流量且无需统计的场景下才用301。5.2 如何防止短码被遍历短码空间有限如6位62进制有560亿种组合恶意攻击者可能通过遍历短码来获取系统内所有链接。增加短码长度使用8位或更长的短码极大增加遍历空间。使用不连续的ID发号器不采用连续自增而使用雪花算法或在其基础上加入随机因子使短码无规律。访问频率限制对同一IP或用户对未知短码的频繁访问进行限流Rate Limiting。监控与告警监控短码查询的失败率异常升高时触发告警。5.3 短链失效与删除策略设置过期时间创建时指定expire_time。后台定时任务扫描并清理过期数据。惰性删除跳转查询时如果发现链接已过期则返回“链接已失效”页面并异步触发删除任务。物理删除 vs 逻辑删除通常采用逻辑删除status0便于问题排查和数据恢复。定期对已逻辑删除的冷数据进行物理归档或删除。5.4 如何保证高可用服务层无状态设计多实例部署健康检查故障自动转移。缓存层Redis Cluster主从切换哨兵或集群模式。数据层数据库主从复制读写分离跨机房容灾。发号器发号器是单点。Redis发号器需配合Redis高可用方案。也可以准备一个备用的发号器方案如预生成号段在本地。降级策略极端情况下如果Redis完全宕机可以考虑降级为直接查数据库虽然慢但服务可用。6. 实战一个Spring Boot短链服务核心代码让我们用Spring Boot实现一个核心流程聚焦于发号器和跳转。6.1 项目结构与依赖!-- pom.xml 关键依赖 -- dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version /dependency /dependencies6.2 发号器服务 (IdGeneratorService)我们采用Redis INCR作为发号器。Service public class IdGeneratorService { Autowired private StringRedisTemplate redisTemplate; private static final String SHORT_URL_ID_KEY short_url_id; /** * 获取下一个全局ID */ public long getNextId() { // INCR 是原子操作保证分布式环境下唯一递增 Long id redisTemplate.opsForValue().increment(SHORT_URL_ID_KEY); if (id null) { throw new RuntimeException(Failed to generate ID from Redis); } return id; } }6.3 短码服务 (ShortCodeService)集成发号器和62进制编码。Service public class ShortCodeService { Autowired private IdGeneratorService idGeneratorService; private static final String BASE62 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz; /** * 生成短码 */ public String generateShortCode() { long id idGeneratorService.getNextId(); return encodeBase62(id); } /** * 62进制编码 */ private String encodeBase62(long num) { StringBuilder sb new StringBuilder(); while (num 0) { sb.append(BASE62.charAt((int)(num % 62))); num / 62; } // 反转使短码更随机因为ID是递增的 return sb.reverse().toString(); } /** * 62进制解码 (用于跳转时如果短码是直接由ID编码而来) */ public long decodeShortCode(String shortCode) { long num 0; for (int i 0; i shortCode.length(); i) { num num * 62 BASE62.indexOf(shortCode.charAt(i)); } return num; } }6.4 控制器 (ShortUrlController)RestController RequestMapping(/api/short-url) public class ShortUrlController { Autowired private ShortUrlService shortUrlService; /** * 创建短链 */ PostMapping(/create) public ApiResponseString createShortUrl(RequestBody CreateShortUrlRequest request) { // 参数校验略 String shortCode shortUrlService.createShortUrl(request.getOriginalUrl()); // 假设域名是 s.cn String shortUrl https://s.cn/ shortCode; return ApiResponse.success(shortUrl); } } RestController // 跳转控制器无需API前缀 public class RedirectController { Autowired private ShortUrlService shortUrlService; /** * 短链跳转 - 核心重定向方法 * 路径如/{shortCode} */ GetMapping(/{shortCode}) public void redirect(PathVariable String shortCode, HttpServletResponse response) throws IOException { String originalUrl shortUrlService.getOriginalUrl(shortCode); if (originalUrl null) { // 返回404页面 response.sendError(HttpStatus.NOT_FOUND.value(), Short link not found); return; } // 记录访问日志异步此处略 // logAccessAsync(shortCode, request); // 使用302临时重定向保证每次点击可追踪 response.setStatus(HttpStatus.FOUND.value()); response.setHeader(Location, originalUrl); } }6.5 核心服务 (ShortUrlService)Service public class ShortUrlService { Autowired private ShortCodeService shortCodeService; Autowired private ShortUrlMapMapper shortUrlMapMapper; // MyBatis Mapper Autowired private RedisTemplateString, String redisTemplate; private static final String CACHE_KEY_PREFIX short:url:; private static final long CACHE_EXPIRE_SECONDS 7 * 24 * 3600; // 7天 /** * 创建短链幂等 */ Transactional public String createShortUrl(String originalUrl) { // 1. 计算长链接哈希判断是否已存在 String hashKey DigestUtils.md5DigestAsHex(originalUrl.getBytes()); ShortUrlMap existing shortUrlMapMapper.selectByHashKey(hashKey); if (existing ! null) { return existing.getShortCode(); // 幂等返回 } // 2. 生成短码 String shortCode shortCodeService.generateShortCode(); // 3. 构造实体并入库 ShortUrlMap entity new ShortUrlMap(); entity.setShortCode(shortCode); entity.setOriginalUrl(originalUrl); entity.setHashKey(hashKey); entity.setStatus(1); shortUrlMapMapper.insert(entity); // 4. 预热缓存可选 cacheShortUrl(shortCode, originalUrl); return shortCode; } /** * 获取原始链接带缓存 */ public String getOriginalUrl(String shortCode) { // 1. 查缓存 String cacheKey CACHE_KEY_PREFIX shortCode; String originalUrl redisTemplate.opsForValue().get(cacheKey); if (originalUrl ! null) { return originalUrl; } // 2. 缓存未命中查数据库 ShortUrlMap entity shortUrlMapMapper.selectByShortCode(shortCode); if (entity null || entity.getStatus() 0) { return null; } // 检查是否过期 if (entity.getExpireTime() ! null entity.getExpireTime().before(new Date())) { // 异步更新状态或删除 return null; } originalUrl entity.getOriginalUrl(); // 3. 回填缓存 cacheShortUrl(shortCode, originalUrl); return originalUrl; } private void cacheShortUrl(String shortCode, String originalUrl) { String cacheKey CACHE_KEY_PREFIX shortCode; redisTemplate.opsForValue().set(cacheKey, originalUrl, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS); } }7. 面试常见问题与回答思路Q为什么选择62进制A62进制A-Z, a-z, 0-9能在有限的字符位数内表达更大的数值范围比纯数字更短。同时这些字符在URL中是安全的无需编码。Q短码生成如何保证全局唯一A核心是保证发号器的全局唯一和递增。我们采用分布式ID生成方案如Redis原子INCR命令或雪花算法。将获取到的唯一ID进行62进制编码得到短码从根源上避免了冲突。Q如何应对海量跳转请求读A采用多层次缓存策略。首先使用CDN缓存热点短链的跳转响应HTTP 302。其次在应用层使用Redis集群缓存短码到长链接的映射。最后数据库进行读写分离和分库分表。99%以上的请求应该在CDN或Redis层被处理。Q存储海量映射关系数据库如何设计A首先对核心表short_url_map进行分库分表分片键可以选择short_code或生成它的id。其次建立合适的索引short_code唯一索引hash_key索引用于判重。对于访问日志这种时序性极强的数据可以考虑使用专门的时序数据库或大数据存储如HBase, ClickHouse并与业务数据库解耦。Q如果Redis挂了怎么办A我们有降级方案。首先Redis本身应配置为高可用集群如Redis Cluster。如果整个集群不可用对于读请求可以降级为直接查询数据库虽然延迟升高但服务可用。对于写请求发号器可以切换到备用发号模式例如提前在本地内存中缓存一批号段号段模式或者启用基于数据库的备份发号器。Q如何统计点击数据A点击统计不能阻塞重定向主流程。我们采用异步化处理。在跳转时将访问日志信息短码、时间、IP、UA等发送到消息队列如Kafka。下游有独立的消费者服务消费这些消息进行清洗、聚合后批量写入分析数据库或数据仓库供后续查询和报表展示。设计短链系统是一次对后端开发者知识体系的综合考察它串联起了分布式ID、缓存、数据库、高并发、异步处理等多个核心知识点。在面试中清晰地陈述从简到繁的演进过程并深入每个环节的权衡与选型理由远比罗列一堆技术名词更有说服力。建议你在理解上述内容的基础上动手画一画架构图写一写核心代码思考每一个环节可能出现的异常及其处理方案这样在面试时才能真正做到胸有成竹。