短链接跳转
# 短链接原理
通过压缩算法生成一个短链接;
访问短链接实际访问的是短链接服务器,查询数据库找到对应的长连接;
302 重定向跳转;
为什么302临时重定向?
如果是301永久重定向,每次访问的是缓存,相当于只访问了一次,不能对用户行为分析;如果做监控系统必须采用临时重定向;
# 短链接跳转
# 路由表
需要一个路由表,请求参数中只有shortUri,需要根据 full_short_url找到 gid;
接口:
@GetMapping("/{short-uri}")
public void restoreUrl(@PathVariable("short-uri") String shortUri, ServletRequest request, ServletResponse response) {
shortLinkService.restoreUrl(shortUri, request, response);
}
1
2
3
4
2
3
4
@SneakyThrows
@Override
public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
String serverName = request.getServerName();
String fullShortUrl = serverName + "/" + shortUri;
LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
.eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
if(shortLinkGotoDO==null){
return ;
}
LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
.eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
.eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
.eq(ShortLinkDO::getDelFlag, 0)
.eq(ShortLinkDO::getEnableStatus, 0);
ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
if (shortLinkDO != null) {
((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
修改短链接创建接口,创建短链接时需要再创建路由表;
@Override
public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
String shortLinkSuffix = generateSuffix(requestParam);
String fullShortUrl = StrBuilder.create(requestParam.getDomain())
.append("/")
.append(shortLinkSuffix)
.toString();
ShortLinkDO shortLinkDO = ShortLinkDO.builder()
.domain(requestParam.getDomain())
.originUrl(requestParam.getOriginUrl())
.gid(requestParam.getGid())
.createdType(requestParam.getCreatedType())
.validDateType(requestParam.getValidDateType())
.validDate(requestParam.getValidDate())
.describe(requestParam.getDescribe())
.shortUri(shortLinkSuffix)
.enableStatus(0)
.fullShortUrl(fullShortUrl)
.build();
// 新增
ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
.fullShortUrl(fullShortUrl)
.gid(requestParam.getGid())
.build();
try {
baseMapper.insert(shortLinkDO);
shortLinkGotoMapper.insert(linkGotoDO);
} catch (DuplicateKeyException ex) {
// 两个请求生成了同一个短链接
LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
.eq(ShortLinkDO::getFullShortUrl, fullShortUrl);
ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
if (hasShortLinkDO != null) {
log.warn("短链接:{} 重复入库", fullShortUrl);
throw new ServiceException("短链接生成重复");
}
}
shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
return ShortLinkCreateRespDTO.builder()
.fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
.originUrl(requestParam.getOriginUrl())
.gid(requestParam.getGid())
.build();
}
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
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
添加本地host:127.0.0.1 nurl.ink
测试:nurl.ink:8001/1IGNBh
# 缓存击穿
key过期,大量请求查询数据库;
缓存过期,只需要一个线程去查询数据库,再更新缓存;后续的线程拿到锁之后,发现缓存不为空,直接走跳转业务;
@SneakyThrows
@Override
public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
String serverName = request.getServerName();
String fullShortUrl = serverName + "/" + shortUri;
String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl));
if(StrUtil.isNotBlank(originalLink)){
((HttpServletResponse) response).sendRedirect(originalLink);
return;
}
RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
lock.lock();
try {
originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
// 双重检验锁,防止后续拿到锁的请求再去查询数据库
if (StrUtil.isNotBlank(originalLink)) {
((HttpServletResponse) response).sendRedirect(originalLink);
return;
}
LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
.eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
if (shortLinkGotoDO == null) {
return;
}
LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
.eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
.eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
.eq(ShortLinkDO::getDelFlag, 0)
.eq(ShortLinkDO::getEnableStatus, 0);
ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
if (shortLinkDO != null) {
stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl());
((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
}
} finally {
lock.unlock();
}
}
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
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
# 缓存穿透
流程:
布隆过滤器:
位数组+hash;
判断一个元素是否在一个集合中,使用多个哈希函数计算处对应的哈希值,检查位数组中的位置是否都为1,如果任意一个位置为0,就说明元素不存在于集合中;
布隆过滤器删除:不能删除的原因,假设位数组长度为10,如果删除一个元素那么数组长度则会变为9,就会导致已经存储的数据经过哈希之后得到的下标错误;
不存在布隆过滤器,就肯定不存在于数据库;
@SneakyThrows
@Override
public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
String serverName = request.getServerName();
String fullShortUrl = serverName + "/" + shortUri;
String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl));
if(StrUtil.isNotBlank(originalLink)){
((HttpServletResponse) response).sendRedirect(originalLink);
return;
}
// 解决缓存穿透
boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
if (!contains) {
return;
}
String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
return;
}
// 解决缓存击穿
RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
lock.lock();
try {
originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
// 双重检验锁,防止后续拿到锁的请求再去查询数据库
if (StrUtil.isNotBlank(originalLink)) {
((HttpServletResponse) response).sendRedirect(originalLink);
return;
}
LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
.eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
if (shortLinkGotoDO == null) {
// 缓存穿透,缓存空值
stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
return;
}
LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
.eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
.eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
.eq(ShortLinkDO::getDelFlag, 0)
.eq(ShortLinkDO::getEnableStatus, 0);
ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
if (shortLinkDO != null) {
stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl());
((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
}
} finally {
lock.unlock();
}
}
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
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
# 缓存预热
创建短链接时假如缓存;
stringRedisTemplate.opsForValue().set(
String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
requestParam.getOriginUrl(),
LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS
);
1
2
3
4
5
2
3
4
5
短链接如果过期将缓存设置为空,#restoreUrl
// 超过有效期,缓存设置为空 if (shortLinkDO == null || shortLinkDO.getValidDate().before(new Date())) { stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES); ((HttpServletResponse) response).sendRedirect("/page/notfound"); return; } stringRedisTemplate.opsForValue().set( String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl(), LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS );
1
2
3
4
5
6
7
8
9
10
11
# 重定向页面不存在
添加thymeleaf依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
1
2
3
4
2
3
4
yml文件中:
mvc:
view:
prefix: /templates/
suffix: .html
1
2
3
4
2
3
4
接口:
@Controller
public class ShortLinkNotfoundController {
/**
* 短链接不存在跳转页面
*/
@RequestMapping("/page/notfound")
public String notfound() {
return "notfound";
}
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
((HttpServletResponse) response).sendRedirect("/page/notfound");
1
布隆过滤器不存在;
goto缓存中不存在;
goDO中不存在;
有效期过期;
# 获取目标网站标题
添加依赖:
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.version}</version>
</dependency>
1
2
3
4
5
2
3
4
5
接口:
@RestController
@RequiredArgsConstructor
public class UrlTitleController {
private final UrlTitleService urlTitleService;
/**
* 根据 URL 获取对应网站的标题
*/
@GetMapping("/api/short-link/v1/title")
public Result<String> getTitleByUrl(@RequestParam("url") String url) {
return Results.success(urlTitleService.getTitleByUrl(url));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class UrlTitleServiceImpl implements UrlTitleService {
@SneakyThrows
@Override
public String getTitleByUrl(String url) {
URL targetUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
connection.setRequestMethod("GET");
connection.connect();
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
Document document = Jsoup.connect(url).get();
return document.title();
}
return "Error while fetching title.";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 获取目标网站图标
@SneakyThrows
private String getFavicon(String url) {
URL targetUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
connection.setRequestMethod("GET");
connection.connect();
int responseCode = connection.getResponseCode();
if (HttpURLConnection.HTTP_OK == responseCode) {
Document document = Jsoup.connect(url).get();
Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
if (faviconLink != null) {
return faviconLink.attr("abs:href");
}
}
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16