从零构建高可用短链接系统:原理、实现与生产实践

📅 2026/8/15 12:30:16
从零构建高可用短链接系统:原理、实现与生产实践
1. 项目概述从“又臭又长”到“短小精悍”的链接魔法你有没有遇到过这样的场景在微信群里分享一个商品链接结果消息气泡被一串长得离谱的网址占满不仅不美观还让人一眼看不到重点或者在抖音评论区想分享一个网盘资源结果链接复杂到让人怀疑是不是病毒。我自己就经常被这种“又臭又长”的链接困扰尤其是在做社群运营和内容分发时一个简洁、可追踪的短链接简直是提升用户体验和运营效率的神器。今天要聊的“长链接转短链接”就是解决这个痛点的核心技术。它不仅仅是把一串字符变短那么简单背后涉及到的哈希算法、重定向策略、高并发设计以及数据统计每一个环节都藏着不少门道。简单来说长链接转短链接就是一个将原始的长URL统一资源定位符通过特定的算法或服务映射成一个简短、易记、易传播的新URL的过程。用户访问这个短链接时服务端会将其“翻译”回原始的长链接并引导浏览器跳转过去。这个过程我们称之为“重定向”。听起来简单但为什么各大平台如微博的t.cn、腾讯的url.cn都要自己做一套因为这里面有品牌曝光、流量控制、数据分析和安全风控等多重价值。对于个人开发者或中小型项目而言自己实现一个短链接服务不仅能加深对HTTP协议、数据库设计和系统架构的理解更能为你的应用增添一个非常实用的功能模块。接下来我就以一个从业者的视角带你从零开始拆解并实现一个具备生产可用性的短链接系统。2. 核心原理与系统设计思路2.1 短链接的核心价值不止于“短”在动手之前我们必须先想清楚我们为什么要做这个转换除了显而易见的“缩短长度”它还有几个关键价值美观与易传播在字符数受限的场景如微博、短信、二维码短链接优势巨大。一个t.cn/A6g1TxBf远比一个包含复杂查询参数的原始链接更友好。数据追踪与分析这是对运营者最具吸引力的点。通过短链接我们可以精确记录每次点击的访问时间、IP地址、用户设备、来源渠道等。例如你可以知道在抖音评论区分享的链接和微信群里分享的链接哪个转化率更高。流量管理与控制可以对短链接设置有效期、访问密码、访问次数上限甚至可以根据访问者的地域、时间进行动态跳转A/B测试。原始长链接一旦发出便难以修改而短链接的后端目标可以随时更换。规避平台封禁有些平台会对特定域名或带参数的链接进行屏蔽或折叠。使用一个中立的、自建的短域名有时可以绕过这些限制需合规使用。品牌曝光使用自定义域名如yourbrand.cn/xxx的短链接每次分享都是一次品牌展示。理解了价值我们就能确定系统的基本需求生成要快、重定向要稳、数据要能存、并发要能抗。2.2 短链生成算法选型自增ID vs 哈希摘要如何将一个可能上百字符的长URL映射成一个只有几位字符的短码这是系统的核心。主流方案有两种方案一发号器模式自增ID进制转换这是最直观、碰撞概率为零的方案。系统维护一个全局自增的数字ID例如从1开始每来一个长链接就分配一个新ID然后将这个十进制ID转换成62进制a-zA-Z0-9共62个字符的字符串作为短码。优点绝对无碰撞生成简单短码长度有序增长先短后长。缺点短码可预测连续的数字转换可能导致短码被遍历存在安全风险同时ID生成器可能成为单点瓶颈和高并发争用点。生成示例ID10000转换为62进制。计算过程10000 ÷ 62 161 余 18 (对应s)161 ÷ 62 2 余 37 (对应b)2 ÷ 62 0 余 2 (对应c)。从后往前拼接得到短码cbs。方案二哈希摘要模式如MD5、MurmurHash对原始长URL进行哈希运算得到一个固定长度的摘要如MD5是32位16进制串然后截取摘要的前若干位作为短码。优点生成不依赖中心化ID生成器分布式环境下友好不可预测安全性相对较好。缺点存在哈希碰撞风险不同长URL可能生成相同短码。需要通过引入“盐值”Salt或冲突检测重试机制来解决。生成示例对https://pan.xunlei.com/s/vNy5ir_8dq...计算MD5得到a7f3e9d1c45b82...取前6位a7f3e9作为短码。若发生碰撞则在原URL后追加一个随机字符串再哈希或换用另一段摘要。实操心得对于中小流量、希望快速上手的项目我推荐使用发号器模式。它的逻辑简单无碰撞烦恼。为了避免可预测性可以不从1开始而是从一个较大的随机数开始自增。高并发下ID生成可以用数据库自增主键简单但扩展性差或者使用Redis的INCR命令、雪花算法Snowflake等分布式ID生成器。2.3 系统架构与数据流设计一个最小化的短链接系统包含两个主要接口和一张核心表生成接口 (/api/shorten)接收长链接返回短链接。重定向接口 (/:shortCode)接收短码返回302跳转到长链接。映射关系表 (short_urls)存储核心映射关系。其核心数据流如下用户提交长链接L至生成接口。服务端对L进行规范化处理如补齐协议头然后通过选定的算法生成短码S。将(S, L)的映射关系持久化到数据库。返回给用户构建好的短链接如https://你的域名/S。当用户访问此短链接时服务端从路径中解析出短码S。查询数据库找到对应的长链接L。返回HTTP 302状态码并在Location头部带上L浏览器自动跳转。这里有一个关键细节为什么用302临时重定向而不是301永久重定向302跳转每次访问短链接请求都会到达我们的服务器我们可以记录这次访问进行数据统计。这是绝大多数短链接服务的首选。301跳转浏览器会永久缓存这个跳转关系下次再访问同一短链接时浏览器会直接跳向长链接不再请求我们的服务器。这节省了服务器资源但使我们失去了数据追踪的能力。注意除非你明确不需要追踪数据否则务必使用302跳转。3. 技术实现与核心代码拆解我们将使用最通用的技术栈进行演示Spring BootJava Web框架、MySQL数据存储、Redis缓存与发号器。其他语言如PythonFlask/Django、GoGin等思路完全一致。3.1 数据库与缓存设计首先设计核心表short_urlCREATE TABLE short_url ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID用于发号, short_code varchar(10) NOT NULL DEFAULT COMMENT 短码唯一索引, original_url varchar(2048) NOT NULL COMMENT 原始长链接, domain varchar(100) DEFAULT NULL COMMENT 短链接域名用于多域名支持, visits int(11) NOT NULL DEFAULT 0 COMMENT 访问次数, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1-启用0-禁用, expired_at datetime DEFAULT NULL COMMENT 过期时间, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_short_code (short_code), KEY idx_original_url (original_url(255)) -- 对长URL前缀创建索引用于查重 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT短链接映射表;字段设计解析short_code短码长度根据业务定6-8位常见必须建立唯一索引。original_url原始链接长度要足够2048并为其前缀创建索引用于实现“同一长链接生成同一短码”的幂等性需求。visits访问计数每次重定向成功时原子递增。expired_at和status用于实现链接失效功能。缓存设计 重定向接口的QPS每秒查询率会远高于生成接口且对延迟极其敏感。直接查数据库是无法承受的。必须引入缓存。策略使用Redis以short:code:{shortCode}为Key存储序列化后的完整映射对象或直接存储长链接字符串。缓存更新生成短链时写入DB后同步写入Redis。可以通过监听数据库Binlog如Canal异步更新更简单的方式是在保存DB后直接set缓存。缓存读取重定向时先读Redis命中则直接跳转未命中则查DB并回种缓存再跳转。3.2 短码生成服务实现发号器模式我们采用“数据库分段发号”来缓解高并发下的ID争用问题并结合62进制转换。1. 发号器服务 (IdGeneratorService)Service public class IdGeneratorService { Autowired private JdbcTemplate jdbcTemplate; private static final String GET_NEXT_SEGMENT_SQL REPLACE INTO sequence_table (stub) VALUES (a); SELECT LAST_INSERT_ID();; public synchronized long getNextId() { // 简单实现利用数据库REPLACE INTO和LAST_INSERT_ID()获取一个唯一ID // 生产环境建议使用更优方案如Redis INCR或美团Leaf、雪花算法 return jdbcTemplate.queryForObject(GET_NEXT_SEGMENT_SQL, Long.class); } }注意这里的synchronized和单数据库查询只是最简单演示。真实高并发场景下这个REPLACE INTO会成为瓶颈。更优方案是使用Redis的INCR命令或者预分配一个号段例如一次取1000个ID到内存中用完了再取这也就是“分段发号”的精髓。2. 进制转换与短码生成 (Base62Util)public class Base62Util { private static final char[] BASE62_CHARS 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.toCharArray(); private static final int BASE 62; // 十进制ID转62进制短码 public static String encode(long id) { StringBuilder sb new StringBuilder(); while (id 0) { sb.append(BASE62_CHARS[(int)(id % BASE)]); id / BASE; } // 反转字符串并补齐固定长度如6位不足前面补‘0’ return String.format(%6s, sb.reverse().toString()).replace( , 0); } // 62进制短码转十进制ID用于调试或某些查询 public static long decode(String shortCode) { long id 0; for (int i 0; i shortCode.length(); i) { id id * BASE new String(BASE62_CHARS).indexOf(shortCode.charAt(i)); } return id; } }3. 生成接口核心逻辑 (ShortenController)RestController RequestMapping(/api) public class ShortenController { Autowired private ShortUrlService shortUrlService; PostMapping(/shorten) public ApiResponseString shorten(RequestParam String longUrl, RequestParam(required false) Long ttl) { // 1. URL校验与规范化 if (!isValidUrl(longUrl)) { return ApiResponse.error(无效的URL); } String normalizedUrl normalizeUrl(longUrl); // 补充http://等 // 2. 查重如果同一长链接已存在则返回已有的短码幂等性 String existCode shortUrlService.findCodeByUrl(normalizedUrl); if (existCode ! null) { return ApiResponse.ok(buildShortUrl(existCode)); } // 3. 生成短码并保存 String shortCode shortUrlService.generateAndSave(normalizedUrl, ttl); // 4. 返回完整的短链接 return ApiResponse.ok(buildShortUrl(shortCode)); } private String buildShortUrl(String code) { return https://你的域名/ code; // 域名可从配置读取 } }在ShortUrlService的generateAndSave方法中我们完成核心操作public String generateAndSave(String longUrl, Long ttl) { // 1. 获取唯一ID long id idGenerator.getNextId(); // 2. 转换为62进制短码 String shortCode Base62Util.encode(id); // 3. 构建实体并保存到数据库 ShortUrl entity new ShortUrl(); entity.setShortCode(shortCode); entity.setOriginalUrl(longUrl); if (ttl ! null ttl 0) { entity.setExpiredAt(LocalDateTime.now().plusSeconds(ttl)); } shortUrlMapper.insert(entity); // 4. 写入缓存 (Key: short:code:{shortCode}) String cacheKey short:code: shortCode; redisTemplate.opsForValue().set(cacheKey, longUrl, Duration.ofHours(24)); // 设置缓存过期时间 return shortCode; }3.3 重定向服务实现重定向接口是系统的流量入口必须高效、稳定。Controller // 注意不是RestController因为需要返回302重定向视图 public class RedirectController { Autowired private ShortUrlService shortUrlService; Autowired private RedisTemplateString, String redisTemplate; Autowired private VisitRecordService visitRecordService; // 访问记录服务 GetMapping(/{shortCode}) public String redirect(PathVariable String shortCode, HttpServletRequest request, HttpServletResponse response) { // 1. 参数校验 if (shortCode null || shortCode.length() 10) { // 可以返回一个自定义的404页面 return error/404; } String originalUrl null; String cacheKey short:code: shortCode; // 2. 先查缓存 originalUrl redisTemplate.opsForValue().get(cacheKey); // 3. 缓存未命中查数据库 if (originalUrl null) { ShortUrl entity shortUrlService.findByShortCode(shortCode); if (entity null) { return error/404; } // 检查是否过期或禁用 if (entity.getStatus() 0 || (entity.getExpiredAt() ! null entity.getExpiredAt().isBefore(LocalDateTime.now()))) { return error/410; // 410 Gone资源已失效 } originalUrl entity.getOriginalUrl(); // 回种缓存并设置一个合理的过期时间如24小时 redisTemplate.opsForValue().set(cacheKey, originalUrl, Duration.ofHours(24)); } // 4. 异步记录访问日志提升响应速度 // 将记录任务放入消息队列或线程池避免阻塞重定向 visitRecordService.recordAsync(shortCode, request); // 5. 返回302重定向 // 注意这里返回的是视图名Spring会处理跳转。更直接的方式是使用HttpServletResponse // response.sendRedirect(originalUrl); // return null; return redirect: originalUrl; } }访问记录 (VisitRecordService)是数据统计的基础需要记录的信息通常包括short_code短码visit_time访问时间ip访问者IP用于解析地域user_agent浏览器标识用于解析设备、操作系统、浏览器referer来源页面短链接在哪里被点击query_string短链接附带的查询参数如果有实操心得记录访问日志一定要异步化。如果同步写入数据库会严重拖慢重定向速度影响用户体验。可以使用内存队列如Disruptor、消息中间件如Kafka、RocketMQ或者直接扔给一个线程池去处理。核心原则是重定向路径查缓存/DB - 302跳转要尽可能快。4. 高级特性与生产环境考量一个基础的短链接系统已经完成但要用于生产还需要考虑更多。4.1 自定义短码与链接管理用户有时希望短码易于记忆如yourdomain.cn/discount。这需要提供一个自定义接口并在生成时检查短码是否已被占用。public ApiResponseString createCustomShortUrl(String longUrl, String customCode) { // 1. 校验customCode格式只允许字母数字 if (!customCode.matches(^[a-zA-Z0-9]{4,10}$)) { return ApiResponse.error(自定义短码格式无效); } // 2. 检查是否已存在 if (shortUrlService.existsByCode(customCode)) { return ApiResponse.error(该短码已被占用); } // 3. 保存自定义映射注意这类映射通常不与发号器ID关联 // ... 保存逻辑 }同时需要一个管理后台让用户可以查看自己生成的短链接列表、访问数据统计、禁用或删除某个短链。4.2 数据统计分析与可视化这是短链接系统的“大脑”。基于visit_record表我们可以分析访问趋势图按天/小时统计点击量。地域分布根据IP地址解析出省、市绘制地图热力图。设备与浏览器分析解析User-Agent了解用户终端构成。来源分析分析Referer知道流量从哪些平台过来。这些数据可以通过定时任务如每日凌晨跑批计算将聚合结果存入统计表供后台快速查询展示。也可以接入ELKElasticsearch, Logstash, Kibana或类似的大数据栈进行实时分析。4.3 高并发与高可用架构当短链接成为爆款面临海量重定向请求时架构需要升级缓存层面Redis采用主从复制哨兵模式或者直接使用Redis Cluster分片集群防止单点故障和容量瓶颈。数据库层面数据库进行读写分离。写操作生成短链走主库读操作重定向查DB走多个从库。short_url表可以按短码首字母或ID范围进行分库分表。服务层面重定向服务无状态可以轻松水平扩展通过负载均衡如Nginx, SLB将流量分发到多个服务实例。重定向优化对于热门短链接其缓存可能成为热点Key。可以考虑使用本地缓存如Caffeine配合Redis的多级缓存策略或者在Redis前再加一层代理如Twemproxy进行分片。4.4 安全与风控短链接也可能被滥用成为传播恶意网址的帮凶。必须加入风控内容安全检测在生成短链接时调用第三方URL安全检测API如腾讯云、阿里云的网址安全产品对长链接进行扫描拦截色情、赌博、诈骗等恶意链接。频率限制对同一个IP或用户ID在短时间内生成短链接的请求进行限流防止恶意刷量。黑名单域名维护一个黑名单域名列表禁止为这些域名生成短链。访问限制为短链接设置访问密码、仅限特定地域访问、限制访问次数等。5. 常见问题排查与实战技巧在实际开发和运维中你肯定会遇到下面这些问题。5.1 短链接跳转失败或报错问题现象可能原因排查步骤与解决方案访问短链接返回4041. 短码不存在于DB。2. 缓存与DB不一致缓存有但DB已删除。3. Nginx等网关路由配置错误。1. 直接查DB确认记录是否存在且状态为启用。2. 清理该短码对应的Redis缓存触发重新回源。3. 检查服务健康状态和网关路由规则。访问短链接返回302但跳转到错误页面1. 原始长链接本身已失效或无法访问。2. 保存的长链接格式错误如缺少协议头。3. 跳转时Location头部的URL被错误编码或截断。1. 手动访问原始长链接确认其有效性。2. 检查DB中original_url字段的完整性确保在生成时做了规范化处理。3. 抓包查看HTTP响应头中的Location值是否正确。短链接访问缓慢1. 缓存未命中频繁穿透到DB。2. DB查询慢或连接池不足。3. 网络延迟或服务器负载高。1. 检查Redis缓存命中率优化缓存策略如预热热门短链。2. 检查short_code字段是否有索引优化SQL。检查数据库连接池配置。3. 监控服务器资源使用情况考虑升级配置或增加节点。5.2 数据统计不准问题访问次数 (visits) 与实际日志条数对不上。原因并发更新导致丢失。visits visits 1这个操作在并发下不是原子的。解决方案数据库原子更新使用SQLUPDATE short_url SET visits visits 1 WHERE id ?。Redis计数器在Redis中为每个短码维护一个计数器Key:short:visits:{code}使用INCR命令原子递增。然后通过定时任务将Redis中的计数同步回DB。这是更推荐的高并发做法。5.3 短码冲突与哈希碰撞发号器模式理论上不会冲突但要确保发号器全局唯一。在分布式环境下如果使用数据库自增ID需要设置不同的自增步长和起始值如果使用雪花算法要保证机器ID不重复。哈希模式必然存在碰撞风险。解决方案生成短码后先查询DB是否已存在。如果存在则对比已存的长链接是否与当前待生成的一致。如果一致则返回已有短码实现幂等如果不一致则说明发生碰撞可以在原长链接后追加一个随机字符串如时间戳重新哈希直到生成唯一短码。这个过程可以设置一个最大重试次数如3次。5.4 关于“该链接是抖音短视频外部的第三方网页”的思考这是一个非常典型的安全提示场景。当你在抖音等平台内点击一个第三方短链接时平台会弹出这样的警告目的是提醒用户注意风险这也是平台履行安全责任的表现。对于我们自建的短链接服务而言合规性务必确保你缩短的链接指向的内容是合法、安全的。如果传播恶意内容你的服务域名很可能被各大平台封禁。品牌信任使用一个看起来正规、专业的自有域名作为短域名比使用来路不明的免费短域名更能降低用户的警惕性提升点击率。技术应对有些平台可能会屏蔽或限制对某些短域名如t.cn, url.cn的跳转。自建服务可以使用一个尚未被广泛屏蔽的域名但这并非长久之计。根本之道还是提供有价值、安全的内容。最后我个人在维护一个中等流量短链接服务的体会是稳定重于一切。重定向服务一旦出问题所有依赖它的流量都会中断。因此监控服务状态、缓存命中率、DB延迟和告警必须到位。同时数据统计的价值会随着时间推移越来越大前期设计一个可扩展的日志和统计架构会为后续的运营分析省下巨大的力气。这个项目麻雀虽小五脏俱全非常适合用来练手和深入理解Web系统的各个环节。