背景
最近偶然看到运营在群里进行页面分享的时候链接很长,导致发出来之后直接被刷屏,这样体验肯定不好,当然也不利于推广,因此比较好的方式是生成一个短链接服务,能够把链接变短,所以自己写个工具进行优化,也调研了下常见的短链接服务设计。
方案对比
方案1: 可以通过摘要MD5运算,得到一个固定长度的值,然后持久化到数据库。当然在生成的时候都需要判断是否重复,哈希碰撞的话,也就是如果重复就重新进行运算,直到生成并写入到数据库。
优点:长度固定、直接调用库简单。
缺点:数据量大的时候可能碰撞比较频繁,导致生成效率比较低,当然可以根据业务考虑进行定期短链接的清理。
方案2:可以用一张表的字段自增来实现长链接的映射,当然为了进一步精简还可以把10进制优化成62进制,也就是[0-9],[a-z][A-Z]62个字符,只用4位即可表达62^4个链接,所以非常够用了。至于高并发网上一般的方案是做成分布式,但是在我看来并不用这么复杂,可以参考分布式id生成方式,不用每次都去请求DB拿到自增值,可以在每台应用机器内缓存一个区间,这样大大降低DB的负载,唯一缺陷是因为机器重启内存断电导致的中间序列可能断,但是这对于业务来说不重要。
核心逻辑代码实现:
public class ShortUrlGenerator {
/**
* 内存缓存的最大值
*/
private int maxId = 0;
/**
* 步长,每次DB获取的长度
*/
private int delta = 10;
/**
* 生成后DB的长短链接映射
*/
private Map<String, String> dbMap = new HashMap<>();
public static void main(String[] args) {
ShortUrlGenerator shortUrlGenerator = new ShortUrlGenerator();
for (int i = 0; i < 10000; i++) {
String s = "http://longLinkDomain/" + i;
String shortUrl = shortUrlGenerator.generate(s);
System.out.println("生成短链接:" + shortUrl + ", 获取到原始链接为:" + shortUrlGenerator.get(shortUrl));
}
}
// 301重定向长链接
private String get(String shortUrl) {
return dbMap.get(shortUrl);
}
private static final String DOMAIN = "http://micro/";
private Table table = new Table();
public String generate(String str) {
if (maxId % delta == 0) {
// 重启,或则内存的id区间消耗完了,请求DB获取区间
maxId = table.auto();
}
int id = maxId --;
// 0,9, a,z A,Z
// 转换为62进制的4位短链接
String s = parseInteger2Char(id, 62, (o) -> {
if (o <= 9) return (char) ('0' + o);
if (o >= 10 && o < 36) return (char) ('a' + (o - 10));
return (char) ('A' + (o - 36));
});
String ret = DOMAIN + s;
dbMap.put(ret, str);
return ret;
}
/**
*
* @param id 需要转换的id自增
* @param hex 转换的进制,我这里是用62
* @param mapperFunction 对应的进制需要用一个数字映射字符的逻辑,回调函数
* @return
*/
private String parseInteger2Char(Integer id, int hex, Function<Integer, Character> mapperFunction) {
/*
进制转换
*/
Stack<Integer> stack = new Stack<>();
while (true) {
int n = id % hex;
stack.push(n);
id /= hex;
if (id < hex) {
stack.push(id);
break;
}
}
/*
短链接字符映射
*/
StringBuffer sb = new StringBuffer();
while (! stack.isEmpty()) {
sb.append(mapperFunction.apply(stack.pop()));
}
return sb.toString();
}
}
/**
* 模拟DB自增获取操作
*/
class Table {
// 原子类,避免并发问题
private AtomicInteger id = new AtomicInteger(0);
// 此处统计DB请求次数
int count = 0;
public Integer auto() {
count ++;
System.out.println("DB请求次数 : " + count);
return id.getAndAdd(1000);
}
}
总结
网上还有一种是生成随机数,当然也会用哈希碰撞的问题,所以个人认为利用DB的自增算法相对复杂度和可靠性来说高一些, 其中批量缓存区间是一个好的思想,另一方面业务上的考虑也是很重要的,例如预期设计复杂的哈希碰撞解决方案可能从如何避免数据量增大导致碰撞概率更简单 。