设计一款高效的短链服务系统
目录
1、短链系统(Short URL System)
1.1、介绍
1.2、设计背景
1.3、缺陷
1.4、使用场景
2、系统设计
2.1、短码如何生成?且保证全局唯一且不重复?
2.2、如何存储?数据库设计
2.3、高并发下如何扛住流量
2.4、短码会不会被穷举?如何防刷?
2.5、如何统计
2.6、短链过期怎么实现
2.7、 CDN/网关怎么配合
3、工程实践
3.1、系统目标
3.2、数据库设计
3.3、技术选型
3.4、短码生成策略
3.5、核心代码实现
4、性能与安全优化建议
前言
在互联网信息爆炸的时代,URL 作为资源定位的核心载体,常常因携带大量参数而变得冗长复杂。这不仅影响用户体验——尤其在移动端、社交媒体和二维码场景下难以传播与识别,也给营销追踪、安全管控和数据分析带来挑战。
如下所示:

短链系统(Short URL System)应运而生。它通过将原始长链接映射为简短、易记、美观的短码(如https://s.example.com/Abc123),在不改变业务逻辑的前提下,显著提升了链接的可分享性、可管理性与安全性。
从微博、微信到电商平台的促销活动,短链已成为现代 Web 应用不可或缺的基础设施之一。
本项目旨在设计并实现一个高性能、高可用、可扩展的短链服务。系统基于 Spring Boot 构建,结合分布式 ID 生成、Redis 缓存加速、异步统计与安全防护机制,兼顾开发效率与生产级稳定性。无论是用于内部运营工具,还是支撑亿级流量的公开服务,该系统均能提供可靠支撑。
1、短链系统(Short URL System)
1.1、介绍
短链不是为“技术炫技”而生的,而是为了解决 特定业务场景下的现实问题。
1.2、设计背景
场景 1:URL 太长,用户没法用
例子:微信/QQ 群里发一个带参数的链接
https://example.com/activity?user_id=12345&campaign=summer2024&source=wechat
&token=abc...xyz(长达 200+ 字符)
问题:
1、微信会自动换行,点开变成两截 → 打不开;
2、用户复制容易漏掉后半段;
3、二维码太密,扫不出来
解决方案:
→ 生成 https://s.xxx.com/Abc123;
→ 链接短、美观、二维码清晰、不怕截断;
场景 2:需要追踪流量来源(营销分析)
-
运营要推广一个活动,通过:
-
短信发给 A 用户群 → 短链s.xxx.com/sms
-
公众号推送给 B 用户群 → 短链 s.xxx.com/wx
-
抖音评论区放链接 → 短链s.xxx.com/dy
-
-
后台统计:
-
sms 链接点击了 1 万次,转化率 5%
-
dy 链接点击了 50 万次,但转化率只有 0.1%
-
-
价值:知道哪个渠道效果好,优化投放策略
📌 本质:把“不可追踪的流量”变成“可分析的数据”
场景 3:安全或风控需求(隐藏真实地址)
-
秒杀、抢购、限量发售时,不想让机器人提前知道真实接口
-
对外只公布一个短链,真实 URL 动态绑定
-
甚至可以做到:同一个短链,不同用户点开跳不同页面(防刷)
📌 本质:增加攻击者成本,不是为了性能
1.3、缺陷
如下所示:

1.4、使用场景
如下所示:

举个例子:
- 小公司做促销 → 用微信自带的“公众号文章链接”,或者用 新浪短网址
- 大厂(如淘宝、拼多多)→ 自研短链平台,每天处理几十亿次跳转
2、系统设计
2.1、短码如何生成?且保证全局唯一且不重复?
采用 分布式唯一 ID + Base62 编码 的方式生成短码。
常见分布式id方案对比如下:

具体来说:
1、使用 Snowflake 算法(或公司内部的分布式 ID 服务,如美团 Leaf)生成全局唯一的 long 类型 ID;
2、将该 ID 通过 Base62 编码(字符集为0-9a-zA-Z)转换为 6~8 位的字符串,作为短码;
3、在数据库中,short_code 字段设置为 唯一索引,防止极端情况下的冲突(虽然 Snowflake 冲突概率极低);
4、如果发生冲突(比如时钟回拨导致 ID 重复),就重试一次生成新 ID。
好处:
- 短码无序,不易被猜测,安全性高;
- 不依赖数据库自增 ID,支持水平扩展;
- 6 位 Base62 可支持约 568 亿条链接(62⁶),完全满足业务需求。
2.2、如何存储?数据库设计
数据库表设计:
CREATE TABLE short_url (id BIGINT PRIMARY KEY, -- 分布式IDlong_url TEXT NOT NULL, -- 原始长链接short_code VARCHAR(10) UNIQUE, -- 短码,唯一索引created_at DATETIME,expired_at DATETIME, -- 可选:过期时间visit_count INT DEFAULT 0 -- 可选:访问次数
);
CREATE INDEX idx_short_code ON short_url(short_code);
存储策略上:
读多写少:99% 是跳转请求(读),只有创建时是写;
用 Redis 缓存 short_code → long_url 的映射,设置合理 TTL(或永不过期);
跳转时优先查 Redis,未命中再查 DB,并回填缓存;
对于超大 long_url,可考虑分表或使用对象存储(如 OSS)+ 引用 ID,但一般 TEXT 足够。
2.3、高并发下如何扛住流量
面对高并发跳转请求,会从三层优化:
1. 缓存层:
所有热点短链映射都缓存在 Redis,QPS 可轻松支撑 10w+。Redis 集群部署,避免单点。
2. 服务层:
跳转接口是无状态的,用 Spring Boot + Nginx 做负载均衡,K8s 自动扩缩容。
3. 边缘优化(高阶):
对于超大流量场景(如微博),可将热点映射推送到 CDN 边缘节点,用户请求直接在 CDN 层 302 跳转,完全绕过后端服务。同时,对
/create接口做 限流(如 Sentinel 或 Guava RateLimiter),防止恶意刷短链。
2.4、短码会不会被穷举?如何防刷?
理论上,短码空间有限(如 6 位 Base62 ≈ 568 亿),但暴力穷举成本极高,实际风险较低。更关键的是:短链本身不应承载敏感信息。
防护策略包括:
- 短码长度 ≥ 6 位,避免空间过小;
- 不使用连续 ID(如自增 ID),防止被顺序猜测;
- 敏感链接额外加 token 或权限校验(例如:/Abc123?token=xxx),仅靠短码无法访问;
- 监控异常行为:同一 IP 短时间内大量 404 请求,触发风控拦截;
- 短链设置有效期,过期自动失效。
总之,安全不能只依赖“隐蔽性”,而要靠“权限控制 + 监控”。
2.5、如何统计
统计是有价值的(用于运营分析、渠道效果评估),但不能影响跳转性能。
做法:
- 跳转接口 异步记录点击事件;
- 将点击日志发送到 消息队列(如 Kafka 或 RocketMQ);
- 由独立的消费服务批量处理,更新 DB 中的 visit_count,或写入数据仓库供 BI 分析;
- 跳转延迟几乎不受影响(< 10ms);
- 即使 MQ 暂时不可用,也可降级(如本地缓冲 + 定时上报);
- 支持后续扩展:用户地域、设备、来源等维度分析。
2.6、短链过期怎么实现
过期机制我会结合 缓存 TTL + DB 状态检查 实现:
- 创建短链时,如果设置了有效期(如 7 天),则:
- 在 Redis 中设置 key 的 TTL(如 expire short:Abc123 604800);
- 同时在 DB 的 expire_at 字段记录过期时间;
- 用户访问时:
- 先查 Redis,若存在则直接跳转;
- 若 Redis 未命中,再查 DB,并校验当前时间是否 < expire_at;
- 如果已过期,返回 404,并清理 DB(或由定时任务归档);
这种方式兼顾性能与准确性,避免定时任务扫全表的低效操作。
2.7、 CDN/网关怎么配合
对于超大规模场景(如 Twitter、微博),完全可以不用后端参与跳转。
具体做法:
- 将热点短链的映射关系(如 JSON 文件或配置表)定期推送到 CDN 边缘节点;
- 用户请求 https://s.xxx.com/Abc123 时,CDN 直接返回 HTTP 302 跳转响应,Location 为原始长链接;
- 整个过程 0 次后端调用,延迟极低,成本也低;
当然,这要求:
- 热点集中(符合 20% 链接占 80% 流量的规律);
- 有完善的配置推送和版本管理机制;
- 冷门链接 fallback 到后端服务。
这是一种典型的 边缘计算 + 静态化 优化思路。
3、工程实践
3.1、系统目标
- 用户输入长链接(如
https://example.com/very/long/path?query=123) - 系统生成唯一短码(如
abc123) - 访问
http://yourdomain/abc123自动跳转到原长链接 - 支持统计(可选)、有效期、去重等
3.2、数据库设计
CREATE TABLE short_url (id BIGINT AUTO_INCREMENT PRIMARY KEY,long_url VARCHAR(2048) NOT NULL, -- 原始长链接short_code VARCHAR(10) NOT NULL UNIQUE, -- 短码(如 abc123)created_at DATETIME DEFAULT CURRENT_TIMESTAMP,expired_at DATETIME NULL, -- 过期时间(可选)visit_count INT DEFAULT 0 -- 访问次数(可选)
);-- 建议加索引
CREATE INDEX idx_short_code ON short_url(short_code);
CREATE INDEX idx_long_url ON short_url(long_url(255)); -- MySQL 对长字段需指定前缀
🔒 注意:
long_url可能超过 255 字符,所以用VARCHAR(2048)或TEXT。
3.3、技术选型

3.4、短码生成策略
方案1:自增 ID + Base62 编码(推荐)
- 利用数据库自增 ID 的唯一性
- 将 ID 转为 62 进制(0-9, a-z, A-Z),缩短长度
public class Base62Util {private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";private static final int BASE = CHARS.length();public static String encode(long num) {if (num == 0) return String.valueOf(CHARS.charAt(0));StringBuilder sb = new StringBuilder();while (num > 0) {sb.append(CHARS.charAt((int)(num % BASE)));num /= BASE;}return sb.reverse().toString();}
}
示例:ID=123456 → 短码=
"21Ic"
3.5、核心代码实现
1. Entity
@Entity
@Table(name = "short_url")
public class ShortUrl {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(nullable = false, length = 2048)private String longUrl;@Column(nullable = false, unique = true, length = 10)private String shortCode;private LocalDateTime createdAt = LocalDateTime.now();private LocalDateTime expiredAt;private Integer visitCount = 0;// getters & setters
}
2. Repository
public interface ShortUrlRepository extends JpaRepository<ShortUrl, Long> {Optional<ShortUrl> findByShortCode(String shortCode);Optional<ShortUrl> findByLongUrl(String longUrl); // 用于去重
}
3. Service
@Service
@Transactional
public class ShortUrlService {@Autowiredprivate ShortUrlRepository repository;// 可选:集成 Redis 缓存// @Autowired private RedisTemplate<String, String> redisTemplate;public String createShortUrl(String longUrl) {// 1. 去重:如果已存在,直接返回var existing = repository.findByLongUrl(longUrl);if (existing.isPresent()) {return existing.get().getShortCode();}// 2. 保存到 DB(利用自增 ID)ShortUrl entity = new ShortUrl();entity.setLongUrl(longUrl);ShortUrl saved = repository.save(entity); // 此时 ID 已生成// 3. 生成短码String shortCode = Base62Util.encode(saved.getId());saved.setShortCode(shortCode);repository.save(saved); // 再次保存短码return shortCode;}public String getLongUrl(String shortCode) {// 可加入 Redis 缓存逻辑return repository.findByShortCode(shortCode).filter(url -> url.getExpiredAt() == null || url.getExpiredAt().isAfter(LocalDateTime.now())).map(ShortUrl::getLongUrl).orElse(null);}public void incrementVisitCount(String shortCode) {repository.findByShortCode(shortCode).ifPresent(url -> {url.setVisitCount(url.getVisitCount() + 1);repository.save(url);});}
}
4. Controller
@RestController
public class ShortUrlController {@Autowiredprivate ShortUrlService shortUrlService;// 创建短链@PostMapping("/shorten")public ResponseEntity<Map<String, String>> shorten(@RequestBody Map<String, String> request) {String longUrl = request.get("url");if (longUrl == null || !longUrl.startsWith("http")) {return ResponseEntity.badRequest().build();}String shortCode = shortUrlService.createShortUrl(longUrl);String shortUrl = "http://localhost:8080/" + shortCode; // 实际应使用域名return ResponseEntity.ok(Map.of("shortUrl", shortUrl, "code", shortCode));}// 跳转@GetMapping("/{code}")public void redirect(@PathVariable String code, HttpServletResponse response) throws IOException {String longUrl = shortUrlService.getLongUrl(code);if (longUrl == null) {response.sendError(HttpServletResponse.SC_NOT_FOUND, "短链不存在或已过期");return;}// 可选:异步增加访问次数(避免阻塞跳转)shortUrlService.incrementVisitCount(code);response.sendRedirect(longUrl); // 默认 302 临时重定向// 若需 301:response.setStatus(301); response.setHeader("Location", longUrl);}
}
部署示例
- 域名:s.example.com
- 用户访问:https://s.example.com/abc123→ 跳转到原始链接;
- 创建接口:https://api.example.com/shorten
4、性能与安全优化建议
| 优化点 | 说明 |
|---|---|
| Redis 缓存 | 缓存shortCode ----> longUrl 映射,减少 DB 查询 |
| 异步统计 | 使用 @Aysnc 异步更新访问次数. |
| 限流 | 对 /shorten 接口做 IP 限流(防止刷短链) |
| URL 校验 | 验证 longUrl 是否合法、是否黑名单域名 |
| HTTPS | 生产环境务必使用 HTTPS |
| 短码长度控制 | Base62 下,6位可支持 568亿条(62^6 ≈ 5.68e10) |
| 分布式 ID | 若不用 DB 自增,可用 Snowflake(需处理时钟回拨) |
总结:
短链系统不是技术亮点,而是一个“业务工具”。它解决的不是“代码怎么写更快”,而是“用户怎么用更方便”、“运营怎么分析更清楚”、“安全怎么防更有效”。
参考文章:
1、https://blog.csdn.net/wy990880/article/details/146535053?ops_request_misc=&request_id=&biz_id=102&utm_term=%E7%9F%AD%E9%93%BE%E7%B3%BB%E7%BB%9F&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-1-146535053.142^v102^pc_search_result_base2&spm=1018.2226.3001.4187
https://blog.csdn.net/wy990880/article/details/146535053?ops_request_misc=&request_id=&biz_id=102&utm_term=%E7%9F%AD%E9%93%BE%E7%B3%BB%E7%BB%9F&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-1-146535053.142^v102^pc_search_result_base2&spm=1018.2226.3001.4187
