如何设计一个短网址服务

·  阅读 1230

一、简介

不知道大家有没有了解过短网址,所谓短网址就是将一个长的url转换成一个短的url,这样用户访问短url的时候也能够正常访问到正常的url。应用广泛,比如短信、分享链接等等。。。

下面我会从几个方面来讲述如何设计一个高性能的短网址服务

干货满满,你准备好了吗🐒

二、正文

你为什么能通过一个短的链接访问的正常的数据呢

这里假设短网址为mtw.so/6kK03S 我们称之为A
对应的长链接为tech.meituan.com/2021/10/20/… 我们称之为B
(这里是通过一个在线网址转换工具生成的,图省事😂)

当我们访问A时,会经过mtw.so的域名解析最终会请求该域名下的某一个http接口(😎后段程序员都懂,就不做多解释了
这个接口会拿到url后的参数 6kK03S
通过这个参数后端会定位到一个长链接,也就是原始链接,最后通过重定向到原始链接(301/302按需使用),至此就结束了

如何将一个长链接变成一个短链接呢🤨

错误的做法 ❌

你首先肯定会想到加密啊,把长的url通过一种算法加密,压缩长度,用户访问加密后的url,通过后段再解密就好了呀。ohhhh!! 我是个天才,这也太简单了吧!!(如果真的有这样子的算法,那么你就创造奇迹了。。。原因自行百度,不做多解释了)

那么应该怎么做呢? 😱
如果我们能吧长链接和短链接的对应关系存储起来,用户访问短链接时,去数据库中把长链接读取出来,不就可以完美解决了吗?

如何通过短链接定位到长链接

那么问题来了,这个存储要怎么设计?如何通过短链接定位到长链接
使用id,如果我们有一个全局id,这个id永远不会重复,是不是就解决问题了?
当一个长链接来申请变成短链接时候,我们为他生成一个id,然后存到数据库中,假设id=996,那么996这个id就对应我们这个长链接,最后我们直接把这个996提供给用户使用就好了,假设生成的短链接baidu.com/996 ,那么接下来的流程就简单了

  1. 用户访问baidu.com/996
  2. 服务器接受到请求,获得996这个参数
  3. 后端服务那996这个id,去数据库中查询长链接
  4. 重定向到这个长链接
  5. 用户看到长链接的页面效果

完美解决问题!!!
你认为这样解决了吗?既然这么简单,那这篇文的意义何在❓

image.png
问题一:全局id怎么生成?
问题二:如果id还是太长了怎么办?
问题三:如果我这个短网址服务一天要生成十万、百万、千万级别的短网址,要如何存储?
问题四:请求量大了怎办?服务扛得住吗?
(面试官的致命组合拳)
莫慌莫慌,下面我们一一道来😴(以下问题解决方案基于分布式环境探讨)

全局id怎么生成

常见的全局id生成有几种,对于高并发情况,我们一般会涉及一个id生成器,后文会详解

redis incr ⭐️⭐️⭐️⭐️

完全依赖于redis,
优点:简单无脑
缺点:数据会丢失,即便是redis有RDB和AOF,也会丢失不能保证

数据库自增id ⭐️⭐️⭐️⭐️

基于数据库的自增id
优点:简单无脑
缺点:要做好并发控制,比如sql层面可以通过select for update,代码层面可以使ReentrantLock来保证线程安全,还要注意id是有上限的(具体上限跟随数据类型),如果达到上限,讲不会再生成id

uuid (不推荐)⭐️

优点:简单无脑
缺点:生成的id不连续切中英文混合,落地到mysql会造成页分裂,影响mysql存储性能

雪花算法(SnowFlake) ⭐️⭐️⭐️⭐️⭐️

优点:通过单例模式保证id唯一性
缺点:由于是基于时间的,所以时间回退会造成id重复 (谁没事会去改服务器的时间???)

如果自增id很长了怎么办

当我们的自增id长度很长,生成了8573834749584939,也不满足我们短链接的需求怎么办?

进制转化
我们可以使用62进制(数字 + 小写字母 + 大写字母)来处理我们的id

10进制62进制
996g4
10241024102410244GNTCX7B6
996996996996996996996j9TiP3ZLxcIA

是不是变短了很多(你那么短,却那么能干🤔)

数据量大怎么存储

数据量大?多大算大呢?个、十、百、千、万、十万、爸、爷、祖宗!!!!

根据《阿里巴巴Java开发手册》,单表超过500W行,或者单表数据大小超过2GB,才推荐做分表操作
如何选择分表策略?
这里给出两张常见的分表策略方案:

  1. 根据日期按月/季度/年分表
  2. 固定表分表

按时间份表

适用场景:日增数据量很大,比如日增十万、百万级别的数据
具体实现:根据时间,把当前时间生成的数据存储到具体的时间表中,比如今天是2021-10-28,选择按月份表,我们就把10月份产生的数据都存到t_id_test_202110,这张表中
优点:可以利用mysql的事件自动创建表,且单表容量可控
缺点:id生成需要带上时间,不方便做统计操作

固定表分表

固定表分表指的就是自己评估未来的数据增长,把数据表分为若干个表,比如分为8、16、32、64、128张表(这里建议是2的x次方

适用场景:能预估数据量的大小,并且不希望创建很多表
具体实现:创建若干数据表8、16、32、64、128张表,以序号命名,比如:t_id_test_1、t_id_test_43等,当生成一个id(996996),我们将这个id取模分表总数
这里也是为什么建议分表数一定要是2的x次方的原因,因为当n=2的x次方,任意数%n 结果和 任意数&n-1 的结果是一致的,但是位运算的效率要高得多得多得多
比如分32张表,996996%32、996996&31=4,那么我们就把数据写到t_id_test_4 这张表中,读取同理
优点:id不用额外处理,分表数量可控,统计方便
缺点:表扩容比较复杂,需要暂停服务,重新做一次hash计算

如何扛住高并发,1W+的QPS

对于高并发情况,我们要如何应对?我们先总结一下短链接的过程

  1. 生成短链接
    a. 获取一个全局id
    b. 把长链接和id存到数据库
    c. 将id做62进制转换
    d. 拼接成短链接返回
  2. 访问短链接
    a. 后段接收请求
    b. 拿到请求参数
    c. 将参数取10进制
    d. 从数据库中讲这个id对应的长链接取出来
    e. 重定向

我们来思考一下,对于高并发场景,我们要考虑以上哪一过程呢?🤔🤔我们一步一步来
先看 1 生成端链接的过程
1.b、1.c 和1.d 是不需要考虑的,因为你并发量再大,他们也不会影响,就是一个计算+返回+写db的过程
1.a有没有并发问题?当然有,什么问题?I/O瓶颈呀,如果选择redis、mysql自增作为全局id,那么I/O是重大损耗,那么如何解决呢?可以利用id生成器

再来看 2 访问短链接的过程
2.a、2.b、2.c、2.e 是不会有高并发的问题的,我们重点看2.d
我们知道,高并发环境下影响性能最严重的就是I/O,而且大批量的请求直接打到mysql,如果mysql的配置不高,也容易搞快mysql,那么如何解决呢?可以利用缓存

全局id生成器

所谓id生成器,就是我负责生(I/O),你负责用(从内存拿),职责单一,减小I/O!
具体实现:一条线程不停的生成id,保证唯一性和顺序性,然后将生成的id放入队列中(先进先出、保证顺序),工作线程要获取id时直接从队列头拿出来即可,这样是不就除去了工作线程I/O的过程,直接读内存,速度大大的提高。当然我们也可以设置一个规则,比如当队列长度小于某一个界限的时候再生成id。

缓存

我们要从两个地方坐缓存,第一数据过滤,第二数据缓存
数据过滤:
为什么要有数据过滤?因为我们要先行过滤无效的数据,如果这个数据是有效的我们才会往后面走流程,常用的数据过滤器可以使用布隆过滤器(Bloom Filter),生成短链接时,讲当时的id放入BloomFilter,后面有请求来先通过一层Filter,如果为true,往下执行,如果是false,直接拒绝 (BloomFilter会有误差,如果存在则不一定存在,如果不存在则一定不存在)

数据缓存:
当每生成一个短链接,我们可以先将其缓存至redis,读取时先读redis,redis没有再去mysql读,然后再放入redis,防止大批量的请求直接打到mysql,将其击垮。这里要考虑一个问题,因为短链接的量非常大,我们要选择性长期缓存,对于一些热点数据,我们要缓存时间长一些,直到他成为冷门数据,对于冷门数据,我们甚至可以不用缓存。
如果还是并发大相应慢,我们可以利用本地缓存,也就是jvm层面的缓存,可以讲链接信息暂时放入本地map,使用lru算法保证不会OOM,先读本地缓存、再读redis、最后mysql兜底。

三、总结

本文主要探讨了一个高性能的短链接服务的设计思路,如有错误的地方,希望各位大佬帮助小弟改正。
新人小白第一文,可以关注我,后续会有很多干货哦~~~




一个努力让自己变强的深漂Java程序员

分类:
后端
标签:
分类:
后端
标签: