高性能短链构建
短链必须性
这里列举3个原因来讲一下短链存在的必要性。
相对安全
短链不容易暴露访问参数,生成方式可以完全迎合短信平台的规则,能够有效地规避关键词、域名屏蔽等风险,而原始 URL地址,很可能因为包含特殊字符被短信系统误判,导致链接无法跳转。
平台限制
短信发送平台有字数限制,在整条短信字数不变的前提下,把链接缩短,其他部分的文字描述就能增加,这样似乎更能达到该短信的实际目的(比如,营销)。
短链构成
短链的组成通常包含两个部分:域名 + 随机码
https://www.ownsyou.com/s2TYdWd
短链的域名最好和其他业务域名分开,而且要尽量简短,可以不具备业务含义(比如: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
publicclass RedirectController {
@GetMapping("/{shortCode}")
public RedirectView redirect(@PathVariable String shortCode) {
String destUrl = "https://yuanjava.com";
// destUrl = getDestUrlByShortCode(shortCode); //真实的业务逻辑
returnnew RedirectView(destUrl);
}
接着,在浏览器访问短链”http://127.0.0.1:8080/s2TYdWd” 后,请求会被重定向到 https://yuanjava.com
从ws抓包可以看到 302状态码并且请求被 Location到另外一个 URL,整个交互流程图如下:
用户通过短链访问到短链系统,然后通过302重定向到目标系统
最后,总结下短链跳转的核心思想:
- 生成随机码,将随机码和目标 URL(长链)的映射关系存入数据库;
- 用域名+随机码生成短链,并推送给目标用户;
- 当用户点击短链后,请求会先到达短链系统,短链系统根据随机码查找出对应的目标 URL,接着将请求 302重定向到目标 URL(长链);
关于重定向有 301 和 302两种,如何选择?
- 302,代表临时重定向:每次请求短链,请求都会先到达短链系统,然后重定向到目标 URL(长链),这样,方便短链系统做一些统计点击数等操作;通常采用 302
- 301,代表永久重定向:第一次请求拿到目标长链接后,下次再次请求短链,请求不会到达短链系统,而是直接跳转到浏览器缓存的目标 URL(长链),短链系统只能统计到第一次访问的数据;一般不采用 301。
短链生成
从短链组成章节可知:短链=域名+随机码,因此,如何生成短链的问题变成了如何生成一个全局唯一的随机码,通常会有 3种做法:
Base62
Base62 表示法是一种基数为62的数制系统,包含26个英文大写字母(A-Z),26个英文小写字母(a-z)和10个数字(0-9)。这样,共有62个字符可以用来表示数值。如下代码:
import java.security.SecureRandom;
publicclass RandomCodeGenerator {
privatestaticfinal String CHAR_62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
privatestaticfinal 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万亿多),足够使用。
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;
publicclass MurmurHashToBase62 {
privatestaticfinal 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);
}
}
全局唯一ID
很多大中型公司都会有自己全局唯一 ID 的生成服务器,可以使用这些服务器生成的 ID来保证全局唯一,也可以使用雪花算法生成全局唯一的ID,再经过 62/64进制压缩。
冲突解决
对于上述 3种方法的前 2种:Base62 或者 Hash算法,因为本质上都是哈希函数,所以,不可避免地会产生哈希冲突,尽管冲突的概率已经很低很低了,so,万一冲突了,该如何解决呢?
要解决冲突,首先需要检测 Hash冲突,通常来说有 2种检测方法。
数据库锁
这里以 MySQL数据库为例,表结构如下:
CREATE TABLE`short_url_map` (
`id`int(11) unsignedNOTNULL AUTO_INCREMENT,
`long_url`varchar(160) DEFAULTNULLCOMMENT'长链',
`short_url`varchar(10) DEFAULTNULLCOMMENT'短链',
`gmt_create`int(11) DEFAULTNULLCOMMENT'创建时间',
PRIMARY KEY (`id`),
UNIQUEINDEX'short_url' ('short_url')
) ENGINE=InnoDBDEFAULTCHARSET=utf8;
首先创建一张长链和短链的关系映射表,然后通过给 short_url字段添加唯一锁,这样,当数据插入时,如果存在 Hash冲突(short_url值相等),数据库就会抛错,插入失败,因此,可以在业务代码里捕获对应的错误,这样就能检测出冲突。
也可以先用 short_url去查询,如果能查到数据,说明 short_url存在 Hash冲突了。
对于这种通过查询数据库或者依赖于数据库唯一锁的机制,因为都涉及DB操作,对数据库是一个额外的开销,如果流量比较大的话,需要保证数据库的性能。
布隆过滤器
在 DB操作的上游增加一个布隆过滤器,在长链生成短链后, 先用短链在布隆过滤器中进行查找,如果存在就代表冲突了,如果不存在,说明 DB里不存在此短链,可以插入。对于布隆过滤器的选择,单机可以采用 Google的布隆过滤器,分布式可以使用 RedisBloom。
检测出了冲突,需要如何解决冲突?
再 Hash,可以在长链后面拼接一个 UUID之类的随机字符串,然后再次进行 Hash,用得出的新值再进行上述检测,这样 Hash冲突的概率又大大大降低了。
高并发场景的架构
在流量不大的情况,上述方法怎么折腾似乎都合理,但是,为了架构的健壮性,很多时候需要考虑高并发,大流量的场景,因此架构需要支持水平扩展,比如:
- 采用微服务
- 功能模块分离,比如,短链生成服务和长链查询服务分离
- 功能模块需要支持水平扩容,比如:短链生成服务和长链查询服务能支持动态扩容
- 缓解数据库压力,比如,分区,分库分表,主从,读写分离等机制
- 服务的限流,自保机制
- 完善的监控和预警机制