短 URL 系统是怎么设计的?

作者:iammutex
链接:https://www.zhihu.com/question/29270034/answer/46446911

这个问题看到就想答。 个人相关:三年前在公司做过一个短地址服务,目前在线上跑。 而这个问题,也是我现在招聘面试题里面必考的一道,这一道题里面有很多可考的地方,能够相对综合的考察候选人的功力。

最烂的回答 实现一个算法,将长地址转成短地址。实现长和短一一对应。然后再实现它的逆运算,将短地址还能换算回长地址。 这个回答看起来挺完美的,然后候选人也会说现在时间比较短,如果给我时间我去找这个算法就解决问题了。但是稍微有点计算机或者信息论常识的人就能发现,这个算法就跟永动机一样,是永远不可能找到的。即使我们定义短地址是100位。那么它的变化是62的100次方。62=10数字+26大写字母+26小写字母。无论这个数多么大,他也不可能大过世界上可能存在的长地址。所以实现一一对应,本身就是不可能的。 再换一个说法来反驳,如果真有这么一个算法和逆运算,那么基本上现在的压缩软件都可以歇菜了,而世界上所有的信息,都可以压缩到100个字符。这~可能吗。

另一个很烂的回答 和上面一样,也找一个算法,把长地址转成短地址,但是不存在逆运算。我们需要把短对长的关系存到DB中,在通过短查长时,需要查DB。 怎么说呢,没有改变本质,如果真有这么一个算法,那必然是会出现碰撞的,也就是多个长地址转成了同一个短地址。因为我们无法预知会输入什么样的长地址到这个系统中,所以不可能实现这样一个绝对不碰撞的hash函数。

比较烂的回答 那我们用一个hash算法,我承认它会碰撞,碰撞后我再在后面加1,2,3不就行了。 ok,这样的话,当通过这个hash算法算出来之后,可能我们会需要做btree式的大于小于或者like查找到能知道现在应该在后面加1,2,或3,这个也可能由于输入的长地址集的不确定性。导致生成短地址时间的不确定性。同样烂的回答还有随机生成一个短地址,去查找是否用过,用过就再随机,如此往复,直到随机到一个没用过的短地址。

正确的原理 上面是几种典型的错误回答,下面咱们直接说正确的原理。 正确的原理就是通过发号策略,给每一个过来的长地址,发一个号即可,小型系统直接用mysql的自增索引就搞定了。如果是大型应用,可以考虑各种分布式key-value系统做发号器。不停的自增就行了。第一个使用这个服务的人得到的短地址是 第二个是 第11个是 第依次往后,相当于实现了一个62进制的自增字段即可。

几个子问题

1. 62进制如何用数据库或者KV存储来做? 其实我们并不需要在存储中用62进制,用10进制就好了。比如第10000个长地址,我们给它的短地址对应的编号是9999,我们通过存储自增拿到9999后,再做一个10进制到62进制的转换,转成62进制数即可。这个10~62进制转换,你完全都可以自己实现。

2. 如何保证同一个长地址,每次转出来都是一样的短地址 上面的发号原理中,是不判断长地址是否已经转过的。也就是说用拿着百度首页地址来转,我给一个 过一段时间你再来转,我还会给你一个 。这看起来挺不好的,但是不好在哪里呢?不好在不是一一对应,而一长对多短。这与我们完美主义的基因不符合,那么除此以外还有什么不对的地方? 有人说它浪费空间,这是对的。同一个长地址,产生多条短地址记录,这明显是浪费空间的。那么我们如何避免空间浪费,有人非常迅速的回答我,建立一个长对短的KV存储即可。嗯,听起来有理,但是。。。这个KV存储本身就是浪费大量空间。所以我们是在用空间换空间,而且貌似是在用大空间换小空间。真的划算吗?这个问题要考虑一下。当然,也不是没有办法解决,我们做不到真正的一一对应,那么打个折扣是不是可以搞定?这个问题的答案太多种,各有各招,我这就不说了。(由于实在太多人纠结这个问题,请见我最下方的更新

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

4. 具体存储如何选择 这个问题就不展开说了,各有各道,主要考察一下对存储的理解。对缓存原理的理解,和对市面上DB、Cache系统可用性,并发能力,一致性等方面的理解。

5. 跳转用301还是302 这也是一个有意思的话题。首先当然考察一个候选人对301和302的理解。浏览器缓存机制的理解。然后是考察他的业务经验。301是永久重定向,302是临时重定向。短地址一经生成就不会变化,所以用301是符合http语义的。同时对服务器压力也会有一定减少。 但是如果使用了301,我们就无法统计到短地址被点击的次数了。而这个点击次数是一个非常有意思的大数据分析数据源。能够分析出的东西非常非常多。所以选择302虽然会增加服务器压力,但是我想是一个更好的选择。

大概就是这样。

------五一假期后更新------- 就回答一点大家最纠结的问题吧,就是如何实现同一个长地址多次转换,出来还是同一个短地址。

我上面其实讲到了,这个方案最简单的是建立一个长对短的hashtable,这样相当于用空间来换空间,同时换取一个设计上的优雅(真正的一对一)。

实际情况是有很多性价比高的打折方案可以用,这个方案设计因人而异了。那我就说一下我的方案吧。

我的方案是:用key-value存储,保存“最近”生成的长对短的一个对应关系。注意是“最近”,也就是说,我并不保存全量的长对短的关系,而只保存最近的。比如采用一小时过期的机制来实现LRU淘汰。

这样的话,长转短的流程变成这样: 1 在这个“最近”表中查看一下,看长地址有没有对应的短地址 1.1 有就直接返回,并且将这个key-value对的过期时间再延长成一小时 1.2 如果没有,就通过发号器生成一个短地址,并且将这个“最近”表中,过期时间为1小时

所以当一个地址被频繁使用,那么它会一直在这个key-value表中,总能返回当初生成那个短地址,不会出现重复的问题。如果它使用并不频繁,那么长对短的key会过期,LRU机制自动就会淘汰掉它。

当然,这不能保证100%的同一个长地址一定能转出同一个短地址,比如你拿一个生僻的url,每间隔1小时来转一次,你会得到不同的短地址。但是这真的有关系吗?

 

 

刚好本人负责短链接。我简单说下。
既然是短链接,那么地址池是有限的。所以在业务上会要求接受短链接失效,保证可回收利用。但是总有一些场景要求长期有效,所以可预留一些特殊的。
以上是最基本的系统功能要求。
至于技术上怎么做,太简单了。DB索引,随机都可以,搞个分库分表,读多写少。随机的时候碰撞了就重试几次,没什么问题。在对总短链接数做个监控,地址用的太多会造成随机碰撞越来越多,也可能是回收机制出问题了。
另外防攻击做好。别个把你链接池地址耗光。
匿了,不想被发现


作者:匿名用户
链接:https://www.zhihu.com/question/29270034/answer/46528514
 
 

短网址服务设计

背景

短网址服务,用来将输入的一个长网址转换为一个短网址(比如附录中的案例),当用户请求这个短网址时,服务查询出真实的url; 
设计这么一个短网址服务,需要考虑哪些点?

数据结构

首先,需要考虑短网址应该如何存储,使用一个key-value结构就可以; 
key是生成的短网址,具有唯一性; 
value为原始真实网址;

算法

计算短网址的算法可以很简单,短网址与原始网址就只存在一个映射关系。 
从1开始递增来映射每一个网址; 
1个位上可以使用26位字母+10个数字,即36进制; 而如果也用上大写字母,就是62进制;
当然,在计算前需要通过value来查一遍,确定是否有重复键,如果有重复,直接返回; 
那通过value如何快速定位是否有重复?再使用一个STL set来解决判重是个方法,有没有更好的方式?

使用一个hash表或STL set保存所有的长url会消耗很大的空间;而如果不保存这个映射关系,用户针对同一个长url的多次请求都返回的是不同的短url,体验不好,也消耗短url资源;
好的做法:保存最近一段时间(比如6小时)的长url记录,这段时间内,对同一长url的转换,返回的是同一个短url;而过期之后再做转换,返回另一个新的URL;

 

确定key的长度和value的长度

value长度可以设置在500,一般的网址不会超过这个数; 
key: t.cn/** 
key的长度决定了能够支持多少个短网址; 
如果是5位长度,能够支持6000多万的网址,6位长度就是21亿;

数据容量

预估数据容量 
会占用多大的空间;对于这类服务,基于效率考虑,一般是全内存操作; 
如果单机能够装下,使用单机; 
如果单机无法装下,需要分片;分片策略可以根据key的递增范围来定,也可以根据取模来确定;

分片策略

根据key的递增范围分片

优点: 扩容简单,超过1个服务器的容量后就增加一台机器; 
缺点:负载可能不均衡;一般后生成的短网址访问比较频繁,造成装载早期短网址的服务器空闲;

根据key的取模来分片

优点:用户的负载比较均衡; 
缺点:难以扩容

取舍:可以先预估数据容量,确定使用的服务器数,使用第二种分片方法;当数据超出预估的容量,对于超出的key再使用第一种分片方法路由到新的服务器上(打补丁)

接口设计

确定用户传入的接口协议,用户的输入和输出

并发读写和数据存储

使用什么来存放这些key-value数据? 
貌似一个STL hash map容器就可以,但map不是线程安全的,考虑加锁? 
如果实时性要求不高,可以使用AB两块内存操作,一块内存线上读,一块线下写,定期更新; 
由于用户输入了长的网址之后,需要在终端上能够显示出被转换的短网址,所有对写的实时性也是有要求的; 
要求实时,针对map可能得用上锁,或者直接使用第三方内存产品,如redis,memcache等; 
对redis的读写使用异步进一步提高并发效率;

网络

对于用户请求量,如果是千兆网能够满足,使用一个单线程事件循环来处理;(IO non-blocking + io multiplexing) 
如果用户请求更大,使用多个Reactor事件循环来处理,接入的reactor只负责事件的监听,连接建立后,将用户请求的处理转到后续的计算reactor中处理; 
查询和更新逻辑简单,可以直接在IO事件循环中处理(类似ngnix架构) 
如果更新逻辑复杂,考虑后台增加额外的进程/线程池,处理异步写操作;

安全

(可选)考虑有恶意用户,构造不存在的网址来连续触发请求,以此来占满短网址的id; 
可对网址进行合法性校验(直接访问那个网址太耗时间,不太显示) 
对同一来源用户限制请求数;

案例

http://t.im/ 这个短网址生成器上使用的就是36进制递增来做的: 
例如,多次输入不同的长网址,得到的短网址: 
http://t.im/vgu8 
http://t.im/vgu9 
http://t.im/vgu0 
http://t.im/vgua 
从这也可看出这个网站的并发并不大,我这几次请求都是相隔几秒的; 
这个网站也没有做特殊的网址校验规则,比如输入a.bb.ccc之类的网址,都为合法;

 

后记

以上是自己的一些想看,看过网上的一些文章后,发现有不少改进的地方:

1. 短url的存储

设计时使用的是字母和数字的组合,使用36进制或62进制是为了让url尽可能的短;在后台存储的时候,使用整型更为合适,
整型比较比字符串比较要高效,像redis等第三方产品对整型的查找都有专门的优化;后台整型存储,返回给用户时,进行10进制到36进制的转换即可;

 

2. 分布式发号器
自增的发号器是单点。如果流量大了,涉及到拆分,分成多个服务器来处理;发号器同样可以扩容到多个,扩到2台,分别发单双号;第一台发单号,第二天发双号,不会重复;而扩容到10台,则分别发0~9尾号的号;

 

Posted by: 大CC | 06NOV,2015 

posted @ 2018-03-10 16:46  dion至君  阅读(236)  评论(0)    收藏  举报