短链接的场景:短链接现在屡见不鲜,用于做促销、引流和数据统计等等,在一些限制字数的场景也很方便,看起来也是简洁,便于输入。
原理
短链接的原理
下面以短链接:wsc.fit/FZ(B站短链接)为例
- 首先,在浏览器输入短链接,DNS先解析获得其IP地址;
- 通过短链接,发送HttpGet请求,通过短链接,获取到原始的长连接;、
- 请求通过重定向,跳转到长链接。302重定向,对服务器压力更大,但是优势在于,可以通过重定向进行流量统计。

短链接生成算法
知道了基本的原理,那短链接是通过什么生成的。短链接的组成分为:域名(区分Http和Https)+当前链接的短码。以bit.ly/2XyAbC为例,域名是bit.ly,短码是2XyAbC。一般来说,短码通过010,az,A~Z这六十二个数字和字母组成,四到八位短码,六位短码就有500多亿的组成方式,已经足够使用。常见的生成短码的方式有三种:
- 通过自增ID。
在查询上一次的ID,在ID的基础上的进行MAX+1,然后然后将这十进制的ID转变为六十二为进制的转化成一个62进制的字符串。这样生成短码的优势在于,不会出现冲突,劣势在于,生成的短码是有序且安全性差,并且初始值不设置好,短码长度不一。可以结合其他数据进行MD5加密。
- 通过随机数生成
将62位的数字和字母,随机排列组合作为短码,然后在数据库中查询是否已经存在。如果已经存在,则再次生成。优势就是逻辑简单,但是计算机生成的随机数是伪随机,如果需要生成大量短链接,可能出现重复需要再次生成。
- 摘要算法(推荐)
摘要算法就是,通过一个函数,把任意的数据长度的数据转换成长度固定的数据。比如说,MD5,SHA1,SHA512等等。摘要算法,不是用于加密算法,因为不能通过摘要反推明文,而是用于防篡改。
通过加盐操作,很可以有效的防止MD5值被碰撞出来。
实现方案
数据库存储方案
sql
# 短链表
create table `t_short_link`
(
`id` bigint primary key auto_increment comment '主键ID',
`short_link` varchar(32) not null default '' comment '短链接',
`long_link_hash` bigint not null default 0 comment 'hash值',
`long_link` varchar(128) not null default '' comment '长链接',
`status` tinyint not null default 1 comment '状态:1-可用,0-不可用',
`expiry_time` datetime null comment '过期时间',
`create_time` datetime not null default current_timestamp comment '创建时间'
) comment '短链表';
# 创建对应的索引
create index idx_sl_hash_long_link on t_short_link (long_link_hash, long_link);
create index idx_sl_short_link on t_short_link (short_link);
缓存方案
如果所有的请求直接打在数据库上,无疑会影响到数据库的性能。但是如果直接将所有的短链信息直接存储在缓存中,也不太现实,当然数据量小也是可以的。
如果数据量大,可以考虑根据LRU(最近最少使用页面置换算法),在缓存中存放热点数据,并且设置合适的过期时间,将缓存进行热更新。
长链接转短链接的工具类:
案例一:
arduino
import org.apache.commons.lang.StringUtils;
import com.google.common.hash.Hashing;
/**
* 短网址工具类
*
* @author Roc-xb
*
*/
public class urlUtil {
/**
* 10个数字+26个小写字母+26个大写字母
*/
private static String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/**
* 62进制
*/
private static int scale = 62;
/**
* 最小长度
*/
private static int minLength = 5;
/**
* 种子
*/
private static int seed = 0;
/**
* 长链接转短链接
*
* @param longUrl 长链接
* @return 短链接
*/
public static String shortUrl(String longUrl) {
// 用MurmurHash算法得出长链接的hash值
int hash = Hashing.murmur3_32(seed).hashBytes(longUrl.getBytes()).asInt();
return hex10To62(hash);
}
/**
* 10进制转62进制
*
* @param hash
* @return 短链接
*/
public static String hex10To62(long hash) {
// 解决hash值小于0时,会触发异常。
hash = Math.abs(hash);
StringBuilder sb = new StringBuilder();
int remainder;
while (hash > scale - 1) {
// 对 scale 进行求余,然后将余数追加至 sb 中,由于是从末位开始追加的,因此最后需要反转字符串
remainder = Long.valueOf(hash % scale).intValue();
sb.append(chars.charAt(remainder));
// 除以进制数,获取下一个末尾数
hash /= scale;
}
sb.append(chars.charAt(Long.valueOf(hash).intValue()));
String value = sb.reverse().toString();
// value长度小于minLength时,在左侧填充字符'0'
return StringUtils.leftPad(value, minLength, '0');
}
}
测试类:
typescript
@Test
void test(){
// 长链接
String longUrl = "https://www.zhangsan.com/community/test_service";
// 短链接
String shortUrl = urlUtil.shortUrl(longUrl);
System.out.println(String.format("长链接:%s", longUrl));
System.out.println(String.format("短链接:https://www.zhangsan.com/%s", shortUrl));
}
测试结果:

案例二
工具类:
arduino
import org.apache.commons.codec.digest.DigestUtils;
import java.util.HashMap;
public class UrlUtil {
public static HashMap<String, String> urls = new HashMap<>();
static String[] chars = new String[]{
"0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b",
"c", "d", "e", "f", "g", "h",
"i", "j", "k", "l", "m", "n",
"o", "p", "q", "r", "s", "t",
"u", "v", "w", "x", "y", "z",
"A", "B", "C", "D", "E", "F",
"G", "H", "I", "J", "K", "L",
"M", "N", "O", "P", "Q", "R",
"S", "T", "U", "V", "W", "X",
"Y", "Z"};
public static String getChar(int index) {
return chars[index];
}
public static String[] toShortAndSave(String str, String key) {
// 拼接后的 url
String realUrl = key + str;
// md5 长度为 32 位
String md5 = DigestUtils.md5Hex(realUrl);
// md5 会被拆分为 4 部分 , 每部分均被转译成一种处理结果
String[] shortUrl = new String[4];
// 对 md5 的4个部分分别处理
for (int beginIndex = 0; beginIndex < 4; beginIndex++) {
// 预定义该部分的处理结果
String out = "";
// 字符串截取的两个坐标
int endIndex = beginIndex + 1;
// 裁剪 8 位
String strPart = md5.substring(beginIndex * 8, endIndex * 8);
// 与0x3FFFFFFF 进行位运算 获得30位长度的数字编码
long index = Long.valueOf("3FFFFFFF", 16) & Long.valueOf(strPart, 16);
// 将30位的数字编码拆分成6个部分 每次处理5位
for (int i = 0; i < 6; i++) {
// 与 0x0000003D 与运算,相当于与运算后5位,获得对应的字符下标
int idx = (int) (Long.valueOf("0000003D", 16) & index);
// 获得下标对应字符
out += getChar(idx);
// 数字右移5位 继续处理
index = index >> 5;
}
// out 即为处理后的一种short方案,可以存储到数据库等存储介质中,这里为了方便展示直接存储到 hashMap 里
urls.put(out, str);
// 一共返回4种方案 分别赋值
shortUrl[beginIndex] = out;
}
return shortUrl;
}
public static String[] head(String[] shortUrls) {
String[] res = new String[4];
int idx = 0;
for (String e : shortUrls) {
res[idx++] = "http://t.cn/" + e;
}
return res;
}
}
测试类:
ini
@Test
void test1(){
String url = "https://blog/zhangsan";
String key = "test";
System.out.println("目标url:" + url);
System.out.println("临时key:" + key);
String[] res = UrlUtil.toShortAndSave(url, key);
System.out.println("短网址处理结果:");
for (String e : res) {
System.out.println(e);
}
String[] head = head(res);
System.out.println("短URL展示:");
for (String e : head) {
System.out.println(e);
}
System.out.println("网址映射:");
for (String e : res) {
System.out.println("短网址 : " + "https://blog/"+e + " -> 实际网址 : " + urls.get(e));
}
}
结果:
