14:link的跳转功能

9:短链接的跳转功能

跳转原理

通过短链接获取到对应的长链接,对长链接进行 302 重定向,最终访问原始网址。

跳转流程

在这里插入图片描述

  1. 缓存怎么办?访问缓存和数据库不存在短网址,数据库压力骤增。
  2. 短时间内大量请求访问一个缓存过期短链接,那么就会造成缓存击穿。
controller
    /**
     * 短链接跳转原始链接
     */
    @GetMapping("/{short-uri}")
    public void restoreUrl(@PathVariable("short-uri") String shortUri, ServletRequest request, ServletResponse response) {
        shortLinkService.restoreUrl(shortUri, request, response);
    }
service
    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
        // 短链接接口的并发量有多少?如何测试?详情查看:https://nageoffer.com/shortlink/question
        // 面试中如何回答短链接是如何跳转长链接?详情查看:https://nageoffer.com/shortlink/question
        String serverName = request.getServerName(); //获取请求的IP地址
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        //如果请求的端口号是80(标准HTTP端口),则不需要在URL中包含端口号,因此返回空字符串。
        //如果请求的端口号不是80(例如8080或其他非标准端口),则返回:端口号的字符串形式(例如:8080),用于构建完整的URL。

        String fullShortUrl = serverName + serverPort + "/" + shortUri;//拼接完整的短链接URL,包括服务器名称、端口(如果有)和短链接标识(shortUri)。

        //1:先查缓存中是否有当前的信息,如果有直接跳转。
        String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(originalLink)) {
            shortLinkStats(buildLinkStatsRecordAndSetUser(fullShortUrl, request, response));
            ((HttpServletResponse) response).sendRedirect(originalLink);
            return;
        }
        //2:如果缓存中没有当前的信息,再查布隆过滤器,如果布隆过滤器没有,直接返回,说明这是一个恶意请求。
        boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        if (!contains) {
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        //3:如果缓存中没有,布隆过滤其中有 -> 这是一个正常的请求,此时恰好缓存过期 / 这是一个恶意的请求,但是布隆过滤器假阳性 误判当前的短链接没有
        //   再查缓存中是否是空值,如果是空值, 直接返回,说明这是一个恶意请求,但是空值表一上来是什么都没有的,所以恶意请求第一次访问的时候,空值表中是没有数据的,会突破这一关。
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        //4:加分布式锁,只允许一个线程进行操作
        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)) {
                shortLinkStats(buildLinkStatsRecordAndSetUser(fullShortUrl, request, response));
                ((HttpServletResponse) response).sendRedirect(originalLink);
                return;
            }
            //进行双重判定 - 对空值缓存进行双重判定 - 可能短时间内大量恶意请求来请求空值缓存 - 此时大量恶意缓存在阻塞队列中 - 只允许一个线程读取数据库然后更新缓存 - 释放锁之后 - 阻塞队列中的线程直接从缓存判断是否是空值,直接返回,防止数据库挂了。
            gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                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);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            //如果路由表数据库中有这个数据 -> 正常请求 -> 通过路由表拿到当前的短链接 -> 判断短链接缓存是否是过期 -> 过期 -> 相当于没有当前的短链接,直接放入空值表中,并设置一个较短的过期时间
            //                                                                                   -> 没有过期 -> 再放入正常缓存中,并设置一个较长的过期时间,然后进行跳转
            //正常的请求没有过期,那是为什么从缓存中消失的 ? -> 在某些缓存系统中,如Redis,当缓存空间不足时,可能会根据设定的策略(如LRU,Least Recently Used)逐出一些不常用的数据。这会导致缓存中的某些数据消失,即使这些数据并未过期。
            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 || (shortLinkDO.getValidDate() != 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
            );
            shortLinkStats(buildLinkStatsRecordAndSetUser(fullShortUrl, request, response));
            ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
        } finally {
            lock.unlock();
        }
    }
image.png

逻辑步骤:

1:先检查缓存中是否有当前的信息,如果有,则直接跳转

2:如果缓存中没有当前的信息,再检查布隆过滤器,如果布隆过滤器中没有,则直接返回,因为这可能是一个恶意请求,由于布隆过滤器存在假阳性问题(误判当前没有的元素存在于布隆过滤器中)/ 这是一个正常的请求,此时恰好缓存过期

3:再查空值缓存,如果空值缓存有,说明这是一个恶意请求,直接返回,如果空值缓存没有 -> 空值表一上来是什么都没有的,所以恶意请求第一次访问的时候,空值表中是没有数据的,会突破这一关。/ 正常请求当然肯定不会存在于空值缓存中

4:加入分布式锁,此时只允许一个线程访问,保证数据库安全,防止大量请求涌入。

  • 对正常缓存进行双重判定 - 可能短时间内大量请求来请求正常的缓存 - 此时大量正常缓存在阻塞队列中 - 只允许一个线程读取数据库然后更新缓存 - 释放锁之后 - 阻塞队列中的线程直接从缓存中取即可。
  • 对空值缓存进行双重判定 - 可能短时间内大量恶意请求来请求空值缓存 - 此时大量恶意缓存在阻塞队列中 - 只允许一个线程读取数据库然后更新缓存 - 释放锁之后 - 阻塞队列中的线程直接从缓存判断是否是空值,直接返回,防止数据库挂了。

5:查数据库

  • 查路由表数据库 路由表数据库中没有这个数据 -> 恶意请求,直接在缓存中存放空值,直接返回
  • 如果路由表数据库中有这个数据 -> 正常请求 -> 通过路由表拿到当前的短链接 -> 判断短链接缓存是否是过期 -> 过期 -> 相当于没有当前的短链接,直接放入空值表中,并设置一个较短的过期时间
  • -> 没有过期 -> 再放入正常缓存中,并设置一个较长的过期时间,然后进行跳转

6:疑问 -> 正常的请求没有过期,那是为什么从缓存中消失的 ?

  • 在某些缓存系统中,如Redis,当缓存空间不足时,可能会根据设定的策略(如LRU,Least Recently Used)逐出一些不常用的数据。这会导致缓存中的某些数据消失,即使这些数据并未过期。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HackerTerry

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值