有不少网站可以提供长链转短链的服务,例如新浪短地址服务bitly。背后的原理其实是将长链和短链进行一一映射。换而言之,通过各家短链服务商的锻炼和长链是一一对应的。其中的关键点在于如何根据长链生成短链,以及当用户访问短链后怎么处理。

如何生成短地址

比较直观想到的解法是使用哈希函数,但是无论怎样构造哈希函数,都存在哈希碰撞的问题。再次想到碰撞之后可以在hash字符串后面补充其他字符串来实现,但是需要补充多少位呢?这种方案显然不是那么方便。联想到全局唯一,以及数据存储我们很容易想到数据库的自增id,其实这样也是可以的,这里为了实现简单(主要是天然的过期时间),采用了redis来存储数据,其key设计如下:

globalId:全局自增id,用于保证短链的唯一性
shortToLong:短地址到长地址的映射,key为短地址,可以很方便通过短地址找到长地址
longToShortInfo:长链到短链的映射,如果这个键存在值就无需重复生成了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const Redis = require('ioredis');
const base62 = require("base62");

class ShortLinkService {
constructor() {
this.redis = new Redis('192.168.3.118');
}

/**
* 长地址 -> 短地址
*/
async longUrlToShortLink(url, ttl) {
let linkInfo = (await this.redis.exists(`longToShortInfo:${url}`)) && await this.redis.hgetall(`longToShortInfo:${url}`);
if (linkInfo) {
return linkInfo;
}

const id = await this.redis.incr('globalID');
linkInfo = {
shortLink: base62.encode(id),
created: Date.now(),
ttl
};
// 存储短地址对应的长地址
await this.redis.set(`shortToLong:${linkInfo.shortLink}`, url,'EX', ttl);
// 存储长地址对应的短地址(防止重复生成)
await this.redis.hmset(`longToShortInfo:${url}`, linkInfo);
await this.redis.expire(`longToShortInfo:${url}`,ttl);
return linkInfo;
}

/**
* 通过短地址反解长地址
*/
async resolveShortLink(shortLink) {
return await this.redis.get(`shortToLong:${shortLink}`);
}
}

其实短地址的核心就是将一个地址对应了一个整数,之所以将id转化为base62编码是因为base62将每个短链对应一个62进制的数(数字10个,大小写字母一共52)。这样单个位能表示的信息量就更多了,意味着我们可以用更少的位数表示更大的整数。

如何解析短连接

当收短链转长链的请求的时候去redis中查找短链对应的长链,并且向浏览器发送302跳转。注意不使用301的原因是301是永久重定向,浏览器会缓存(虽然301符合HTTP语义并且对服务器的压力更小),这样我们就没法追踪用户了。

进一步优化

上面的代码的问题是我们的发号器是一个单点。如果要做成分布式就需要多个节点保持同步加1,这个以CAP理论来看本身是不可能真正做到的,况且存在比较大的数据冗余。其实这个问题的解决非常简单,我们可以退一步考虑,我们是否可以实现两个发号器,一个发单号,一个发双号,这样就变单点为多点了?依次类推,我们可以实现1000个逻辑发号器,分别发尾号为0到999的号。每发一个号,每个发号器加1000,而不是加1。这些发号器独立工作,互不干扰即可。而且在实现上,也可以先是逻辑的,真的压力变大了,再拆分成独立的物理机器单元。1000个节点,估计对人类来说应该够用了。如果你真的还想更多,理论上也是可以的。

参考资料

系统设计-设计短网址系统Tiny-URL