Spring Boot 实战:从零设计短链系统
问题分析:为什么我们需要'短链'?
相信很多小伙伴在日常生活中都收到过一些营销短信,里面附带了一些链接地址。是否发现链接实际上很短,点击后会再次访问一个相对长的链接。
例如我们收到的短链可能是这样子:
https://www.example.com/aBcDeF
点开它,会跳转到以下原始长链接(甚至更长…参数更多):
https://www.example.com/product/detail?id=134985&from=share&ref=longtext&utm_source=wechat
那就有小伙伴问了,直接分享这个长链接不行么?为何多此一举通过短链条转长链?
那我们就来看看常见的短链应用场景:
- 社交平台分享:如微博、朋友圈、抖音简介中限制字符数;
- 短信营销:短链可以有效减少短信的字符数,从而减少短信费用;
- 营销追踪:每个渠道生成不同短链,统计访问量;
- 二维码应用:短链生成二维码更清晰、美观;
- 安全控制:短链服务可做访问限制、过期时间等策略。
系统设计分析
要实现一个简易的短链系统,我们至少需要解决以下问题:
| 模块 | 说明 |
|---|---|
| 短链生成策略 | 如何将长链接映射成短码?并保证唯一性和随机性 |
| 存储设计 | 如何保存短码与原始链接的映射关系? |
| 重定向服务 | 访问短链时,如何高效跳转到原始链接? |
| 过期策略(可选) | 是否允许短链设置有效期? |
| 统计分析(可选) | 统计点击量、来源等指标。 |
如果你要实现一个更高性能的短链系统,你还需要引入缓存设计,如 redis。本章节更多的是探讨短链系统,小伙伴们可以自行在业务层追加缓存来实现减少数据库的访问。
数据库设计
这里我们只实现最核心的短链映射功能。
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| short_code | varchar(10) | 短链编码(唯一索引) |
| original_url | varchar(500) | 原始长链接 |
| expire_time | datetime | 过期时间(可选) |
| create_time | datetime | 创建时间 |
SQL 建表语句:
CREATE TABLE short_link (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
short_code VARCHAR(10) NOT NULL UNIQUE,
original_url VARCHAR(500) NOT NULL,
expire_time DATETIME NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
短链生成策略设计
短链生成核心是将长链接转换成唯一的短码,常见方式有:
- 自增 ID + Base62 编码(简单可靠,唯一性高)
- 哈希算法(MD5/SHA)(可重复生成,长度可控)
自增 ID + Base62 编码:Base62 使用
[0-9][a-z][A-Z]共 62 个字符编码,可生成短而唯一的字符串。哈希算法(如 MD5 或 SHA-256)的主要作用是将一个长度不定的输入(如 URL)映射为固定长度的输出(哈希值)。
实战开始
下面讲解以上两种的实现方式。
❶ 项目依赖
在正式开发前我们先在 SpringBoot 项目引入相关依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
❷ 构建实体类
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "short_link")
public class ShortLink {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String shortCode;
@Column(nullable = false, length = 500)
private String originalUrl;
private LocalDateTime expireTime;
private LocalDateTime createTime = LocalDateTime.now();
}
❸ 工具类
Base62 工具类:使用自增 ID + Base62 编码。
public class Base62Utils {
private static final String CHARSET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static String encode(long num) {
StringBuilder sb = new StringBuilder();
while (num > 0) {
int index = (int) (num % 62);
sb.append(CHARSET.charAt(index));
num /= 62;
}
return sb.reverse().toString();
}
}
哈希生成工具类(MD5/SHA):我们将实现一个工具类,通过 MD5 或 SHA-256 来生成哈希值,然后从中截取短链。
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class HashUtils {
// 使用 MD5 算法生成短链
public static String generateHash(String input, String algorithm) {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] hashBytes = md.digest(input.getBytes());
// 转换为 16 进制表示
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("哈希算法异常:" + e.getMessage());
}
}
// 使用 MD5 生成短链的前 8 位
public static String generateShortLink(String originalUrl) {
String md5Hash = generateHash(originalUrl, "MD5");
return md5Hash.substring(0, 8); // 返回前 8 位
}
// 使用 SHA-256 生成短链的前 10 位
public static String generateShortLinkSHA(String originalUrl) {
String sha256Hash = generateHash(originalUrl, "SHA-256");
return sha256Hash.substring(0, 10); // 返回前 10 位
}
}
❹ 服务层 Service
为了更好的区分两种方式,这里我们将用两个 Service 来方便区分。
Base62ShortLinkService
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class Base62ShortLinkService {
private final ShortLinkRepository repository;
@Transactional
public String createShortLink(String originalUrl) {
ShortLink entity = new ShortLink();
entity.setOriginalUrl(originalUrl);
ShortLink saved = repository.save(entity);
String shortCode = Base62Utils.encode(saved.getId());
saved.setShortCode(shortCode);
repository.save(saved);
return shortCode;
}
public String getOriginalUrl(String shortCode) {
ShortLink link = repository.findByShortCode(shortCode);
if (link == null) {
throw new RuntimeException("短链不存在或已失效");
}
return link.getOriginalUrl();
}
}
HashShortLinkService
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class HashShortLinkService {
private final ShortLinkRepository repository;
@Transactional
public String createShortLink(String originalUrl) {
// 先生成哈希值(使用 MD5 或 SHA-256)
String shortCode = HashUtils.generateShortLink(originalUrl);
// 检查数据库中是否已经存在该短链,避免重复
if (repository.findByShortCode(shortCode) != null) {
shortCode = HashUtils.generateShortLinkSHA(originalUrl); // 如果冲突,尝试用 SHA-256 生成短链
}
// 保存短链映射
ShortLink entity = new ShortLink();
entity.setOriginalUrl(originalUrl);
entity.setShortCode(shortCode);
repository.save(entity);
return shortCode;
}
public String getOriginalUrl(String shortCode) {
ShortLink link = repository.findByShortCode(shortCode);
if (link == null) {
throw new RuntimeException("短链不存在或已失效");
}
return link.getOriginalUrl();
}
}
❺ 控制层 Controller
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.view.RedirectView;
@RestController
@RequiredArgsConstructor
public class ShortLinkController {
private final Base62ShortLinkService base62ShortLinkService;
private final HashShortLinkService hashShortLinkService;
// 创建短链
@PostMapping("/api/shorten")
public String create(@RequestParam String url) {
String code = base62ShortLinkService.createShortLink(url);
// 切换 Hash
// String code = hashShortLinkService.createShortLink(url);
return "短链生成成功:" + "http://localhost:8080/" + code;
}
// 访问短链
@GetMapping("/{code}")
public RedirectView redirect(@PathVariable String code) {
String originalUrl = base62ShortLinkService.getOriginalUrl(code);
// 切换 Hash
// String originalUrl = hashShortLinkService.getOriginalUrl(code);
// 302 重定向
return new RedirectView(originalUrl);
}
}
❻ 运行与测试
根据控制层的代码演示,小伙伴们可以自行切换两种实现方式,进行测试。
以 Hash 为例生成短链接口:
POST http://localhost:8080/api/shorten?url=https://www.example.com/article/long-url
返回:
短链生成成功:http://localhost:8080/d41d8cd9
浏览器中打开短链,直接看跳转效果!
总结
本文通过讲解了两种常见的短链生成方式:自增 ID + Base62 编码 和 哈希算法。并通过完整的简易示例代码,让开发者可以快速理解并掌握短链系统的设计!


