短网址介绍
想必大家也经常收到垃圾短信吧…短信中的链接一般都是短链接,类似于下图这样:
为什么这里面的URL都是短的呢?有什么好处呢?怎么做到的呢?
短URL的好处
-
短信和许多平台(微博)有字数限制 ,太长的链接加进去都没有办法写正文了.
-
好看。 比起一大堆不知所以的参数,短链接更加简洁友好.
-
方便做一些统计。 你点了链接会有人记录然后分析的.
-
安全。 不暴露访问参数.
这就是为什么我们现在收到的垃圾短信大多数都是短URL的原因了.
短URL基础原理
短URL从生成到使用分为以下几步:
-
有一个服务,将要发送给你的长URL对应到一个短URL上.例如www.baidu.com -> www.t.cn/1
-
把短URL拼接到短信等的内容上发送.
-
用户点击短URL,浏览器用301/302进行重定向,访问到对应的长URL.
-
展示对应的内容.
跳转用301还是302?
301是永久重定向,302是临时重定向。短地址一经生成就不会变化,所以用301是符合http语义的。同时对服务器压力也会有一定减少。
但是如果使用了301,我们就无法统计到短地址被点击的次数了。而这个点击次数是一个非常有意思的大数据分析数据源。能够分析出的东西非常非常多。所以选择302虽然会增加服务器压力,但是我想是一个更好的选择。
那么短URL系统是怎么设计的?
常见的错误理解
- 实现一个算法,将长地址转成短地址,实现长和短一一对应,然后再实现它的逆运算,将短地址还能换算回长地址。
实现一一对应,本身就是不可能的。何况如果真的有这么一个算法和逆运算,那么基本上现在的压缩软件都可以歇菜了。
- 和上面一样,也找一个算法,把长地址转成短地址,但是不存在逆运算。我们需要把短对长的关系存到DB中,在通过短查长时,需要查DB。
这个想法没有改变本质,如果真的有这个算法, 那么必然会出现碰撞的,也就是多个长地址转成了同一个短地址。
- 用一个hash算法,我承认它会碰撞,碰撞后我再在后面加1,2,3不就行了。
这样的话可能要进行大于小于或者是like查询才知道应该加1还是2还是3等等,这个也可能由于输入的长地址集的不确定性,导致生成短地址时间的不确定性。
同样错误的想法还有,随机生成一个短地址,去查找是否用过,用过就再随机,如此往复,直到随机到一个没用过的短地址。
正确的思路
建立一个发号器
,每次有一个新的长URL进来,我们就增加一,并且将新的数值返回.第一个来的URL返回"www.x.cn/0",第二个返回"www.x.cn/1",第11个是 http://xx.xx/a 第依次往后,相当于实现了一个62进制
的自增字段即可。.
这里为什么要是用62进制?
因为64进制会含有±这些特殊字符。
62位字符:
private static final String baseDigits = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
字符超长问题
即使到了 10 亿 (Billion) 转换而成的 62 进制也无非是 6 位字符,所以长度基本不在考虑范围内,这个范围足够使用了。
相关细节问题
对应关系如何存储?
这个对应数据肯定是要落盘的,不能每次系统重启就重新排号,所以可以采用 mysql 等数据库来存储。而且如果数据量小且 qps 低,直接使用数据库的自增主键就可以实现.
如何保证长短链接一一对应?
按照上面的发号器策略,是不能保证长短链接的一一对应的,你连续用同一个 URL 请求两次,结果值都是不一样的.
为了实现长短链接一一对应,我们需要付出很大的空间代价,尤其是为了快速响应,我们可以需要在内存中做一层缓存,这样子太浪费了.
但是可以实现一些变种的,来实现部分的一一对应,比如将最近 / 最热门的对应关系存储在 K-V 数据库中,这样子可以节省空间的同时,加快响应速度.
同一长网址短 url 是否应该相同问题
发号策略中,是不判断长地址是否已转过,所以造成结果就是一长对多短,有人说浪费空间,建立一个长对短的 map 存储即可,但是用 map 存储本身就是浪费大量空间,甚至是用大空间换小空间,这就要考虑是否真有必要做一一对应,不能一对多;
最简单方案:建一个长对短的 map, 空间换空间.
更好的方案:用 map 存储” 最近” 生成的长对短关系,一小时过期机制实现 LRU
淘汰
长对短流程:
-
这个 “最近” 表中查看一下,看长地址有没有对应的短地址
-
有就直接返回,并且将这个 key-value 对的过期时间重置为一小时
-
如果没有,就通过发号器生成一个短地址,并且将这个 “最近” 表中,过期时间为 1 小时
当一个地址被频繁使用,那么它会一直在这个 key-value 表中,总能返回当初生成那个短地址,不会出现重复的问题。如果它使用并不频繁,那么长对短的 key 会过期,LRU 机制自动就会淘汰掉它。
这样在空间和发号数量之间取得了一个平衡,此处也应该看具体的业务需求来,是否会存在一对多的情况。比如下单未支付,给用户发短信召回,短信内的短 url 里面存在用户昵称,订单号等个性化信息,即不需要增加这一逻辑环节了。
短 URL 的存储
我们返回的短 URL 一般是将数字转换成 62 进制,这样子可以更加有效的缩短 URL 长度,那么 62 进制的数字对计算机来说只是字符串,怎么存储呢?直接存储字符串对等值查找好找,对范围查找等太不友好了.
其实可以直接存储 10 进制的数字,这样不仅占用空间少,对查找的支持较好,同时还可以更加方便的转换到更多 / 更少的进制来进一步缩短 URL.
短码安全问题
按照算法从 0-61 都是 1 位字符,然后 2 位、3 位… 这样的话很容易被人发现规律并进行攻击,当然防御手段很多,请求签名之类的安全验证手段不在本文讨论范围内。
首先计数器可以从一个比较大的随机中间值开始,比如从 10000 开始计数,他的 62 进制是 2Bi 3 位的字符串;
然后采用一些校验位算法 (比如 Luhn 改进一下),计算出 1 位校验位拼接起来,4 位短码,这样可以排除一定的安全风险;
再加点安全料的话,可以在 62 进制的转换过程中把排序好的 62 个字母数字随机打乱,比如 ABCD1234 打乱成 1BC43A2D, 转换的 62 进制也就更难 hack 了;
最后如果仍不放心,还可以在某些位置(比如 1,3,5)插入随机数,让人无法看出规律来也可以达到良好的效果。
高并发和分布式
高并发
如果直接存储在 MySQL 中,当并发请求增大,对数据库的压力太大,可能会造成瓶颈,这时候是可以有一些优化的.
1. 缓存
上面保证长短链接一一对应中也提到过缓存,这里我们是为了加快程序处理速度.
可以将热门的长链接 (需要对长链接进来的次数进行计数), 最近的长链接 (可以使用 redis 保存最近一个小时的) 等等进行一个缓存,保存在内存中或者类似 redis 的内存数据库中,如果请求的长 URL 命中了缓存,那么直接获取对应的短 URL 进行返回,不需要再进行生成操作.
2. 批量发号
每一次发号都需要访问一次 MySQL 来获取当前的最大号码,并且在获取之后更新最大号码,这个压力是比较大的.
我们可以每次从数据库获取 10000 个号码,然后在内存中进行发放,当剩余的号码不足 1000 时,重新向 MySQL 请求下 10000 个号码。在上一批号码发放完了之后,批量进行写入.
这样可以将对数据库持续的操作移到代码中进行,并且异步进行获取和写入操作,保证服务的持续高并发.
分布式
上面设计的系统是有单点的,那就是发号器是个单点,容易挂掉.
可以采用分布式服务,分布式的话,如果每一个发号器进行发号之后都需要同步给其他发号器,那未必也太麻烦了.
换一种思路,可以有两个发号器,一个发单号,一个发双号,发号之后不再是递增 1, 而是递增 2.
类比可得,我们可以用 1000 个服务,分别发放 0-999 尾号的数字,每次发号之后递增 1000. 这样做很简单,服务互相之间基本都不用通信,做好自己的事情就好了.