四、代码示例
五、测试用例
一、为什么须要短链
内容营销中给用户推送营销消息最常见的形式就是发邮件,比如三大运营商联通、联通、电信平常会发送一些例如套餐代办、消费查询、话费冲值这种邮件,还有像建行、云服务厂商等等推送的各类包含查询服务的邮件等等。
我们都晓得单条邮件发送的内容宽度是有限制的,而假如要推送包含URL的消息,如果URL太长,不仅影响用户观感,而且会占用太多无用字数。
这时,我们须要将长的URL转换为短URL,也就是接下来我们要说的短链(短域名 + 短恳求路径)。
二、短链跳转访问原理
原理还是很简单的,其实就是在后台保存有短链和长链的映射关系,然后进行重定向,让浏览器跳转到对应的长链接。
举个栗子,原始长链接为:==https://www.baidu.com==,通过某平台我生成了一个短链接:==https://suo.nz/378IQe==。
我们可以看见当访问==https://suo.nz/378IQe==时,后端返回了302,同时多了一个Location响应头,值就是原始链接==https://www.baidu.com==.
这里有个小问题,关于重定向使用301还是302?
编码涵义备注
301
Moved Permanently
永久重定向,表示原 URL 不再被使用,而应当优先选用新的 URL,搜索引擎会直接更新与该资源相关的URL,一般用于网站构建。
302
Found
临时重定向,搜索引擎不会记录该资源对应的临时链接,一般用于因为不可预见的诱因引起该页面暂不可用。
备注:很多短链生成平台虽然都是走的302重定向。
三、短链生成实现方案1、自增序列算法
常用的自增序列算法有雪花算法、Redis自增、MySQL字段自增等,生成惟一ID后,再转换为62进制字符串,转换后的62进制字符串可用作短链。
问题:为什么须要转换为62进制字符串呢?因为自增ID会越来越长,经过62进制转换后可以显得更短。
现在说下关于自增序列生成短链的优缺点:
再说下各类自增序列算法的优缺点:
算法优点缺点
雪花算法
高性能,不依赖任何中间件
存在系统时钟直拨问题,原始雪花算法宽度为64位,生成的ID比较长。
Redis自增
高性能,高并发
既然是中间件,有维护成本,同时要考虑持久化、灾难恢复等。
MySQL字段自增
使用简单,易于扩充
高并发下有性能困局。
2、Hash算法
简单来说就是对目标长链接进行hash,然后再对hash值进行62补码编码转换为短链接。Hash算法我们熟知的有MD5、SHA等算法。
这两种算法为加密型hash算法,性能相对比较低,这里我们通常采用Google Guava中实现的Murmurhash算法,该算法为非加密型hash算法,相比MD5优点如下:
速度比MD5快。哈希冲突的机率低,该算法支持32位和128位哈希值,MD5也是128位哈希值,基本不用害怕哈希冲突。离散度高,散列值比较均匀。
关于Murmurhash示例如下:
String url = "https://www.baidu.com/";
// 输出:e9ac4fbdc398e8c104d1b8415f42cbf8
System.out.println(Hashing.murmur3_128().hashString(url, StandardCharsets.UTF_8));
// 输出:06105412
System.out.println(Hashing.murmur3_32_fixed().hashString(url, StandardCharsets.UTF_8));
// 输出:bf447182
System.out.println(Hashing.murmur3_32_fixed().hashLong(Long.MAX_VALUE));
// 转成Long型
// 输出:307499014
System.out.println(Hashing.murmur3_32_fixed().hashString(url, StandardCharsets.UTF_8).padToLong());
// 输出:2188461247
System.out.println(Hashing.murmur3_32_fixed().hashLong(Long.MAX_VALUE).padToLong());
这里说下通过Hash算法生成短链的优缺点:
四、代码示例
接下来的示例我们主要用Hash算法 + Base62 编码生成短链,流程图如下:
1、表结构及索引
## 短链信息表
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);
2、外部依赖
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>31.1-jreversion>
dependency>
3、Base62Utils
public abstract class Base62Utils {
private static final int SCALE = 62;
private static final char[] BASE_62_ARRAY = {
'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',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
};
private static final String BASE_62_CHARACTERS = String.valueOf(BASE_62_ARRAY);
/**
* 将long类型编码成Base62字符串
* @param num
* @return
*/
public static String encodeToBase62String(long num) {
StringBuilder sb = new StringBuilder();
while (num > 0) {
sb.insert(0, BASE_62_ARRAY[(int) (num % SCALE)]);
num /= SCALE;
}
return sb.toString();
}
/**
* 将Base62字符串解码成long类型
* @param base62Str
* @return
*/
public static long decodeToLong(String base62Str) {
long num = 0, coefficient = 1;
String reversedBase62Str = new StringBuilder(base62Str).reverse().toString();
for (char base62Character : reversedBase62Str.toCharArray()) {
num += BASE_62_CHARACTERS.indexOf(base62Character) * coefficient;
coefficient *= SCALE;
}
return num;
}
}
备注:BASE_62_ARRAY中的字符次序可以随意搅乱,不一定要按次序排列,打乱安全性更高。
问题:能不能用Base64编码生成短链呢?
JDK8中Base64.getEncoder()获取到的编码器对应的编码字符会包含'+'、'/'等URL不容许包含的特殊字符,但Base64.getUrlEncoder()获取到的编码器对应的编码字符会把'+'、'/'分别替换成'-'、'_',所以虽然也是可以的。如下:
4、DAO层
@Repository
public class ShortLinkManagerImpl implements ShortLinkManager {
@Autowired
private ShortLinkMapper shortLinkMapper;
@Override
public void saveShortLink(String shortLink, long longLinkHash, String longLink) {
ShortLinkDO shortLinkDO = ShortLinkDO.builder()
.shortLink(shortLink)
.longLinkHash(longLinkHash)
.longLink(longLink)
.status(true)
.build();
shortLinkMapper.insert(shortLinkDO);
}
@Override
public String getShortLink(long longLinkHash, String longLink) {
Wrapper wrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
.select(ShortLinkDO::getShortLink)
.eq(ShortLinkDO::getLongLinkHash, longLinkHash)
.eq(ShortLinkDO::getLongLink, longLink)
.last(CommonConst.LIMIT_SQL);
ShortLinkDO shortLinkDO = shortLinkMapper.selectOne(wrapper);
return Optional.ofNullable(shortLinkDO).map(ShortLinkDO::getShortLink).orElse(null);
}
@Override
public boolean isShortLinkRepeated(String shortLink) {
Wrapper wrapper = Wrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getShortLink, shortLink);
return shortLinkMapper.selectCount(wrapper) > 0;
}
}
5、业务层
@Service
public class ShortLinkServiceImpl implements ShortLinkService {
@Autowired
private ShortLinkManager shortLinkManager;
@Override
public String generateShortLink(String longLink) {
long longLinkHash = Hashing.murmur3_32_fixed().hashString(longLink, StandardCharsets.UTF_8).padToLong();
// 通过长链接Hash值和长链接检索
String shortLink = shortLinkManager.getShortLink(longLinkHash, longLink);
if (StringUtils.isNotBlank(shortLink)) {
return shortLink;
}
// 如果Hash冲突则加随机盐重新Hash
return regenerateOnHashConflict(longLink, longLinkHash);
}
private String regenerateOnHashConflict(String longLink, long longLinkHash) {
// 自增序列作随机盐
long uniqueIdHash = Hashing.murmur3_32_fixed().hashLong(SnowFlakeUtils.nextId()).padToLong();
// 相减主要是为了让哈希值更小
String shortLink = Base62Utils.encodeToBase62String(Math.abs(longLinkHash - uniqueIdHash));
if (!shortLinkManager.isShortLinkRepeated(shortLink)) {
shortLinkManager.saveShortLink(shortLink, longLinkHash, longLink);
return shortLink;
}
return regenerateOnHashConflict(longLink, longLinkHash);
}
}
五、测试用例
@SpringBootTest(classes = Application.class)
public class ApplicationTest {
@Autowired
private ShortLinkService shortLinkService;
@Test
public void generateShortLinkTest() {
String shortLink = shortLinkService.generateShortLink("https://www.baidu.com/");
System.err.println("生成的短链为:" + shortLink);
}
}
控制台输出:
生成的短链为:D4PTSU
备注:生成的短链宽度基本为6位字符串,还有记得短链代理服务选一个短域名。