一、简介
不知道大家有没有了解过短网址,所谓短网址就是将一个长的url转换成一个短的url,这样用户访问短url的时候也能够正常访问到正常的url。应用广泛,比如短信、分享链接等等。。。
下面我会从几个方面来讲述如何设计一个高性能的短网址服务
干货满满,你准备好了吗
二、正文
你为什么能通过一个短的链接访问的正常的数据呢
这里假设短网址为http://mtw.so/6kk03s 我们称之为a
对应的长链接为https://tech.meituan.com/2021/10/20/the-applications-of-sentiment-analysis-meituan.html 我们称之为b
(这里是通过一个在线网址转换工具生成的,图省事 )
当我们访问a时,会经过mtw.so的域名解析最终会请求该域名下的某一个http接口( 后段程序员都懂,就不做多解释了)
这个接口会拿到url后的参数 6kk03s
通过这个参数后端会定位到一个长链接,也就是原始链接,最后通过重定向到原始链接(301/302按需使用),至此就结束了
如何将一个长链接变成一个短链接呢
错误的做法 ❌
你首先肯定会想到加密啊,把长的url通过一种算法加密,压缩长度,用户访问加密后的url,通过后段再解密就好了呀。ohhhh!! 我是个天才,这也太简单了吧!!(如果真的有这样子的算法,那么你就创造奇迹了。。。原因自行百度,不做多解释了)
那么应该怎么做呢?
如果我们能吧长链接和短链接的对应关系存储起来,用户访问短链接时,去数据库中把长链接读取出来,不就可以完美解决了吗?
如何通过短链接定位到长链接
那么问题来了,这个存储要怎么设计?如何通过短链接定位到长链接?
使用id,如果我们有一个全局id,这个id永远不会重复,是不是就解决问题了?
当一个长链接来申请变成短链接时候,我们为他生成一个id,然后存到数据库中,假设id=996,那么996这个id就对应我们这个长链接,最后我们直接把这个996提供给用户使用就好了,假设生成的短链接为https://baidu.com/996 ,那么接下来的流程就简单了
1. 用户访问https://baidu.com/996 2. 服务器接受到请求,获得996这个参数 3. 后端服务那996这个id,去数据库中查询长链接 4. 重定向到这个长链接 5. 用户看到长链接的页面效果 完美解决问题!!!
你认为这样解决了吗?既然这么简单,那这篇文的意义何在❓
问题一:全局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进制 | | --------------------- | ------------ | | 996 | g4 | | 1024102410241024 | 4gntcx7b6 | | 996996996996996996996 | j9tip3zlxcia |
是不是变短了很多(你那么短,却那么能干 )
数据量大怎么存储
数据量大?多大算大呢?个、十、百、千、万、十万、爸、爷、祖宗!!!!
根据《阿里巴巴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张表,9969962、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. 重定向
我们来思考一下,对于高并发场景,我们要考虑以上哪一过程呢? 我们一步一步来r/> 先看 1 生成端链接的过程
1.b、1.c 和1.d 是不需要考虑的,因为你并发量再大,他们也不会影响,就是一个计算 返回 写db的过程
1.a有没有并发问题?当然有,什么问题?i/o瓶颈呀,如果选择redis、mysql自增作为全局id,那么i/o是重大损耗,那么如何解决呢?可以利用id生成器
r/> 再来看 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兜底。
三、总结
本文主要探讨了一个高性能的短链接服务的设计思路,如有错误的地方,希望各位大佬帮助小弟改正。
新人小白第一文,可以关注我,后续会有很多干货哦~~~