高性能短链构建
短链必须性
这里列举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冲突的概率又大大大降低了。
高并发场景的架构
在流量不大的情况,上述方法怎么折腾似乎都合理,但是,为了架构的健壮性,很多时候需要考虑高并发,大流量的场景,因此架构需要支持水平扩展,比如:
- 采用微服务
- 功能模块分离,比如,短链生成服务和长链查询服务分离
- 功能模块需要支持水平扩容,比如:短链生成服务和长链查询服务能支持动态扩容
- 缓解数据库压力,比如,分区,分库分表,主从,读写分离等机制
- 服务的限流,自保机制
- 完善的监控和预警机制
4 comments
2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
新车首发,新的一年,只带想赚米的人coinsrore.com
新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
新车上路,只带前10个人coinsrore.com
新盘首开 新盘首开 征召客户!!!coinsrore.com
新项目准备上线,寻找志同道合 的合作伙伴coinsrore.com
新车即将上线 真正的项目,期待你的参与coinsrore.com
新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com
2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
新车首发,新的一年,只带想赚米的人coinsrore.com
新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
新车上路,只带前10个人coinsrore.com
新盘首开 新盘首开 征召客户!!!coinsrore.com
新项目准备上线,寻找志同道合 的合作伙伴coinsrore.com
新车即将上线 真正的项目,期待你的参与coinsrore.com
新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com
新车即将上线 真正的项目,期待你的参与coinsrore.com
新车上路,只带前10个人