Hi,你好,我是猿java
如下图,对于这种客评短信,相信大家并不陌生,通过点击短信里“蓝色字体”跳转到一个具体的网页。
其实,上图中那串蓝色字符,有个美丽的专业术语:短链。它可以是一个 URL地址,也可以是一个二维码。而整个跳转流程的背后是一套完整的短链系统,因此,今天我们来一起分析:如何设计一套高性能短链系统。
为什么需要短链?
存在即合理,这里列举 3个主要原因。
1. 相对安全
短链不容易暴露访问参数,生成方式可以完全迎合短信平台的规则,能够有效地规避关键词、域名屏蔽等风险,而原始 URL地址,很可能因为包含特殊字符被短信系统误判,导致链接无法跳转。
2. 美观
对于精简的文字,似乎更符合美学观念,不太让人产生反感。
3. 平台限制
短信发送平台有字数限制,在整条短信字数不变的前提下,把链接缩短,其他部分的文字描述就能增加,这样似乎更能达到该短信的实际目的(比如,营销)。
短链的组成
如下图,短链的组成通常包含两个部分:域名 + 随机码
短链的域名最好和其他业务域名分开,而且要尽量简短,可以不具备业务含义(比如:xyz.com),因为短链大部分是用于营销,可能会被三方平台屏蔽。
短链的随机码需要全局唯一,建议 10位以下。
短链的跳转原理
首先,我们先看一个短链跳转的简单例子,如下代码,定义了一个 302重定向的代码示例:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.servlet.view.RedirectView;
@Controller
public class RedirectController {
@GetMapping("/{shortCode}")
public RedirectView redirect(@PathVariable String shortCode) {
String destUrl = "https://yuanjava.com";
// destUrl = getDestUrlByShortCode(shortCode); //真实的业务逻辑
return new RedirectView(destUrl);
}
接着,在浏览器访问短链”http://127.0.0.1:8080/s2TYdWd” 后,请求会被重定向到 https://yuanjava.com ,下图为浏览器控制台信息:
从上图,我们看到了 302状态码并且请求被 Location到另外一个 URL,整个交互流程图如下:
是不是有一种偷梁换柱的感觉???
最后,总结下短链跳转的核心思想:
-
生成随机码,将随机码和目标 URL(长链)的映射关系存入数据库;
-
用域名+随机码生成短链,并推送给目标用户;
-
当用户点击短链后,请求会先到达短链系统,短链系统根据随机码查找出对应的目标 URL,接着将请求 302重定向到目标 URL(长链);
关于重定向有 301 和 302两种,如何选择?
-
302,代表临时重定向:每次请求短链,请求都会先到达短链系统,然后重定向到目标 URL(长链),这样,方便短链系统做一些统计点击数等操作;通常采用 302
-
301,代表永久重定向:第一次请求拿到目标长链接后,下次再次请求短链,请求不会到达短链系统,而是直接跳转到浏览器缓存的目标 URL(长链),短链系统只能统计到第一次访问的数据;一般不采用 301。
如何生成短链?
从短链组成章节可知:短链=域名+随机码,因此,如何生成短链的问题变成了如何生成一个全局唯一的随机码,通常会有 3种做法:
1. Base62
Base62 表示法是一种基数为62的数制系统,包含26个英文大写字母(A-Z),26个英文小写字母(a-z)和10个数字(0-9)。这样,共有62个字符可以用来表示数值。如下代码:
import java.security.SecureRandom;
public class RandomCodeGenerator {
private static final String CHAR_62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final SecureRandom random = new SecureRandom();
public static String generateRandomCode(int length) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int rndCharAt = random.nextInt(CHAR_62.length());
char rndChar = CHAR_62.charAt(rndCharAt);
sb.append(rndChar);
}
return sb.toString();
}
}
对于 Base62算法,如果是生成 6位随机数有 62^6 – 1 = 56800235583(568亿多),如果是生成 7位随机数有 62^7 – 1 = 3521614606208(3.5万亿多),足够使用。
2. Hash算法
Hash算法是我们最容易想到的办法,比如 MD5, SHA-1, SHA-256, MurmurHash, 但是这种算法生成的 Hash值还是比较长,常用的做法是把这个 Hash值进行 62/64进行压缩。
如下代码,通过 Google的 MurmurHash算法把长链 Hash成一个 32位的 10进制正数,然后再转换成62进制(压缩),这样就可以得到一个 6位随机数,
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import java.nio.charset.StandardCharsets;
public class MurmurHashToBase62 {
private static final String BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static String toBase62(int value) {
StringBuilder sb = new StringBuilder();
while (value > 0) {
sb.insert(0, BASE62.charAt(value % 62));
value /= 62;
}
return sb.toString();
}
public static void main(String[] args) {
// 长链
String input = "https://yuanjava.cnposts/short-link-system/design?code=xsd&page=1";
// 长链利用 MurmurHash算法生成 32位 10进制数
HashFunction hashFunction = Hashing.murmur3_32();
int hash = hashFunction.hashString(input, StandardCharsets.UTF_8).asInt();
if (hash < 0) {
hash = hash & 0x7fffffff; // Convert to positive by dropping the sign bit
}
// 将 32位 10进制数 转换成 62进制
String base62Hash = toBase62(hash);
System.out.println("base62Hash:" + base62Hash);
}
}
3. 全局唯一 ID
比如,很多大中型公司都会有自己全局唯一 ID 的生成服务器,可以使用这些服务器生成的 ID来保证全局唯一,也可以使用雪花算法生成全局唯一的ID,再经过 62/64进制压缩。
如何解决冲突?
对于上述 3种方法的前 2种:Base62 或者 Hash算法,因为本质上都是哈希函数,所以,不可避免地会产生哈希冲突,尽管冲突的概率已经很低很低了,so,万一冲突了,该如何解决呢?
要解决冲突,首先需要检测 Hash冲突,通常来说有 2种检测方法。
数据库锁
这里以 MySQL数据库为例,表结构如下:
CREATE TABLE `short_url_map` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`long_url` varchar(160) DEFAULT NULL COMMENT '长链',
`short_url` varchar(10) DEFAULT NULL COMMENT '短链',
`gmt_create` int(11) DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE INDEX 'short_url' ('short_url')
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
首先创建一张长链和短链的关系映射表,然后通过给 short_url字段添加唯一锁,这样,当数据插入时,如果存在 Hash冲突(short_url值相等),数据库就会抛错,插入失败,因此,可以在业务代码里捕获对应的错误,这样就能检测出冲突。
也可以先用 short_url去查询,如果能查到数据,说明 short_url存在 Hash冲突了。
对于这种通过查询数据库或者依赖于数据库唯一锁的机制,因为都涉及DB操作,对数据库是一个额外的开销,如果流量比较大的话,需要保证数据库的性能。
布隆过滤器
在 DB操作的上游增加一个布隆过滤器,在长链生成短链后, 先用短链在布隆过滤器中进行查找,如果存在就代表冲突了,如果不存在,说明 DB里不存在此短链,可以插入。对于布隆过滤器的选择,单机可以采用 Google的布隆过滤器,分布式可以使用 RedisBloom。
整体流程可以抽象成下图:
检测出了冲突,需要如何解决冲突?
再 Hash,可以在长链后面拼接一个 UUID之类的随机字符串,然后再次进行 Hash,用得出的新值再进行上述检测,这样 Hash冲突的概率又大大大降低了。
高并发场景的架构
在流量不大的情况,上述方法怎么折腾似乎都合理,但是,为了架构的健壮性,很多时候需要考虑高并发,大流量的场景,因此架构需要支持水平扩展,比如:
-
采用微服务
-
功能模块分离,比如,短链生成服务和长链查询服务分离
-
功能模块需要支持水平扩容,比如:短链生成服务和长链查询服务能支持动态扩容
-
缓解数据库压力,比如,分区,分库分表,主从,读写分离等机制
-
服务的限流,自保机制
-
完善的监控和预警机制
这里给出一套比较完整的设计思路图:
总结
本文通过一个客服评价的短信开始,分析了短链的构成,短链跳转的原理,同时也给出了业内的一些实现算法,以及一些架构上的建议。
对于业务体量小的公司,可以根据成本来搭建服务(单机或者少量服务器做负载),对于业务体量比较大的公司,更多需要考虑到高并发的场景,如何保证服务的稳定性,如何支持水平扩展,当服务出现问题时如何具备一套完善的监控和预警服务器。
其实,很多系统都是在一次又一次的业务流量挑战下成长起来的,我们需要不断打磨自己宏观看架构,微观看代码的能力,这样自己也就跟着业务,系统一起成长起来了。
最后,希望本文可以给你带来收获和思考,如果有任何疑问,欢迎评论区留言讨论。如果本文对你有帮助,请帮忙点赞或转发,想获取更多技术好文和面试经,请关注公众号
-THE End-
点击下方名片,阅读更多技术好文
原文始发于微信公众号(猿java):腾讯女后端设计了一套短链系统,当场就想给她offer!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/257135.html