news 2026/4/16 11:09:48

【短链接项目笔记】6 短链接跳转

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【短链接项目笔记】6 短链接跳转

0.前言

这篇文章主要实现短链接跳转的功能,包括基础功能的实现以及缓存穿透、缓存击穿问题的解决。

1.短链接跳转原理

大多短链接系统短链接跳转逻辑应该是这样的:用户通过浏览器输入短链接访问,通过短链接获取到原始链接并进行跳转。短短一句话就把短链接跳转的逻辑说完了,但事实上里面要考虑的东西非常多,下面我们一层一层分析。

2.数据库准备

前期我们通过分库分表创建了16张t_link表,用于存放短链接,但其中的分片键是gid,也就是分组标识,但我们在跳转的时候,肯定是只输入一个短链接的,如果我们只通过短链接去查找link表,其效率可想而知,因此我们还需要创建一张路由表(同样用分库分表完成)。这张t_link_goto表只有三个字段:id,gid,fullShortUri。因此我们的跳转流程是:先根据传入的参数shortUri来查路由表t_link_goto,查到对应的gid后再去查主表t_link,获取对应的原始链接。

3.基础代码

  • ShortLinkServiceImpl.java
/** * 短链接跳转 * @param shortLink * @param request * @param response */@SneakyThrows@Overridepublicvoidredirect(StringshortLink,HttpServletRequestrequest,HttpServletResponseresponse){StringserverName=request.getServerName();StringfullShortUrl=serverName+"/"+shortLink;LambdaQueryWrapper<ShortLinkGotoDO>gotoQueryWrapper=Wrappers.lambdaQuery(ShortLinkGotoDO.class).eq(ShortLinkGotoDO::getFullShortUrl,fullShortUrl);ShortLinkGotoDOshortLinkGotoDO=shortLinkGotoMapper.selectOne(gotoQueryWrapper);if(shortLinkGotoDO==null){return;}LambdaQueryWrapper<ShortLinkDO>queryWrapper=Wrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getGid,shortLinkGotoDO.getGid()).eq(ShortLinkDO::getFullShortUrl,fullShortUrl).eq(ShortLinkDO::getEnableStatus,0).eq(ShortLinkDO::getDelFlag,0);ShortLinkDOshortLinkDO=baseMapper.selectOne(queryWrapper);if(shortLinkDO!=null){response.sendRedirect(shortLinkDO.getOriginUrl());}}

在基础的代码中,我们不考虑任何功能,只实现基础功能,首先由request获取域名,拼接上短链接,然后根据拼接后的fullShortUrl,去查找路由表,如果为空的话直接返回。不为空的话就根据fullShortUrl去查找主表,如果查找到的不为空,就实现跳转。(在这之前大家需要先配置下本地host,可以去搜一下教程,这边不做介绍,就是充当Nginx的作用)。

4.缓存击穿

缓存击穿指在高并发的系统中,一个热点数据缓存过期或者在缓存中不存在,导致大量并发请求直接访问数据库,从而给数据库造成巨大压力,甚至可能引起宕机。
具体来说,当某个热点数据在缓存中过期时,如果此时有大量并发请求同时访问这个数据,由于缓存中不存在,所有请求都会直接访问数据库,导致数据库负载急剧增加。
这里使用基于Redisson的分布式锁来实现。

/** * 短链接跳转 * @param shortLink * @param request * @param response */@SneakyThrows@Overridepublicvoidredirect(StringshortLink,HttpServletRequestrequest,HttpServletResponseresponse){StringserverName=request.getServerName();StringfullShortUrl=serverName+"/"+shortLink;//缓存为空,加锁RLocklock=redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY,fullShortUrl));try{lock.lock();originalLink=stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl));if(StrUtil.isNotBlank(originalLink)){//缓存不为空,直接跳转response.sendRedirect(originalLink);return;}LambdaQueryWrapper<ShortLinkGotoDO>gotoQueryWrapper=Wrappers.lambdaQuery(ShortLinkGotoDO.class).eq(ShortLinkGotoDO::getFullShortUrl,fullShortUrl);ShortLinkGotoDOshortLinkGotoDO=shortLinkGotoMapper.selectOne(gotoQueryWrapper);if(shortLinkGotoDO==null){return;}LambdaQueryWrapper<ShortLinkDO>queryWrapper=Wrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getGid,shortLinkGotoDO.getGid()).eq(ShortLinkDO::getFullShortUrl,fullShortUrl).eq(ShortLinkDO::getEnableStatus,0).eq(ShortLinkDO::getDelFlag,0);ShortLinkDOshortLinkDO=baseMapper.selectOne(queryWrapper);if(shortLinkDO!=null){response.sendRedirect(shortLinkDO.getOriginUrl());}}finally{lock.unlock();}}

5.缓存穿透

缓存穿透是指在缓存中查询一个一定不存在的数据,由于缓存不命中,导致请求直接访问数据库,这将导致大量的请求打到数据库上,可能会导致数据库压力过大。
通常情况下,缓存是为了提高数据访问速度,避免频繁查询数据库。但如果攻击者故意请求缓存中不存在的数据,就会导致缓存不命中,请求直接访问数据库。

5.1 空对象值缓存

当查询结果为空时,也将结果进行缓存,但是设置一个较短的过期时间。这样在接下来的一段时间内,如果再次请求相同的数据,就可以直接从缓存中获取,而不是再次访问数据库,可以一定程度上解决缓存穿透问题。

这种方式是比较简单的一种实现方案,会存在一些弊端。那就是当短时间内存在大量恶意请求,缓存系统会存在大量的内存占用。如果要解决这种海量恶意请求带来的内存占用问题,需要搭配一套风控系统,对用户请求缓存不存在数据进行统计,进而封禁用户。整体设计就较为复杂,不推荐使用。

5.2 使用锁

当请求发现缓存不存在时,可以使用锁机制来避免多个相同的请求同时访问数据库,只让一个请求去加载数据,其他请求等待。
这种方式可以解决数据库压力过大问题,如果会出现“误杀”现象,那就是如果缓存中不存在但是数据库存在这种情况,也会等待获取锁,用户等待时间过长,不推荐使用。

5.3 布隆过滤器

布隆过滤器是一种数据结构,可以用于判断一个元素是否存在于一个集合中。它可以在很大程度上减轻缓存穿透问题,因为它可以快速判断一个数据是否可能存在于缓存中。
这种方式较为推荐,可以将所有存量数据全部放入布隆过滤器,然后如果缓存中不存在数据,紧接着判断布隆过滤器是否存在,如果存在访问数据库请求数据,如果不存在直接返回错误响应即可。
但是这种问题还是会有一些小概率问题,那就是如果使用一种小概率误判的缓存进行攻击,依然会对数据库造成比较大的压力。

6.组合方案

上面的这些方案或多或少都会有些问题,应该用三者进行组合用来解决缓存穿透问题。

如果说缓存不存在,那么就通过布隆过滤器进行初步筛选,然后判断是否存在缓存空值,如果存在直接返回失败。如果不存在缓存空值,使用锁机制避免多个相同请求同时访问数据库。最后,如果请求数据库为空,那么将为空的 Key 进行空对象值缓存。

6.1 常规缓存查询

  • 用户发起请求时,首先访问Redis缓存,如果命中了,直接返回进行跳转,流程结束。
  • 缓存未命中,可能是数据不存在,也可能是热点Key刚过期,继续执行下面的流程。

6.2 布隆过滤器(防穿透)

判断请求Key是否存在于布隆过滤器中。如果布隆过滤器说不存在,那就一定不存在!直接返回404,并退出。如果布隆过滤器说存在,那有一定的误判几率,继续向下执行。

6.3 缓存空值判断

判断RedisKey是否存在空值,如果存的是null,直接返回空,不再往下走。如果不为空,说明这可能是个真正的热点 Key 失效,准备去查库。

6.4 分布式锁

此时,可能已经有了1w+个请求通过了前面的流程,要去查数据库来了。这里利用Redisson的分布式锁,只允许一个线程拿到锁,剩下的在门外等待。

6.5 查数据库

拿到锁的线程去查MySQL数据库。查到了就将真实数据写入Redis,释放锁;没查到说明布隆过滤器产生了误判,为了防止下一次请求来到数据库,必须往Redis写入一个空值,并设置较短的过期时间。

/** * 短链接跳转 * @param shortLink * @param request * @param response */@SneakyThrows@Overridepublicvoidredirect(StringshortLink,HttpServletRequestrequest,HttpServletResponseresponse){StringserverName=request.getServerName();StringfullShortUrl=serverName+"/"+shortLink;//解决缓存穿透和缓存击穿/** * 阶段1:查询缓存 *///从redis获取原始链接StringoriginalLink=stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl));if(StrUtil.isNotBlank(originalLink)){//判断请求key是否为空值if(GOTO_IS_NULL_SHORT_LINK_KEY.equals(originalLink)){//Redis里存储的是空值,说明以前查过数据库不存在,返回404response.sendError(HttpServletResponse.SC_NOT_FOUND);}//查到缓存,直接跳转response.sendRedirect(originalLink);return;}//缓存为空,去布隆过滤器中查询/** * 阶段2:布隆过滤器(防穿透) */if(!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)){//布隆过滤器说不存在,一定不存在,返回404response.sendError(HttpServletResponse.SC_NOT_FOUND);return;}//布隆过滤器说存在,代表可能存在,需要进一步查询/** * 阶段3:分布式锁&数据库查询(防击穿) *///缓存为空,加锁RLocklock=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)){if(GOTO_IS_NULL_SHORT_LINK_KEY.equals(originalLink)){response.sendError(HttpServletResponse.SC_NOT_FOUND);return;}//缓存不为空,直接跳转response.sendRedirect(originalLink);return;}//查询数据库//1.先查路由表LambdaQueryWrapper<ShortLinkGotoDO>gotoQueryWrapper=Wrappers.lambdaQuery(ShortLinkGotoDO.class).eq(ShortLinkGotoDO::getFullShortUrl,fullShortUrl);ShortLinkGotoDOshortLinkGotoDO=shortLinkGotoMapper.selectOne(gotoQueryWrapper);if(shortLinkGotoDO==null){//数据不存在,需要缓存空值到redisstringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl),GOTO_IS_NULL_SHORT_LINK_KEY,30,TimeUnit.SECONDS);response.sendError(HttpServletResponse.SC_NOT_FOUND);return;}//2.查主表LambdaQueryWrapper<ShortLinkDO>queryWrapper=Wrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getGid,shortLinkGotoDO.getGid()).eq(ShortLinkDO::getFullShortUrl,fullShortUrl).eq(ShortLinkDO::getEnableStatus,0).eq(ShortLinkDO::getDelFlag,0);ShortLinkDOshortLinkDO=baseMapper.selectOne(queryWrapper);if(shortLinkDO!=null){//将数据新增进缓存StringtargetUrl=shortLinkDO.getOriginUrl();if(!targetUrl.startsWith("http://")){targetUrl="http://"+targetUrl;}stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl),targetUrl,1,TimeUnit.DAYS);response.sendRedirect(targetUrl);}else{stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl),GOTO_IS_NULL_SHORT_LINK_KEY,30,TimeUnit.SECONDS);response.sendError(HttpServletResponse.SC_NOT_FOUND);}}finally{// 释放锁if(lock.isLocked()&&lock.isHeldByCurrentThread()){lock.unlock();}}}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 10:12:08

Markdown语法高亮插件适配Miniconda-Python3.10代码块

Markdown语法高亮插件适配Miniconda-Python3.10代码块 在当今AI与数据科学项目日益复杂的背景下&#xff0c;技术文档的准确性不再只是“锦上添花”&#xff0c;而是保障协作效率、实验复现和知识传承的关键。一个看似简单的代码块渲染问题——比如Python 3.10特有的match-case…

作者头像 李华
网站建设 2026/4/16 11:08:49

【计算机毕业设计案例】基于SpringBoot的智慧社区系统设计与实现基于Web的智慧社区系统设计与实现(程序+文档+讲解+定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/4/16 10:54:03

【计算机毕业设计案例】基于SpringBoot+Vue房屋租赁系统的设计和实现基于SpringBoot的房屋租赁系统的设计与实现(程序+文档+讲解+定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/4/16 0:28:18

Pyenv与Miniconda共存方案:独立管理Python与AI依赖

Pyenv与Miniconda共存方案&#xff1a;独立管理Python与AI依赖 在现代AI和数据科学项目中&#xff0c;一个常见的“噩梦”场景是&#xff1a;你在本地调试好的模型&#xff0c;换到同事机器上却报错——ModuleNotFoundError、版本不兼容、CUDA驱动冲突……归根结底&#xff0c;…

作者头像 李华
网站建设 2026/4/14 18:37:31

Java计算机毕设之基于SpringBoot少数民族服饰在线销售系统民族文化在线展示与传承的设计与实现完整前后端代码+说明文档+LW,调试定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华