前言 系统设计专栏我一般都会按照下面的三大块进行分析,对于下面的第三大块,如果系统体量适中,我一般会给自己3-4个月的时间来做这件事🙈,并贴上github链接,由社区的小伙伴来进行指正补充,如果觉得自己做不了或者系统涉及面太广,希望社区有小伙伴可以一起交流,安排好分工,大家一起看看这个系统该如何设计~
提前声明: 以下的分析思路均来自Designing a URL Shortening service like TinyURL,附带一些我自己的理解,如果对原文感兴趣的伙伴可以直接阅读原文
为什么需要短网址系统 短网址系统主要用于将原来的长链接重命名为较短的链接,这样不管是在展示、发送消息上都会节省很多的空间,例如下面的这个链接
https://www.google.com.hk/search?q=
%E6%AF%94%E8%BE%83%E9%95%BF%E7%9A%84%E9%93%BE%E6%8E%A5+%26+%E6%AF%94%E8%BE%83%E7%9F%A
D%E7%9A%84%E8%BF%9E%E6%8E%A5&newwindow=1&ei=q2vZYa3sNt6F4t4Pj7GAkAw&ved=0ahUKEwjt7b6Z_a
H1AhXegtgFHY8YAMIQ4dUDCA4&uact=5&oq=%E6%AF%94%E8%BE%83%E9%95%BF%E7%9A%84%E9%93%BE%E6%8E
%A5+%26+%E6%AF%94%E8%BE%83%E7%9F%AD%E7%9A%84%E8%BF%9E%E6%8E%A5&gs_lcp=Cgdnd3Mtd2l6EAM6B
QghEKABOgkIIRAKEKABECo6BwghEAoQoAFKBAhBGAFKBAhGGABQuBFY3jBgiDpoAnAAeAGAAZ0CiAHHJZIBBjAu
MjAuNpgBAKABAcABAQ&sclient=gws-wiz
最终转化为的短链为:
https://tinyurl.com/bdzftrfb
短网址系统的设计过程
需求分析
对于某个问题的需求分析,这里以功能需求和非功能需求进行划分,所谓的功能需求就是系统必须需要具备的,而非功能需求可以进行实现,但不是必须的
1. 功能需求
- 用户可以自定义自己的短链
- 用户可以给生成的短链增加超时时间
- 用户可以通过我们的网站实现将长链转变为短链
- 用户通过浏览器访问短链,可以访问到源站的地址
2. 非功能需求
- 系统应该是高可用的
- 生成短链的性能要好
- 生成的短链是不可预测的
- 对于生成短链的源站域名进行统计分析
体量预估
对于系统的吞吐量、存储耗费空间进行提前的预估,以便于之后的技术选型做出更加明智的决定
注: 以下的计算有近似的成分
-
QPS
该系统是一个读多写少的系统,我们这里假设每个月有新的URL产生(是不是有点大了😏),那么按照读写的比例计算,每个月要有的读请求,这样写请求的QPS为,那么读请求就为
-
存储
如果我们按照每月新的URL产生来计算,年下来,可以产生的URL总数为,如果按照每个URL为,那么最终需要的存储大概为
-
带宽
对于写请求来说,如果每秒写个URL,每个URL的大小为,那么对于写请求的带宽为,读请求是写请求的倍,对应的带宽为
-
内存估计
如果我们想要去缓存某些频繁访问的URL,这里我们遵循二八原则(20%的URL可以解决80%的事务),我们可以去缓存20%的"热"URL,按照上面的体量继续分析,我们每秒会读20000条URL,那么每天就会读,我们缓存20%的URL,也就大约要,当然可能会存在许多重复的URL请求,所以真实需要的缓存大小要比小
系统API设计
一旦我们明确了需求,可以尝试先定义下系统的API,也就是系统功能的明确说明
// createURL 新建短链接
// uuid => 注册用户的唯一标识,可以被用来限定用户的一些举动,egs: 生成短链的个数
// originalUrl => 原始url
// customAlias => [可选]用户自定义字符串,最终生成的短链为短链域名 + customAlias
// userName => [可选]用户名用户编码
// expireDate => [可选]短链的有效时间
func createURL(uuid string, originalUrl string, customAlias string, userName string, expire_date int64) string
// deleteURL 删除短链接
// uuid => 同上
// urlKey => 标识短链的字符串
func deleteURL(uuid string, urlKey string)
数据库选型
我们的数据库存在以下情况:
- 我们需要存储大量的数据
- 每一条记录是很小的
- 记录之间存在的关系很少
- 我们的服务读请求很重
Schema设计
url_key
| 字段名 | 类型 |
|---|---|
| id | int(10) |
| url_key | varchar(16) |
| state | int(10) 1表示可用,2表示不可用 |
url_transfer
| 字段名 | 类型 |
|---|---|
| url_key_id | int(10) |
| original_url | varchar(1024) |
| uuid | varchar(256) |
| create_date | datetime |
| expire_date | datetime |
user
| 字段名 | 类型 |
|---|---|
| uuid | int(10) |
| name | varchar(256) |
| varchar(256) | |
| create_date | datetime |
| last_login | datetime |
表结构设计出来后,我们需要思考下究竟是使用关系型数据库还是非关系性型数据库进行存储,由于这块儿需要存储海量的数据,且我们不需要维护各个实体间的关系,所以我们考虑可以使用非关系型的数据库来进行存储
实现细节
1. urlKey的生成
这里我们需要解决的重点问题是如何生成一个短的并且唯一的Key针对每个URL,在上面的例子中tinyurl生成的短链为https://tinyurl.com/bdzftrfb,我们需要讨论的就是bdzftrfb的生成规则
这里我们探讨两种实现方式(ps: 笔者在这个比较中学到了一些东西,希望正在读文章的你也可以有一些收获~)
-
编码原始的URL
这种方式偏向于实时产生,也就是对于用户提供的原始URL,我们使用某种算法来产生特定长度的字符串(urlKey)来对原始的URL进行唯一的标识
例如说我们可以使用MD5算法先对原始的URL进行加密,得到一个128位的值,之后使用base64进行编码,base64编码是对每6位进行编码,最终我们可以得到一个长度大约为21的一个字符串序列,而我们真正需要的是前6位/前8位
上面描述的方法会引发以下几个问题:
-
如果每次都是截取长度为21的字符串序列的前6或8个字符,会不会产生重复
对于这个问题,可以通过交换字符直到唯一或其它的方式规避
-
如果是对原始的URL进行编码,那么对于不同的用户,相同的原始URL,最终的结果是一样的
对于这个问题,我们可以在编码的过程中添加上面说的用户的唯一标识uuid来解决,当然对于未注册的用户,也就是用户并没有uuid,这种情况还是不能得到很好的解决,只能强制用户去注册账户,对于用户的体验不是很好
-
-
离线生成唯一标识
既然在线产生URL的唯一标识会产生一系列的问题,我们思考下是不是可以将这个唯一标识(urlKey)的产生线下化,也就是我们可以单独出一个服务,这个服务用于产生url的唯一标识
用户在编码原始URL的时候,会有大量的并发请求打到唯一标识的产生服务上,为了应对并发条件下的数据一致性,我们可以在取出某个urlKey之后就将它软删除(该操作保证是原子的),保证该urlKey不会被重复使用
我们继续探讨该服务的一些其它属性,例如当前服务需要使用的存储大小,假设我们生成urlKey的长度为,如果我们使用base64进行编码,每一个位置的字符有64种可能,那么最终产生的urlKey的个数为,按照每个字符占个Byte,最终需要的存储为,为了方便服务之后的横向扩展,这里最好对数据进行分区处理,在保证数据的均匀分布时可以对我们要存储的对象进行hash,使用得到的hash值取模分区的数量,来决定数据最终的存储位置
说到这里,不清楚读者有没有种恍然大悟的感觉,当在线的urlKey产生需要耗费大量精力考虑边界的情况下,使用这种离线的方式确实可以减轻实现的难度,且易横向扩展
2. 性能优化
这里有两个点可以进行优化,提升系统的吞吐
-
客户端从产生URL的唯一标识的服务中获取urlKey时可以一次性获取一批urlKey,并缓存到本地,当然这些urlKey对应的记录也会被标记为不可用,这样,之后客户端再获取urlKey的时候优先会从本地获取,没有的话才会请求urlKey的生成服务
-
服务本身就是一个读多写少的场景,对于经常访问的url,我们可以对它们进行缓存,每次客户端输入一个短链之后,我们优先从缓存中获取原始的url,如果没有,则从数据库中进行获取,之后再更新缓存
当然因为短链接是有过期时间的,所以缓存中也是需要存储过期时间的,当某个请求命中缓存,发现当前链接已经失效,则返回404,并删除改缓存,同时删除数据库中的记录,并释放当前的urlKey
动手实现自己的短网址系统