天天看点

长链接转短链接的一次尝试

偶然的一次业务需求,需要使用到这样的功能。虽然很多大平台提供了这样的接口(新浪,百度等等)。但是还是对其中的原理想在梳理一下。

我们不妨先来看一下短链接服务的整个流程,以前面提到的微博短网址服务为例。用户输入想要缩短的长网址,转化后得到一个以

http://t.cn

开头的短网址,然后用户将该链接通过微信或者微博等方式分享给朋友,其他人点击之后即可进入原本长网址所对应的页面。整个流程如下图所示:

长链接转短链接的一次尝试

从图中可以很清楚地看到,实现短链接服务的关键是两个步骤:1、如何把一个任意长的字符串转化成一个较短的字符串;2、从短网址如何还原出长网址。第一个问题很容易让人想到哈希算法,通过一定的方式将任意长的文本转化成一个固定长度的字符串,只要目标字符串的长度适当,那么不同的输入几乎不可能对应同一个字符串。不过这么做有个缺点就是无法从得到的结果还原输入的字符串,因此不适用于我们的场景。但基于哈希算法的思想,我们可以设计一种以多进制为基础的算法完成这个任务。

具体而言,我们可以创建一个用于保存长网址的数据表,比如就叫Url,这张表很简单,只需要两个字段,一个主键用于保存id,一个url字段用于存放原始的长网址,每个长网址都在这张表有一条记录。当进行长网址转换时,先检查数据表中是否存在该长网址,若是直接获取该记录的id,否则在数据表中创建一条新记录,并返回其id。对于这个id,我们可以得到一个多进制表示下的新值,比如在以“0-9a-z”这36个字符表示的36进制中,一亿这个数字可以被表示成1njchs,只需要6个字符即可,将这6个字符拼接到准备好的域名后即可得到一个对应的短网址返回给用户。由于一亿个网址只需要6个字符,因此这种方式足够满足大部分网站的需求。

而当用户点击了我们生成的短网址后,只需要将代表多进制的这部分提取出来,还原成十进制的数字后查表即可得到原始的长网址,再根据网址做一个重定向即可让用户访问到原始的网页。具体的实现可以参考下面的

typescript

代码

// 将原始的长链接通过36进制转化为短链接
export async function long2short(url: string) {
    if (!url.startsWith('http://') && !url.startsWith('https://')) {
        throw new Error('Invalid url');
    }
    if (url.startsWith(config.shortLinkBaseUrl)) {
        return url;
    }
    let item = await Url.getByUrl(url);
    if (!item) {
        item = await Url.create(url);
    }
    return config.shortLinkBaseUrl + item.id.toString(36);
}

// 将短链接还原为真实的长链接
export async function short2long(url: string) {
    let item = await Url.select(Number.parseInt(url, 36));
    if (!item) {
        throw new Error('Invalid url');
    }
    return item.url;
}
           

这里的config.shortLinkBaseUrl也就是我们用来做短链接服务的域名,在前面的例子中就是http://t.cn,我们需要在这个域名对应的服务器内实现短链接的服务,同时这个域名本身不能太长,否则就失去了它的意义。另外还有一点值得注意,就是在根据长网址去数据表查找它是否存在时,因为长网址可以任意长,因此直接用它作为索引在数据表中查找的话效率较低,可以考虑在表中增加一个hash字段,保存长网址的哈希值,并通过查找哈希值来判断条目是否存在,提高查找的效率。

以上就是短链接服务的基本实现方法,最核心的其实就是多进制的使用。

当然网上还有一种对于这种功能实现最优的方法。

最优的算法

通过发号原理

顾名思义这个系统的第一个请求过来了,我们以微博为例,短链接系统的第一个请求我们可以给变为t.cn/0,第二个t.cn/1等等;

实现的方式也会很简单

1、小型的系统用MySQL的自增索引就可以满足。

2、大型系统可以考虑分布式key-value系统。

存储原理

发号策略是这样的,当一个新的链接过来时,发号器发一个号与之对应。往后只要有新链接过来,发号器不停发号就好。举个例子,第一个进来的链接发号器发0号,对应的短链接为 xx.xxx/0,第二个进来的链接发号器发1号,对应的短链接为 xx.xxx/1,以此类推。

发号器发出的10进制号需要转换成62进制,这样可以大大缩短号码转换成字符串后的长度。比如发号器发出 10,000,000,000 这个号码,如果不转换成62进制,直接拼接在域名后面,得到这样一个链接 xx.xxx/10000000000。将上面的号码转换成62进制,结果为AOYKUa,长度只有6位,拼接得到的链接为 xx.xxx/AOYKUa。可以看得出,进制转换后得到的短链接长度变短了一些。6位62进制数,对应的号码空间为626,约等于568亿,所以基本上不用担心发号器无号可发的情况。

高并发场景下

上面设计看起来有一个单点,那就是发号器。如果做成分布式的,那么多节点要保持同步加1,多点同时写入。这样难以避免产生单点性能瓶颈。因此我们可以考虑将单点变为多点。我们可以引入多个机器,我们可以设定机器A发号只发向100取余等于0的数字100n,同理机器B只发向100取余等于1数字 100n+1,以此类推,各个机器相互独立互不干扰,我们可以随时扩展我们的机器了。

同一长链接,每次转成的短链接是否一样

同一长链接,每次转成的短链接不一定一样,原因在于如果查询缓存时,如果未命中,发号器会发新号给这个链接。需要说明的是,缓存应该缓存经常转换的热门链接,假设设定缓存过期时间为一小时,如果某个链接很活跃的话,缓存查询命中后,缓存会刷新这个链接的存活时间,重新计时,这个链接就会长久存在缓存中。

我们也可以引入LRU算法。进行淘汰我们不经常使用到的链接。

重定向问题

选取301,还是302?

301是永久重定向,302是临时重定向。

如果选择301:短地址生成以后就不会变化,所以用301是符合http语义的。同时对服务器压力也会有一定减少。这样一来,我们就无法统计到短地址被点击的次数了。

如果选择302:选择302虽然会增加服务器压力,但是可以统计到短地址被点击的次数了,我可以针对点击的次数来进行后期的大数据处理,机器学习,以及推荐算法。

选择302还是301,想必读者心中有肯定有数了。

以上就是我看到网上的一下帖子。特此放在一起。也算做个参考吧。虽然是一个很小的需求,但是还是有的深究的。