短链接day-07 技术重点

张开发
2026/5/17 12:09:37 15 分钟阅读
短链接day-07 技术重点
短链接跳转通过短链接获取到对应长链接对长链接进行302重定向最终访问原始网址跳转流程跳转实现首先先创建一个路由表因为要获取分片后的link表里的完整地址就需要gidlink表根据gid分片分片逻辑controller方法shortUri路径参数短链接的唯一标识request获取请求信息主要是域名用于构建完整的短链接进行查询response执行重定向操作将用户跳转到原始长链接GetMapping(/{short-uri}) public void restoreUrl(PathVariable(short-uri) String shortUri, ServletRequest request, ServletResponse response) { shortLinkService.restoreUrl(shortUri, request, response); }具体实现层SneakyThrows Override public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) { //第一步获取域名 String serverName request.getServerName(); //第二步获取完整的短链接 String fullShortUrl serverName / shortUri; //第三步查询短链接是否存在 //并修改添加短链接方法时将gid和对应拼接后的链接存入goto路由表中 LambdaQueryWrapperShortLinkGotoDO linkGotoQueryWrapper Wrappers.lambdaQuery(ShortLinkGotoDO.class) .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl); ShortLinkGotoDO shortLinkGotoDO shortLinkGotoMapper.selectOne(linkGotoQueryWrapper); if (shortLinkGotoDO null) { // 严谨来说此处需要进行封控 return; } //第四步根据拿到的gid查询短链接 LambdaQueryWrapperShortLinkDO 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 作用 // ServletResponse (通用) // ↓ 强制类型转换 //HttpServletResponse (HTTP 专业版) → 才会 sendRedirect重定向 技能 ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl()); } }其中最重要的就是((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());重定向方法。而ServletRequest是通用的请求接收器ServletResponse是通用的请求响应其需要转成HttpServletResponse才能调用http特有的方法。客户端 (浏览器)↓ 发送请求ServletRequest ← 接收请求数据 (输入)↓ 处理...ServletResponse ← 发送响应数据 (输出)↓ 返回结果客户端 (浏览器)关于继承强转子类转父类永远安全子类可以有父类没有的字段只是被隐藏了⚠️ 父类转子类看情况只有实际对象是子类时才安全问题分片link表后因为link表是根据gid分片而用户访问某个短链接的时候又不会携带gid所以此时需要一个路由表路由表根据用户携带的域名短链接进行分片通过路由表可以找到对应的gid再通过gid定位到对应的分片表找到对应原始域名。关于跳转时获取原始链接的缓存问题在查询原始链接时先查询redis中是否有key为GOTO_SHORT_LINK_KEY, fullShortUrl为key的如果没有则往下走获取锁防止有多个人同时存数据进redis然后再次判断redis中是否已经存了数据如果在判断redis中是否有到获取锁之间这段时间已经有人运行好了添加redis缓存的方法然后进行查询数据库中是否存在存在则存入redis并进行跳转。public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) { //第一步获取域名 String serverName request.getServerName(); //第二步获取完整的短链接 String fullShortUrl serverName / shortUri; //第三步查询在redis中短链接是否存在 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 { //第五步判断短链接是否在redis中存在二次确认。 originalLink stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl)); //如果存在则直接跳转 if (StrUtil.isNotBlank(originalLink)) { ((HttpServletResponse) response).sendRedirect(originalLink); return; } //第六步查询路由数据库 LambdaQueryWrapperShortLinkGotoDO linkGotoQueryWrapper Wrappers.lambdaQuery(ShortLinkGotoDO.class) .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl); ShortLinkGotoDO shortLinkGotoDO shortLinkGotoMapper.selectOne(linkGotoQueryWrapper); if (shortLinkGotoDO null) { // 严谨来说此处需要进行封控 return; } //如果路由数据库中存在则用拿到的gid去查已经分片的link表 LambdaQueryWrapperShortLinkDO 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) { //存入redis中拼接GOTO_SHORT_LINK_KEY, fullShortUrl为keyvalue为shortLinkDO.getOriginUrl() stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl()); ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl()); } } finally { lock.unlock(); } }关于缓存穿透和根据布隆过滤器误判攻击服务器解决方案设置一层布隆过滤器和一层空值判定在第一次进入到数据库的时候就缓存一个30min的该key的空缓存。缓存预热把预测高访问量的数据在创建时就存入redis//为防止缓存雪崩将对应的原始地址永久存入缓存 stringRedisTemplate.opsForValue().set( fullShortUrl, shortLinkCreateReqDTO.getOriginUrl(), LinkUtil.getLinkCacheValidTime(shortLinkCreateReqDTO.getValidDate()), TimeUnit.MILLISECONDS );如果传入的validdata有时间则会用DateUtil.between(new Date(), each, DateUnit.MS)获取距今的时间s。public static long getLinkCacheValidTime(Date validDate) { return Optional.ofNullable(validDate) //如果有效期存在也就是传入validdate就走map否则走orElse .map(each - DateUtil.between(new Date(), each, DateUnit.MS)) .orElse(DEFAULT_CACHE_VALID_TIME); }自定义错误页面通过新建controllerController public class ShortLinkNotfoundController { /** * 短链接不存在跳转页面 */ RequestMapping(/page/notfound) public String notfound() { return notfound; } }然后在代码里通过http请求((HttpServletResponse) response).sendRedirect(/page/notfound);跳转到写好的resources.templates.notfound.html页面。总跳转逻辑从原始链接目标中获取信息获取标题Service public class UrlTitleServiceImpl implements UrlTitleService { SneakyThrows//自动抛出异常 Override public String getTitleByUrl(String url) { //把传入的字符串网址变成 Java 能识别的 URL 对象 URL targetUrl new URL(url); //打开一个HTTP 连接准备访问目标网站 HttpURLConnection connection (HttpURLConnection) targetUrl.openConnection(); //设置请求方式为 GET最常用的网页访问方式 connection.setRequestMethod(GET); //真正发起连接去访问目标网站 connection.connect(); int responseCode connection.getResponseCode(); //判断响应码是不是 200成功 if (responseCode HttpURLConnection.HTTP_OK) { //使用 Jsoup 工具去请求网页并把网页内容解析成 Document 对象整个 HTML 文档 Document document Jsoup.connect(url).get(); //从 HTML 里提取 title 标签内容 return document.title(); } return Error while fetching title.; } }获取图标SneakyThrows private String getFavicon(String url) { //把传入的字符串网址变成 Java 能识别的 URL 对象 URL targetUrl new URL(url); //打开一个HTTP 连接准备访问目标网站 HttpURLConnection connection (HttpURLConnection) targetUrl.openConnection(); //设置请求方式为 GET最常用的网页访问方式 connection.setRequestMethod(GET); //真正发起连接去访问目标网站 connection.connect(); int responseCode connection.getResponseCode(); //判断响应码是不是 200成功 if (responseCode HttpURLConnection.HTTP_OK) { //使用 Jsoup 工具去请求网页并把网页内容解析成 Document 对象整个 HTML 文档 Document document Jsoup.connect(url).get(); //从 HTML 里提取 title 标签内容 Element element document.select(link[rel~(?i)^(shortcut\\s)?icon$]).first(); return element ! null ? element.attr(abs:href) : null; } return Error while fetching title.; }

更多文章