简易短链接项目

简介

前面已经预告(MurmurHash算法初探)过了,现在也是完成了,但是确实时没时间写,只能抽空“填坑”了🥱

关于这次从打算做到做完,应该是执行力最强的一次,仅仅不到一周时间就完成了,要是在过去,估计还要拖个几周甚至个把月。

参考:

高性能短链设计

BloomFilter布隆过滤器使用

说明

关于这篇高性能短链设计(下面提到的文章都是这个),写的非常好,非常建议有兴趣的认认真真看完。看完了这篇文章,解答了我对现在出现频次越来越高的短链接的很多疑惑。

本次做的简易短链接项目就是基于文章中说的哈希算法做的,使用的便是MurmurHash算法,并使用布隆过滤器优化。

流程如下(图片来源于上面的文章)

短链流程图

要用到的工具:

计算布隆过滤器大小

在线进制转换

数据库设计

1
2
3
4
5
6
7
CREATE TABLE `short_url_map` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`lurl` varchar(160) DEFAULT NULL COMMENT '长地址',
`surl` varchar(10) DEFAULT NULL COMMENT '短地址',
`gmt_create` int(11) DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

文章中数据库是这样设计的,可以看到其中主键类型为 unsigned int,即无符号数,这样的话就可以与MurmurHash算法的32位对应上了,但是Java不支持无符号数,如果使用int/Integer类型就有可能会出现溢出情况(虽然可能性很小,因为毕竟至少要有\(2^{32}\)条数据,也就是要有\(2^{32}\)种链接,很难的啦),为了避免有可能发生的问题,我的数据库设计如下

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `url_map` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`surl` varchar(10) NOT NULL COMMENT '短链接',
`lurl` varchar(160) NOT NULL COMMENT '长链接',
`valid` int(1) NOT NULL DEFAULT '0' COMMENT '是否有效',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_surl` (`surl`) USING BTREE COMMENT '短链接唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

id改为了bigint,加上了我习惯使用的三字段(validcreate_timeupdate_time),还有短链接的唯一索引。

环境搭建

主要的依赖还是这个,不仅有必要的MurmurHash算法,还有布隆过滤器可直接使用(重点说明!!!这种布隆过滤器的使用是无效的,因为毕竟是内存布隆过滤器(重启即无效了),并不适用分布式场景,所以这样使用不怎么严谨)

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>

其他的依赖我就不粘了。为了加快开发,使用了Mybatis-Plus-Generator快速生成代码,所以工作量就减少很多了,剩下的只是MurMurHash算法、BloomFilter、62进制转换这些了。

代码

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* <p>
* 前端控制器
* </p>
*
* @author wnhyang
* @since 2022-08-22 02:52:47
*/
@Controller
@RequestMapping("/urlMap")
@Slf4j
public class UrlMapController {

private static final String REDIRECT = "redirect:";

@Autowired
private UrlMapService urlMapService;

@GetMapping("/{sUrl}")
public String redirect(@PathVariable("sUrl") String sUrl, HttpServletResponse response) {
log.info("重定向{}", sUrl);
return REDIRECT + urlMapService.redirect(sUrl);
}

@PostMapping
@ResponseBody
public R<String> urlMap(@RequestBody Url url) {
log.info("生成短链接{}", url);
return urlMapService.urlMap(url.getUrl());
}
}

一共两个接口,一个生成短链接,一个短链接重定向。

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* <p>
* 服务实现类
* </p>
*
* @author wnhyang
* @since 2022-08-22 02:52:47
*/
@Service
@Slf4j
public class UrlMapServiceImpl extends ServiceImpl<UrlMapMapper, UrlMap> implements UrlMapService {

@Autowired
private UrlMapMapper urlMapMapper;

private static final HashFunction HASH_FUNCTION = Hashing.murmur3_32();

private static final BloomFilter<String> BLOOM_FILTER = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), Encoder62.SIZE);

private static final String STR = "ZRCTGD";

@Override
public String redirect(String sUrl) {
UrlMap urlMap = urlMapMapper.selectOne(new QueryWrapper<UrlMap>().eq("surl", sUrl));
return urlMap.getLurl().replaceAll(STR, "");
}

@Override
@Transactional(rollbackFor = Exception.class)
public R<String> urlMap(String url) {
String sUrl = "";
while (true) {
log.info("url:{}", url);
// 1、MurmurHash加密
HashCode hashCode = HASH_FUNCTION.hashString(url, StandardCharsets.UTF_8);

// 2、短链
try {
sUrl = Encoder62.encode62(hashCode.padToLong());
} catch (Exception e) {
e.printStackTrace();
}
log.info("sUrl:{}", sUrl);

//3、布隆过滤器
boolean contain = BLOOM_FILTER.mightContain(sUrl);

UrlMap urlMap = new UrlMap();
urlMap.setSurl(sUrl);
urlMap.setLurl(url);
if (contain) {
url += STR;
log.info("contain,new url:{}", url);
} else {
urlMapMapper.insert(urlMap);
BLOOM_FILTER.put(sUrl);
log.info("not contain,put({})", sUrl);
break;
}
}
return R.ok(sUrl);
}
}

这里就是接口的业务实现

  • 重定向比较简单,通过短链接查询长链接,移除可能存在的自定义字符串(理论上有可能会误删,关键在于自定义字符串定义)。

  • 生成短链接流程如上图,对应代码看即可

62进制转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @author: wnhyang
* @create: 2022-08-22 15:47
**/
public class Encoder62 {

/**
* BloomFilter预期插入次数
* 1L << 16 << 16
*/
public static final long SIZE = 1L << 16;

public static final int SCALE = 62;

private static final char[] ARRAYS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();

public static String encode62(long num) {
// if (num > SIZE) {
// throw new RuntimeException("num:" + num + "过大");
// }
StringBuilder sb = new StringBuilder();

int remain = 0;
while (num > 0) {
remain = (int) (num % SCALE);
sb.append(ARRAYS[remain]);
num /= SCALE;
}

return sb.reverse().toString();
}
}

这里说明一下,BloomFilter理论上应该设置\(2^{32}\),但是通过计算可知

布隆过滤器大小计算

需要3.6G内存,我第一次这样设置好,直接堆溢出,太可笑了😂

所以我最后设置的是\(2^{16}\),还可以接受

测试

就两个接口,简单测试一下就好了,还是比较顺利的

总结

关于生成短链接,除了Hash算法还有自增序列算法等方法,文章中还给出了“高性能短链的架构设计”,我还不是太懂,可以后续回看一下😯