看上去业务简单,其实,覆盖的知识点非常多:
总体来说,高并发、高性能系统的核心领域,都覆盖了。所以,分析下来,得到一个结论:是一个超级好的问题。
短网址替代长URL,在互联网网上传播和引用。例如QQ微博的url.cn,新郎的sinaurl.cn等。在QQ、微博上发布网址的时候,会自动判别网址,并将其转换,例如:url.cn/2hytQx
为什么要这样做的?
无外乎几点:
短URL系统的核心:将长的 URL 转化成短的 URL。
客户端在访问系统时,短URL的工作流程如下:
如下图所示:
那么,原始URL如何变短呢?简单来说, 可以将原始的地址,使用编号进行替代。
编号如何进一步变短呢? 可以使用更大的进制来表示。
顾名思义短网址就是非常短的网址,比如xxx.cn/EYyCO9T,其中核… EYyCO9T 只有7位长度。
其实这里的7位长度是使用62进制来表示的,就是常用的0-9、a-z、A-Z,也就是10个数字+26个小写+26个大写=62位。
那么7位长度62进制可以表示多大范围呢?
62^7 = 3,521,614,606,208 (合计3.5万亿),
说明:
10进制 最大只能生成 10 ^ 6 - 1 =999999个
16进制 最大只能生成 16 ^ 6 - 1 =16777215个
16进制里面已经包含了 A B C D E F 这几个字母
62进制 最大竟能生成 62 ^ 6 - 1 =56800235583个 基本上够了。
A-Z a-z 0-9 刚好等于62位
注意:
int(4个字节) ,存储的范围是-21亿到21亿
long(8个字节),存储的范围是-900万万亿 到 900万万亿
至于短网址的长度,可以根据自己需要来调整,如果需要更多,可以增加位数,即使6位长度62^6也能达到568亿的范围,这样的话只要算法得当,可以覆盖很大的数据范围。
在编码的过程中,可以按照自己的需求来调整62进制各位代表的含义。
一个典型的场景是, 在编码的过程中,如果不想让人明确知道转换前是什么,可以进行弱加密,比如A站点将字母c表示32、B站点将字母c表示60,就相当于密码本了。
标准ASCII 码也叫基础ASCII码,使用7 位二进制数(剩下的1位二进制为0),包含128个字符,
看到这里你或许会说,使用128进制(如果有的话)岂不是网址更短,
是的,
7 位二进制数(剩下的1位二进制为0)表示所有的大写和小写字母,数字0 到9、标点符号,以及在美式英语中使用的特殊控制字符 [1] 。
注意:
128个进制就可能会出现大量的不常用字符
比如 # % & * 这些,
这样的话,对于短链接而言,通用性和记忆性就变差了,
所以,62进制是个权衡折中。
用户使用62进制的短地址请求服务:
回顾一下发号器的功能:
以下对目前流行的分布式ID方案做简单介绍
可以通过 原始Url的 hash编码,得到一个 整数,作为 短链的ID
哈希算法简单来说就是将一个元素映射成另一个元素,
哈希算法可以简单分类两类,
MD5算法
MD5消息摘要算法(MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),MD5算法将数据(如一段文字)运算变为另一固定长度值,是散列算法的基础原理。
由美国密码学家 Ronald Linn Rivest设计,于1992年公开并在 RFC 1321 中被加以规范。
CRC算法
循环冗余校验(Cyclic Redundancy Check)是一种根据网络数据包或电脑文件等数据,产生简短固定位数校验码的一种散列函数,由 W. Wesley Peterson 于1961年发表。
生成的数字在传输或者存储之前计算出来并且附加到数据后面,然后接收方进行检验确定数据是否发生变化。
由于本函数易于用二进制的电脑硬件使用、容易进行数学分析并且尤其善于检测传输通道干扰引起的错误,因此获得广泛应用。
MurmurHash
MurmurHash 是一种非加密型哈希函数,适用于一般的哈希检索操作。
由 Austin Appleby 在2008年发明,并出现了多个变种,与其它流行的哈希函数相比,对于规律性较强的键,MurmurHash的随机分布特征表现更良好。
这个算法已经被很多开源项目使用,比如libstdc++ (4.6版)、Perl、nginx (不早于1.0.1版)、Rubinius、 libmemcached、maatkit、Hadoop、Redis,Memcached,Cassandra,HBase,Lucene等。
MurmurHash 计算可以是 128位、64位、32位,位数越多,碰撞概率越少。
所以,可以把长链做 MurmurHash 计算,可以得到的一个整数哈希值 ,
所得到的短链,类似于下面的形式
固定短链域名+哈希值 = www.weibo.com/888888888
如何缩短域名?传输的时候,可以把 MurmurHash之后的数字为10进制,可以把数字转成62进制
www.weibo.com/abcdef
snowflake ID原理是使用Long类型(64位),按照一定的规则进行分段填充:时间(毫秒级)+集群ID+机器ID+序列号,每段占用的位数可以根据实际需要分配,其中集群ID和机器ID这两部分,在实际应用场景中要依赖外部参数配置或数据库记录。
总结一下,snowflake ID 的优缺点和使用场景:
这里,不用地址的hash 编码作为ID
这里,不用数据库的自增长ID
这里,不用redis、mongdb的分布式ID
最终,这里,从发号性能、整体有序(B+树索引结构更加友好)的角度出发,最终选择的snowflake算法
snowflake算法的吞吐量在 100W ops +
但是 snowflake算法 问题是啥呢?需要解决时钟回拨的问题。
如何解决时钟回拨的问题,可以参考 推特官方的 代码、 百度ID的代码、Shardingjdbc ID的源码,综合存储方案设计解决。
这个数据,非常的结构化,可以使用结构化数据库MYSQL存储。
结构非常简单,我们会有二列:
1. ID,int, // 分布式雪花id;
2. SURL,varchar, // 原始URL;
接下来,开始高并发、海量数据场景,需要进行 MYSQL存储 的分库分表架构。
提示一下,这里可以说说自己的分库分表 操作经验,操作案例。
然后进行 互动式作答。
也就是,首先是进行 输入条件 询问,并且进行确认。
然后按照分治模式,进行两大维度的分析架构:
所谓的地址二义性,就行同一个长址多次请求得到的短址不一样。
在生产地址的时候,需要进行二义性检查,防止每次都会重新为该长址生成一个短址,一个个长址多次请求得到的短址是不一样。
通过二义性检查,实现长短链接真正意义上的一对一。
怎么进行 二义性检查?
最简单,最为粗暴的方案是:直接去数据库中检查。
但是,这就需要付出很大的性能代价。
要知道:数据库主键不是 原始url,而是 短链url 。
如果根据 原始url 去进行存在性检查,还需要额外建立索引。
问题的关键是,数据库性能特低,没有办法支撑超高并发 二义性检查。
所以,这里肯定不能每次用数据库去检查。
这里很多同学可能会想到另一种方案,就是 redis 的布隆过滤, 把已经生成过了的 原始url,大致的方案是,可以把已经生成过的 原始url ,在 redis 布隆过滤器中进行记录。
布隆过滤器就是bitset+多次hash的架构,宏观上是空间换时间,不对所有的 surl (原始url)进行内容存储,只对surl进行存在性存储,这样就节省大家大量的内存空间。
在数据量比较大的情况下,既满足时间要求,又满足空间的要求。
布隆过滤器的巨大用处就是,能够迅速判断一个元素是否在一个集合中。
布隆过滤器的常用使用场景如下:
Bloom Filter 专门用来解决我们上面所说的去重问题的,使用 Bloom Filter 不会像使用缓存那么浪费空间。
当然,他也存在一个小小问题,就是不太精确。
Bloom Filter 相当于是一个不太精确的 set 集合,我们可以利用它里边的 contains 方法去判断某一个对象是否存在,但是需要注意,这个判断不是特别精确。
一般来说,通过 contains 判断某个值不存在,那就一定不存在,但是判断某个值存在的话,则他可能不存在。
但是, redis bloom filter误判的概率很低,合理优化之后,也就在1%以下。
可能有小伙伴说,如果100Wqps,1%也是10W1ps,DB还是扛不住,怎么办?
具体来说,可以使用 Redis 缓存进行 热门url的缓存,实现部分地址的一对一缓存。
比如将最近/最热门的对应关系存储在K-V数据库中,比如在本地缓存 Caffeine中存储最近生成的长对短的对应关系,并采用过期机制实现 LRU 淘汰,从而保证频繁使用的 URL 的总是对应同一个短址的,但是不保证不频繁使用的URL的对应关系,从而大大减少了空间上的消耗。
这里,主要是介绍自己对 多级缓存的 掌握和了解。
可以使用了缓存,二级缓存、三级缓存,加快id 到 surl的转换。
将热门的长链接(需要对长链接进来的次数进行计数)、最近的长链接(可以使用 Redis 保存最近一个小时的数据)等等进行一个缓存,如果请求的长URL命中了缓存,那么直接获取对应的短URL进行返回,不需要再进行生成操作。
301永久重定向和 302 临时重定向。
使用 301 虽然可以减少服务器的压力,但是无法在 server 层获取到短网址的访问次数了,如果链接刚好是某个活动的链接,就无法分析此活动的效果以及用于大数据分析了。
而 302 虽然会增加服务器压力,但便于在 server 层统计访问数,所以如果对这些数据有需求,可以采用 302,因为这点代价是值得的,但是具体采用哪种跳转方式,还是要结合实际情况进行选型。
短链生成的具体逻辑可以分为以下几个关键步骤:
在生成短链之前,需要对输入的长链接进行合法性校验,确保其不是恶意链接或无效链接。
使用非加密型哈希算法(如 MurmurHash3)对长链接进行哈希计算,得到一个固定长度的哈希值。哈希算法的选择需要考虑性能和分布均匀性。
将哈希值转换为62进制字符串(字符集通常是 [0-9][A-Z][a-z]
),作为短链的标识。这种转换可以有效缩短短链的长度。
由于哈希算法可能存在冲突,需要处理这种情况。常见的方法包括:
将短链与长链的映射关系存储在数据库中,同时可以使用缓存(如 Redis)来提升查询性能。
当用户访问短链时,服务端根据短链标识查询对应的长链接,并使用HTTP 302临时重定向到长链接。同时,可以记录访问信息(如IP、时间、来源等)用于监控和统计。
在重定向过程中,可以收集短链的访问信息,如PV/UV、来源、浏览器类型等,并进行数据可视化分析。