短链接day6

短链接跳转原始链接功能

  • 穿透 :大量请求了缓存和数据库中都没有的数据,每次都查询数据库,导致数据库压力过大
  • 击穿 : 大量key在同一时间过期,导致所有请求都达到数据库,导致数据库压力过大

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

解决方案:

接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短一些,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
使用布隆过滤器,需要安装redis组件
使用布谷鸟滤器,布谷鸟过滤器是布隆过滤器的升级版,需要安装redis组件
在客户端自行实现布隆过滤算法;

缓存击穿

缓存击穿指的是大量的key在同一时间过期,但是又有大量的请求需要用到这些已经过期的key,那么程序在redis找不到数据,就会去数据库里查询,数据库处理大量的请求的同时导致压力瞬间增大,造成压力过大,甚至导致崩溃;

解决方案

  1. 设置key值永不过期
  2. 将key的过期时间设为随机
  3. 使用布隆过滤器或者布谷鸟过滤器
  4. 使用分布式锁,当多个key过期时,同一时间只有一个查询请求下发到数据库,其他的key等待一个个地轮流查,就可以避免数据库压力过大的问题;

参考:谈谈redis缓存击穿透和缓存击穿的区别,以及它们所引起的雪崩效应-CSDN博客

缓存击穿:

String originallink=stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl));
  1. StringRedisTemplate:这是Spring Data Redis中用于操作Redis数据库的一个模板类,专门用于处理字符串类型的数据。它提供了很多便利的方法来操作Redis中的字符串数据。

  2. opsForValue():这是StringRedisTemplate中的一个方法,它返回了一个ValueOperations<K, V>类型的对象,这个对象提供了对Redis中字符串(String)类型数据的操作方法,如getset等。

  3. get(String key):这是ValueOperations<K, V>接口中的一个方法,用于根据给定的键(key)来获取对应的值(value)。如果键存在,则返回对应的值;如果键不存在,则通常返回null(但这也取决于具体的配置和Redis的版本)。

  4. String.format(GOTO_SHORT_LINK_KEY, fullShortUrl):这部分代码是动态生成键的过程。GOTO_SHORT_LINK_KEY是一个格式化字符串,它定义了键的结构,而fullShortUrl是一个变量,它包含了短链接的完整信息(可能是ID或者某种标识符)。String.format方法将fullShortUrl的值按照GOTO_SHORT_LINK_KEY中定义的格式插入到相应的位置,从而生成最终的键。

这行代码的作用是:根据给定的短链接标识符(fullShortUrl),通过特定的格式(GOTO_SHORT_LINK_KEY)生成一个键,然后从Redis数据库中检索出这个键所对应的值,这个值预期是一个原始链接(originallink)。这种机制常用于短链接服务中,用于将简短的URL映射回原始的、较长的URL。

RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();
  1. RedissonClientredissonClient是Redisson库的客户端实例,它提供了对Redis的丰富操作,包括但不限于数据结构操作、发布/订阅、Lua脚本执行、事务和分布式锁等。

  2. getLock(String name): 这个方法是RedissonClient接口中的一个方法,用于根据提供的名称(或键)获取一个RLock实例。RLock是Redisson对Java java.util.concurrent.locks.Lock接口的一个分布式实现,它允许在分布式环境中以线程安全的方式加锁和解锁资源。

  3. lock.lock(): 调用lock实例的lock()方法尝试获取锁。如果锁当前未被其他客户端持有,则当前客户端将成功获取锁并继续执行后续操作。如果锁已被其他客户端持有,则当前客户端将等待(可配置等待时间,如果未指定则为无限等待),直到锁被释放并变得可用。

    @Override
    public void restoreUrl(String shortUrl, ServletRequest request, ServletResponse response) throws IOException {
        //查短链接路由
        String servername=request.getServerName();
        String fullShortUrl=servername+"/"+shortUrl;
        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;
            }
            //根据路由的gid查短链接,从而定向到原始链接
            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(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl),shortLinkDO.getOriginUrl());
                ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
            }
        }finally {
            lock.unlock();
        }

    }

双重判定锁:首先加分布式锁,防止缓存过期之后的大量请求过来的缓存穿透问题;同时再加上一个双重判定锁,可以让只有第一个拿到锁的请求进行缓存重构,之后拿到锁的请求直接查询缓存即可,提高了程序运行效率。

对于缓存击穿 进行了双重锁的判定 极端情况下 当我们的缓存没有原始链接(originalLink)大量的请求就回去访问数据库 去重构缓存,这时候我们要设置第一层锁 防止多个请求同时重建缓存 当然为了节省数据库资源 在加一层锁 实现双重锁 第二层锁再次判定是否缓存 中包含原始链接 如果有 则直接去缓存 如果没有就去数据库里面查询在把数据库中的 数据传入给缓存 

缓存穿透:

布隆过滤器返回存在结果,真实情况可能存在也可能不存在;返回不存在,则一定不存在。

使用布隆过滤器检查,然后检查缓存里面是否是空值。

public void restoreUrl(String shortUrl, ServletRequest request, ServletResponse response) throws IOException {
        //查短链接路由
        String servername=request.getServerName();
        String fullShortUrl=servername+"/"+shortUrl;
        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;
            }
            //根据路由的gid查短链接,从而定向到原始链接
            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(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl),shortLinkDO.getOriginUrl());
                ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
            }
        }finally {
            lock.unlock();
        }

 缓存预热:

在创建短链接以及重定向短链接时进行预热(因为可能某个短链接很多访问量,这个时候就需要预热,提高性能)

public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        String shortLinkSuffix=generateSuffix(requestParam);
        String fullShortUrl=requestParam.getDomain()+"/"+shortLinkSuffix;
        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 shortLinkGotoDO = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();
        shortLinkGotoMapper.insert(shortLinkGotoDO);
        try{
            //数据库如果存在,则会报错,进入catch
            baseMapper.insert(shortLinkDO);
        }catch (DuplicateKeyException exp){
            //检查是否存在于数据库中,如果没存在,则说明布隆过滤器误判了。
            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("短链接生成重复");
            }
        }
        stringRedisTemplate.opsForValue()
                .set(
                        String.format(GOTO_SHORT_LINK_KEY,fullShortUrl),
                        requestParam.getOriginUrl(),
                        LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()),TimeUnit.MILLISECONDS);
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://"+shortLinkDO.getFullShortUrl())
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }

   
public void restoreUrl(String shortUrl, ServletRequest request, ServletResponse response) throws IOException {
        //查短链接路由
        String servername=request.getServerName();
        String fullShortUrl=servername+"/"+shortUrl;
        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;
            }
            //根据路由的gid查短链接,从而定向到原始链接
            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){
                if(shortLinkDO.getValidDate()!=null&&shortLinkDO.getValidDate().before(new Date())){
                    //如果缓存有效期已经失效了,就当作没有DO一样处理
                    stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortUrl),"-",30, TimeUnit.MINUTES);
                    return;
                }
                stringRedisTemplate.opsForValue()
                        .set(
                                String.format(GOTO_SHORT_LINK_KEY,fullShortUrl),
                                shortLinkDO.getOriginUrl(),
                                LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),TimeUnit.MILLISECONDS);
                ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
            }
        }finally {
            lock.unlock();
        }

    }

短链接不存在跳转指定页面功能

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
spring:
  mvc:
    view:
      prefix: /templates/
      suffix: .html

templates/notfound.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta
            name="viewport"
            content="width=device-width,initial-scale=1.0,minimum-scale=1.0,
    maximum-scale=1.0, user-scalable=no, shrink-to-fit=no, viewport-fit=cover"
    />
    <link rel="shortcut icon" href="" />
    <meta name="theme-color" content="#000000" />
    <meta property="og:title" lang="zh-CN" content="" />
    <meta name="theme-color" content="#000000" />
    <meta property="og:type" content="video" />
    <meta property="og:title" content="" />
    <meta property="og:description" content="" />
    <meta property="og:image" content="" />
    <meta property="og:image:width" content="750" />
    <meta property="og:image:height" content="1334" />
    <title></title>
    <style>
        .container,
        .pc-container {
            margin-top: 32vh;
            background: white;
            display: flex;
            align-items: center;
            flex-direction: column;
        }

        .text {
            color: #333333;
            line-height: 28px;
        }

        .container .text {
            margin-top: 16px;
            font-size: 3vw;
        }

        .pc-container .text {
            /* margin-top: 100px; */
            font-size: 18px;
        }

        .pc-container .img {
            height: 200px;
        }

        .container .img {
            width: 50vw;
        }

        textarea {
            width: 90vw;
        }
    </style>
</head>
<body>

<div class="pc-container">
    <div>
        <img
                class="img"
                src="//p3-live.byteimg.com/tos-cn-i-gjr78lqtd0/c03071dcdc52c24e0aab256518e51557.png~tplv-gjr78lqtd0-image.image"
        />
    </div>
    <div class="text">您访问的页面不存在,请确认链接是否正确</div>

</div>

</body>
</html>
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * 短链接不存在跳转控制器
 */
@Controller
public class ShortLinkNotFoundController {

	/**
     * 短链接不存在跳转页面
     */
    @RequestMapping("/page/notfound")
    public String notfound() {
        return "notfound";
    }
}

获取目标网站标题功能

        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.17.2</version>
        </dependency>
public class UrlTitleServiceImpl implements UrlTitleService {
    /**
     * 根据URL获取标题
     * @param url
     * @return
     */
    @SneakyThrows
    @Override
    public String getTitleByUrl(String url) {
        Document doc = Jsoup.connect(url).get();
        return doc.title();
//        URL tagetUrl = new URL(url);
//        HttpURLConnection connection = (HttpURLConnection) tagetUrl.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 "Erro while fetching title";
    }
}

获取目标完整图标功能

这里的匹配规则一定记得别写错了,一开始我就将rel写成了ref。

/**
     * 获取目标网站图标
     * @param url
     * @return
     * @throws IOException
     */
    private String getFavicon(String url) throws IOException {
        //创建URL对象
        URL targetUrl = new URL(url);
        //打开连接
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        // 禁止自动处理重定向
        connection.setInstanceFollowRedirects(false);
        // 设置请求方法为GET
        connection.setRequestMethod("GET");
        //连接
        connection.connect();
        //获取响应码
        int responseCode = connection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
            //获取重定向的URL
            String redirectUrl = connection.getHeaderField("Location");
            //如果重定向URL不为空
            if (redirectUrl != null) {
                // 创建新的URL对象
                URL newUrl = new URL(redirectUrl);//打开新的连接
                connection = (HttpURLConnection) newUrl.openConnection();//设置请求方法为GET
                connection.setRequestMethod("GET");//连接
                connection.connect();//获取新的响应码
                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;
    }

回收站管理

短链接移至回收站功能

修改enableStatus 0:启用 1:未启用,从0变更为1。

还需要删除该短链接相关的缓存。

回收站分页查询功能

可以复用短链接分页查询,但是需要注意,回收站分页查询不需要传gid,因为这样不利用检索。由于shortlink的分片键是gid,所以不传分组的话,默认会查全表。所以就通过查询当前用户的所有分组,然后传递给真正调用接口的地方,通过gid in gidList可以查。

回收站恢复短链接功能

回收站移除短链接功能

相关推荐

  1. day3

    2024-07-13 07:00:03       24 阅读
  2. 的理解

    2024-07-13 07:00:03       33 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-13 07:00:03       66 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-13 07:00:03       70 阅读
  3. 在Django里面运行非项目文件

    2024-07-13 07:00:03       57 阅读
  4. Python语言-面向对象

    2024-07-13 07:00:03       68 阅读

热门阅读

  1. Knife4j的原理及应用详解(一)

    2024-07-13 07:00:03       21 阅读
  2. Linux Vim基础教程

    2024-07-13 07:00:03       24 阅读
  3. 在Qt C++项目中调用7z API实现压缩和解压

    2024-07-13 07:00:03       16 阅读
  4. 详解C#委托与事件

    2024-07-13 07:00:03       27 阅读
  5. 在Spring Boot项目中集成监控与报警

    2024-07-13 07:00:03       27 阅读
  6. 第二讲 数据结构

    2024-07-13 07:00:03       21 阅读
  7. 11网络层-分组转发算法

    2024-07-13 07:00:03       27 阅读
  8. MySQL与Redis优化

    2024-07-13 07:00:03       24 阅读
  9. C++中的RTTI(运行时类型识别)的定义

    2024-07-13 07:00:03       26 阅读
  10. 「字符串匹配算法 1/3」朴素和Rabin-Karp

    2024-07-13 07:00:03       27 阅读
  11. Vue 组件之间的通信方式

    2024-07-13 07:00:03       25 阅读
  12. centos 安装vnc,配置图形界面

    2024-07-13 07:00:03       19 阅读
  13. 客户端与服务端之间的通信连接

    2024-07-13 07:00:03       23 阅读