Home
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • JavaSE
  • JVM
  • JUC
  • Netty
  • CPP
  • QT
  • UE
  • Go
  • Gin
  • Gorm
  • HTML
  • CSS
  • JavaScript
  • vue2
  • TypeScript
  • vue3
  • react
  • Spring
  • SpringMVC
  • Mybatis
  • SpringBoot
  • SpringSecurity
  • SpringCloud
  • Mysql
  • Redis
  • 消息中间件
  • RPC
  • 分布式锁
  • 分布式事务
  • 个人博客
  • 弹幕视频平台
  • API网关
  • 售票系统
  • 消息推送平台
  • SaaS短链接系统
  • Linux
  • Docker
  • Git
GitHub (opens new window)
Home
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • JavaSE
  • JVM
  • JUC
  • Netty
  • CPP
  • QT
  • UE
  • Go
  • Gin
  • Gorm
  • HTML
  • CSS
  • JavaScript
  • vue2
  • TypeScript
  • vue3
  • react
  • Spring
  • SpringMVC
  • Mybatis
  • SpringBoot
  • SpringSecurity
  • SpringCloud
  • Mysql
  • Redis
  • 消息中间件
  • RPC
  • 分布式锁
  • 分布式事务
  • 个人博客
  • 弹幕视频平台
  • API网关
  • 售票系统
  • 消息推送平台
  • SaaS短链接系统
  • Linux
  • Docker
  • Git
GitHub (opens new window)
  • 如何设计一个短链系统
  • 新Get的开发技巧
  • 项目通用工具

  • 用户模块

  • 短链模块

    • 短链接分组

    • 短链接管理

      • 持久层
      • 短链接管理CRUD
      • 后管联调
      • 短链接跳转
        • 短链接原理
        • 短链接跳转
          • 路由表
        • 缓存击穿
        • 缓存穿透
        • 缓存预热
        • 重定向页面不存在
        • 获取目标网站标题
        • 获取目标网站图标
      • 短链管理之今日历史访问统计
    • 回收站管理

    • 短链接监控

  • 流量风控
  • 如何获取用户IP?
  • SaaS短链接系统
  • 短链模块
  • 短链接管理
Nreal
2024-01-14
目录

短链接跳转

# 短链接原理

  1. 通过压缩算法生成一个短链接;

  2. 访问短链接实际访问的是短链接服务器,查询数据库找到对应的长连接;

  3. 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
@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

修改短链接创建接口,创建短链接时需要再创建路由表;

@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

添加本地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

# 缓存穿透

流程:

布隆过滤器:

位数组+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

# 缓存预热

创建短链接时假如缓存;

stringRedisTemplate.opsForValue().set(
        String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
        requestParam.getOriginUrl(),
        LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS
);
1
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

yml文件中:

mvc:
view:
  prefix: /templates/
  suffix: .html
1
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
((HttpServletResponse) response).sendRedirect("/page/notfound");
1
  1. 布隆过滤器不存在;

  2. goto缓存中不存在;

  3. goDO中不存在;

  4. 有效期过期;

# 获取目标网站标题

添加依赖:

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>${jsoup.version}</version>
</dependency>
1
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
@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

# 获取目标网站图标

@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
后管联调
短链管理之今日历史访问统计

← 后管联调 短链管理之今日历史访问统计→

Theme by Vdoing | Copyright © 2021-2024
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式