文章目录
- 前言
- 一、短链
- 1、原理
- 1.1 短链生成原理
- 1.2 短链跳转原理:
- 2、设计:
- 2.1 短链需求
- 2.2 考虑的问题?
- 二、实践案例
- 1、设计表:
- 2、生成短链:
前言
说到 URL 你肯定不陌生,浏览器输入一段 URL,立马就跳转到你想要的网站,不过你应该也遇到过一些带了很多参数、特别长的 URL,看起来就乱糟糟的,能不能把它变短一点?
首先你要知道的是,长链是没法压缩成短链的,那我们这里怎么设计短链?
答案是:映射
。
将需要的长链生成对应的短链,你可以把这个映射关系放在缓存、也可以放在数据库。然后,每次访问短链的时候,都需查到这个对应关系,并重定向
到真正长链接对应的网址。
短链设计本身很简单,不过现在的需求量非常大,这个量级上去了,简单的东西都会变得复杂,因此,本文从简入深探讨锻炼设计原理与方法。
一、短链
URL 短链,就是把原来较长的网址,转换成比较短的网址,比如这样:https://s.xxx.com/1WB5A3
。那这样设计的好处?
首先,网址短、美观、便于发布、传播,可以写更多有意义的文字。
其次,省钱省成本:在短信中,如果含长网址的短信内容超过 70 字,就会被拆成两条发送,而用短链则可能一条短信就搞定,如果短信量大也可以省下不少钱。
再则,易识别:我们平常看到的二维码,本质上也是一串 URL ,如果是长链,对应的二维码会密密麻麻,扫码的时候机器很难识别,而短链则不存在这个问题。
另外,出于安全考虑,不想让有意图的人看到原始网址。
1、原理
1.1 短链生成原理
获取短链:
短链标志(flag)长度一般 5 - 7位,一般由数字、字母(可选择区分大小写)等字符组成。有几种常见的获取方式:
1)截取前缀:我们可以从众多 hash 算法中选择一种(比如 MD5),将 url 经过 hash 计算之后得到一串字符,然后截取前缀 5 - 7位即可。
案例如下:
public static String toHex(String url) {
String hash = Md5.hash(url);
return hash.substring(0, 6);
}
2)除法取模:选择一种 hash 算法可以输出 32 位、64位 … 的整数 x,对字符集合大小取模,得到下标 id,记录下来,从指定的字符编码集合中找到对应字符,循环该操作直到 x 小于编码集大小。
这里的 hash 算法我们选择比较有代表性的 MurMurHash
,是一种简单、高效、散列均匀的算法,众多开源框架中都采用了这种算法,比如 Redis。
这种算法不用你自己去实现,很多工具包已经帮你实现了,我们这里采用 Hutool
工具包里的实现,如下:
int hash = HashUtil.murmur32(url.getBytes());
然后将整数散列值通过 除法取模 运算并从指定字符编码集选定字符输出,如下:
private static String to62HEX(int num) {
num = Math.abs(num);
String chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder sb = new StringBuilder();
int remainder;
while (num > 62 - 1) {
remainder = Long.valueOf(num % 62).intValue();
sb.append(chars.charAt(remainder));
num = num / 62;
}
sb.append(chars.charAt(Long.valueOf(num).intValue()));
return sb.reverse().toString();
}
解决短链碰撞:
所谓碰撞就是出现了重复,短链重复了肯定不行,因为映射关系需要唯一对应。
你也知道,很多常见的散列算法输出的散列值,都有可能出现极小概率冲突,尤其是我们这里只有 5 - 7 位,冲突概率就进一步加大,怎么解决?考虑短链的应用场景,我们介绍常见的一种方式:
加随机串,多次计算,直到找到没有冲突的短链:
// 已被占用,则增加随机延长串进行重新计算
for (int i = 0; i < 10; i++) {
flag = genShortLink(link + RandomStringUtils.randomAlphabetic(10));
if (shortLinkDao.findByFlag(flag).isEmpty()) {
break;
}
}
以上是根据我们目前的场景设计,循环 10 次基本够用,如果你的场景不够用,你可以考虑酌情优化。
1.2 短链跳转原理:
当你从浏览器输入短链地址并回车时,浏览器地址栏立马出现类似于这样一长串的地址:
https://wx36fb82dbb481a6a2.shop.xxxxxx.com.cn/shop/nw0f7c60fd7000/success/19515?type=source&sign=01FB60A657676C6905C84F8C90A1DA99×tamp=1678366800
这个地址才是短链真正的目标地址。也就是说,短链不是真正的目标网址,我们需要重定向找到短链映射的目标地址
,怎么找?从哪里找?
还是那句话,完全取决于你的场景选择,如果你的用量非常大,设计一个单独的短链服务并独立部署也是很有必要的。
当然,如果你的用量一般,直接在基础服务上设计短链接口也是没有问题的。
然后,重定向?请求一般是先通过网关,所以,在网关,需要识别到当前是 短链
请求,然后直接调用接口查询短链的映射地址,并给客户端返回重定向信息。
具体我这里给一个例子:
访问流程基本就以下几点:
-
1)客户端通过浏览器访问短链
-
2)服务端返回 301 / 302 重定向码并携带 location=
原链接
-
3)客户端浏览器重新访问原链接
2、设计:
前文我们讲到了短链的基本原理,实际场景变化万千,我们来看看通常情况下,你在设计短链的时候需要考虑到哪些问题。
2.1 短链需求
1)唯一:给定原始的长 URL ,短链服务能生成比它短且唯一
的 URL,即 短链
2)映射:用户点击短链 , 能跳转到原始的长 URL
3)过期:短链经过一定时间后,会过期
4)REST API:接口需要设计成 REST API
5)过滤:有些关键字眼,比如 NSSB
M 比如这种,可酌情考虑过滤掉。
2.2 考虑的问题?
1)过期:短链一般都不是长期有效的,3个月、6个月、一年 … 处理方式很多,你可以定期手动删除、任务定时检查删除 …
2)过滤:有些客户可能对特殊的字眼比较敏感,比如 SB、SC、MD … 可以酌情考虑过滤掉。
3)安全:短链不可被预测,否则简单的遍历就能把所有短链都遍历完,空耗系统资源。因此,常见的递增主键的方式一般不能采用。
4)高性能:生成短链的过程,以及从短链跳转到原始 URL 要近实时,你可以考虑预先生成足够多的短链、映射关系缓存等等 …
5)高可用:服务不能存在单点故障,如果你的请求量比较大或者要求低容错率,一般你需要考虑集群部署,分散单节点的压力。
6)系统容量预估:需要提前预估你的需求场景,尤其是量大的时候,如何快速存、快速取是一个设计要点。
7)系统扩展:这个一般也是量大的时候需要考虑。
…
二、实践案例
这里我们看看如何快速设计一个可线上使用的短链服务。
1、设计表:
CREATE TABLE `t_short_link` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`flag` varchar(10) NOT NULL COMMENT '短链标识',
`link` varchar(2048) NOT NULL COMMENT '原始链接',
`md5` varchar(64) NOT NULL COMMENT '原始链接的md5',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`deleted` tinyint(4) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_flag` (`flag`),
UNIQUE KEY `uk_md5` (`md5`)
)
其中,flag 就是我们的短链标志,通过 flag 可以找出具体的映射的 link(url),然后重定向访问 link 即可。
另外,这里我们记录了原始链接的 MD5 值,有什么作用?你想,如果你想要判断某个长链接是否已经存在,要去对比原链接吗?太麻烦,直接用 MD5 值来判断,非常方便。
最后,还是建议你从数据库层面确保短链的唯一性,因此,我们这里将 flag、md5 字段设置成了唯一键,确保不会出现重复。
2、生成短链:
业务层面还是比较简单了,主要有查询判断、去重、以及散列冲突解决。
public String createShortLink(Params params) {
String link=params.getLink();
String host=params.getHost();
String md5 = DigestUtils.md5DigestAsHex(link.getBytes(StandardCharsets.UTF_8));
Optional<ShortLink> shortLinkOptional = shortLinkDao.findByMd5(md5);
// 已存在,直接返回
if (shortLinkOptional.isPresent()) {
return getShortLink(shortLink, host);
}
// 加锁避免相同链接并发处理
final RLock lock = redissonClient.getLock(CacheConstants.LOCK_SHORT_LOCK_CREATE + md5);
try {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
// 二次检测
shortLinkOptional = shortLinkDao.findByMd5(md5);
if (shortLinkOptional.isPresent()) {
return getShortLink(shortLinkOptional.get(), host);
}
String flag = "";
flag = genShortLink(link);
if (shortLinkDao.findByFlag(flag).isPresent()) {
// 已被占用,则增加随机延长串进行重新计算
for (int i = 0; i < 10; i++) {
flag = genShortLink(link + RandomStringUtils.randomAlphabetic(10));
if (shortLinkDao.findByFlag(flag).isEmpty()) {
break;
}
}
}
ShortLink shortLink = new ShortLink(flag, link, md5);
shortLinkDao.save(shortLink);
return getShortLink(shortLink, host);
}
} catch (Exception e) {
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
log.error("[short_link] 短链生成失败", e);
throw new BizException(ErrorEnum.SHORT_LINK_FAIL);
} finally {
if (lock.isHeldByCurrentThread() && lock.isLocked()) {
lock.unlock();
}
}
return link;
}
生成短链,核心就是要生成一个不重复的 短链标志(flag),一般短链的 flag 会设计成 5、6 位由数字、字母组成的随机
字符串,原则上长度越长,冲突的概率越小。
具体生成短链的方法我们前面已经提过了,我们简单回顾下:
public static String genShortLink(String link) {
return to62HEX(HashUtil.murmur32(link.getBytes()));
}
至此,一个基本可用短链能够满足一般的场景了 …