ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/8/15 12:33:23 拓冰建站 浏览量
从零构建高可用短链接系统:原理、实现与生产实践

1. 项目概述:从“又臭又长”到“短小精悍”的链接魔法

你有没有遇到过这样的场景?在微信群里分享一个商品链接,结果消息气泡被一串长得离谱的网址占满,不仅不美观,还让人一眼看不到重点;或者在抖音评论区想分享一个网盘资源,结果链接复杂到让人怀疑是不是病毒。我自己就经常被这种“又臭又长”的链接困扰,尤其是在做社群运营和内容分发时,一个简洁、可追踪的短链接,简直是提升用户体验和运营效率的神器。今天要聊的“长链接转短链接”,就是解决这个痛点的核心技术。它不仅仅是把一串字符变短那么简单,背后涉及到的哈希算法、重定向策略、高并发设计以及数据统计,每一个环节都藏着不少门道。

简单来说,长链接转短链接,就是一个将原始的长URL(统一资源定位符)通过特定的算法或服务,映射成一个简短、易记、易传播的新URL的过程。用户访问这个短链接时,服务端会将其“翻译”回原始的长链接,并引导浏览器跳转过去。这个过程,我们称之为“重定向”。听起来简单,但为什么各大平台(如微博的t.cn、腾讯的url.cn)都要自己做一套?因为这里面有品牌曝光、流量控制、数据分析和安全风控等多重价值。对于个人开发者或中小型项目而言,自己实现一个短链接服务,不仅能加深对HTTP协议、数据库设计和系统架构的理解,更能为你的应用增添一个非常实用的功能模块。接下来,我就以一个从业者的视角,带你从零开始,拆解并实现一个具备生产可用性的短链接系统。

2. 核心原理与系统设计思路

2.1 短链接的核心价值:不止于“短”

在动手之前,我们必须先想清楚,我们为什么要做这个转换?除了显而易见的“缩短长度”,它还有几个关键价值:

  1. 美观与易传播:在字符数受限的场景(如微博、短信、二维码),短链接优势巨大。一个t.cn/A6g1TxBf远比一个包含复杂查询参数的原始链接更友好。
  2. 数据追踪与分析:这是对运营者最具吸引力的点。通过短链接,我们可以精确记录每次点击的访问时间、IP地址、用户设备、来源渠道等。例如,你可以知道在抖音评论区分享的链接和微信群里分享的链接,哪个转化率更高。
  3. 流量管理与控制:可以对短链接设置有效期、访问密码、访问次数上限,甚至可以根据访问者的地域、时间进行动态跳转(A/B测试)。原始长链接一旦发出便难以修改,而短链接的后端目标可以随时更换。
  4. 规避平台封禁:有些平台会对特定域名或带参数的链接进行屏蔽或折叠。使用一个中立的、自建的短域名,有时可以绕过这些限制(需合规使用)。
  5. 品牌曝光:使用自定义域名(如yourbrand.cn/xxx)的短链接,每次分享都是一次品牌展示。

理解了价值,我们就能确定系统的基本需求:生成要快、重定向要稳、数据要能存、并发要能抗

2.2 短链生成算法选型:自增ID vs 哈希摘要

如何将一个可能上百字符的长URL,映射成一个只有几位字符的短码?这是系统的核心。主流方案有两种:

方案一:发号器模式(自增ID进制转换)这是最直观、碰撞概率为零的方案。系统维护一个全局自增的数字ID(例如从1开始),每来一个长链接,就分配一个新ID,然后将这个十进制ID转换成62进制(a-zA-Z0-9,共62个字符)的字符串作为短码。

  • 优点:绝对无碰撞,生成简单,短码长度有序增长(先短后长)。
  • 缺点:短码可预测,连续的数字转换可能导致短码被遍历,存在安全风险;同时,ID生成器可能成为单点瓶颈和高并发争用点。
  • 生成示例:ID=10000,转换为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 系统架构与数据流设计

一个最小化的短链接系统包含两个主要接口和一张核心表:

  1. 生成接口 (/api/shorten):接收长链接,返回短链接。
  2. 重定向接口 (/:shortCode):接收短码,返回302跳转到长链接。
  3. 映射关系表 (short_urls):存储核心映射关系。

其核心数据流如下:

  1. 用户提交长链接L至生成接口。
  2. 服务端对L进行规范化处理(如补齐协议头),然后通过选定的算法生成短码S
  3. (S, L)的映射关系持久化到数据库。
  4. 返回给用户构建好的短链接,如https://你的域名/S
  5. 当用户访问此短链接时,服务端从路径中解析出短码S
  6. 查询数据库,找到对应的长链接L
  7. 返回HTTP 302状态码,并在Location头部带上L,浏览器自动跳转。

这里有一个关键细节:为什么用302(临时重定向)而不是301(永久重定向)?

  • 302跳转:每次访问短链接,请求都会到达我们的服务器,我们可以记录这次访问,进行数据统计。这是绝大多数短链接服务的首选。
  • 301跳转:浏览器会永久缓存这个跳转关系,下次再访问同一短链接时,浏览器会直接跳向长链接,不再请求我们的服务器。这节省了服务器资源,但使我们失去了数据追踪的能力。

注意:除非你明确不需要追踪数据,否则务必使用302跳转。

3. 技术实现与核心代码拆解

我们将使用最通用的技术栈进行演示:Spring Boot(Java Web框架)、MySQL(数据存储)、Redis(缓存与发号器)。其他语言如Python(Flask/Django)、Go(Gin)等思路完全一致。

3.1 数据库与缓存设计

首先设计核心表short_url

CREATE 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前缀创建索引,用于查重 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短链接映射表';

字段设计解析

  • short_code:短码,长度根据业务定(6-8位常见),必须建立唯一索引。
  • original_url:原始链接,长度要足够(2048),并为其前缀创建索引,用于实现“同一长链接生成同一短码”的幂等性需求。
  • visits:访问计数,每次重定向成功时原子递增。
  • expired_atstatus:用于实现链接失效功能。

缓存设计: 重定向接口的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 ApiResponse<String> 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; // 域名可从配置读取 } }

ShortUrlServicegenerateAndSave方法中,我们完成核心操作:

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 RedisTemplate<String, 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 ApiResponse<String> 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,知道流量从哪些平台过来。

这些数据可以通过定时任务(如每日凌晨)跑批计算,将聚合结果存入统计表,供后台快速查询展示。也可以接入ELK(Elasticsearch, Logstash, Kibana)或类似的大数据栈进行实时分析。

4.3 高并发与高可用架构

当短链接成为爆款,面临海量重定向请求时,架构需要升级:

  • 缓存层面:Redis采用主从复制+哨兵模式,或者直接使用Redis Cluster分片集群,防止单点故障和容量瓶颈。
  • 数据库层面:数据库进行读写分离。写操作(生成短链)走主库,读操作(重定向查DB)走多个从库。short_url表可以按短码首字母或ID范围进行分库分表。
  • 服务层面:重定向服务无状态,可以轻松水平扩展,通过负载均衡(如Nginx, SLB)将流量分发到多个服务实例。
  • 重定向优化:对于热门短链接,其缓存可能成为热点Key。可以考虑使用本地缓存(如Caffeine)配合Redis的多级缓存策略,或者在Redis前再加一层代理(如Twemproxy)进行分片。

4.4 安全与风控

短链接也可能被滥用,成为传播恶意网址的帮凶。必须加入风控:

  1. 内容安全检测:在生成短链接时,调用第三方URL安全检测API(如腾讯云、阿里云的网址安全产品),对长链接进行扫描,拦截色情、赌博、诈骗等恶意链接。
  2. 频率限制:对同一个IP或用户ID在短时间内生成短链接的请求进行限流,防止恶意刷量。
  3. 黑名单域名:维护一个黑名单域名列表,禁止为这些域名生成短链。
  4. 访问限制:为短链接设置访问密码、仅限特定地域访问、限制访问次数等。

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这个操作在并发下不是原子的。
  • 解决方案
    1. 数据库原子更新:使用SQLUPDATE short_url SET visits = visits + 1 WHERE id = ?
    2. Redis计数器:在Redis中为每个短码维护一个计数器(Key:short:visits:{code}),使用INCR命令原子递增。然后通过定时任务,将Redis中的计数同步回DB。这是更推荐的高并发做法。

5.3 短码冲突与哈希碰撞

  • 发号器模式:理论上不会冲突,但要确保发号器全局唯一。在分布式环境下,如果使用数据库自增ID,需要设置不同的自增步长和起始值;如果使用雪花算法,要保证机器ID不重复。
  • 哈希模式:必然存在碰撞风险。
    • 解决方案:生成短码后,先查询DB是否已存在。如果存在,则对比已存的长链接是否与当前待生成的一致。如果一致,则返回已有短码(实现幂等);如果不一致,则说明发生碰撞,可以在原长链接后追加一个随机字符串(如时间戳)重新哈希,直到生成唯一短码。这个过程可以设置一个最大重试次数(如3次)。

5.4 关于“该链接是抖音短视频外部的第三方网页”的思考

这是一个非常典型的安全提示场景。当你在抖音等平台内点击一个第三方短链接时,平台会弹出这样的警告,目的是提醒用户注意风险,这也是平台履行安全责任的表现。对于我们自建的短链接服务而言:

  1. 合规性:务必确保你缩短的链接指向的内容是合法、安全的。如果传播恶意内容,你的服务域名很可能被各大平台封禁。
  2. 品牌信任:使用一个看起来正规、专业的自有域名作为短域名,比使用来路不明的免费短域名,更能降低用户的警惕性,提升点击率。
  3. 技术应对:有些平台可能会屏蔽或限制对某些短域名(如t.cn, url.cn)的跳转。自建服务可以使用一个尚未被广泛屏蔽的域名,但这并非长久之计。根本之道还是提供有价值、安全的内容。

最后,我个人在维护一个中等流量短链接服务的体会是,稳定重于一切。重定向服务一旦出问题,所有依赖它的流量都会中断。因此,监控(服务状态、缓存命中率、DB延迟)和告警必须到位。同时,数据统计的价值会随着时间推移越来越大,前期设计一个可扩展的日志和统计架构,会为后续的运营分析省下巨大的力气。这个项目麻雀虽小,五脏俱全,非常适合用来练手和深入理解Web系统的各个环节。