seo建站营销,宣传片拍摄清单,网站的服务器每年都要续费的吗,企业为何要建设网站0.前言
这篇文章主要实现短链接跳转的功能#xff0c;包括基础功能的实现以及缓存穿透、缓存击穿问题的解决。
1.短链接跳转原理
大多短链接系统短链接跳转逻辑应该是这样的#xff1a;用户通过浏览器输入短链接访问#xff0c;通过短链接获取到原始链接并进行跳转。短短一句…0.前言这篇文章主要实现短链接跳转的功能包括基础功能的实现以及缓存穿透、缓存击穿问题的解决。1.短链接跳转原理大多短链接系统短链接跳转逻辑应该是这样的用户通过浏览器输入短链接访问通过短链接获取到原始链接并进行跳转。短短一句话就把短链接跳转的逻辑说完了但事实上里面要考虑的东西非常多下面我们一层一层分析。2.数据库准备前期我们通过分库分表创建了16张t_link表用于存放短链接但其中的分片键是gid也就是分组标识但我们在跳转的时候肯定是只输入一个短链接的如果我们只通过短链接去查找link表其效率可想而知因此我们还需要创建一张路由表同样用分库分表完成。这张t_link_goto表只有三个字段idgidfullShortUri。因此我们的跳转流程是先根据传入的参数shortUri来查路由表t_link_goto查到对应的gid后再去查主表t_link获取对应的原始链接。3.基础代码ShortLinkServiceImpl.java/** * 短链接跳转 * param shortLink * param request * param response */SneakyThrowsOverridepublicvoidredirect(StringshortLink,HttpServletRequestrequest,HttpServletResponseresponse){StringserverNamerequest.getServerName();StringfullShortUrlserverName/shortLink;LambdaQueryWrapperShortLinkGotoDOgotoQueryWrapperWrappers.lambdaQuery(ShortLinkGotoDO.class).eq(ShortLinkGotoDO::getFullShortUrl,fullShortUrl);ShortLinkGotoDOshortLinkGotoDOshortLinkGotoMapper.selectOne(gotoQueryWrapper);if(shortLinkGotoDOnull){return;}LambdaQueryWrapperShortLinkDOqueryWrapperWrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getGid,shortLinkGotoDO.getGid()).eq(ShortLinkDO::getFullShortUrl,fullShortUrl).eq(ShortLinkDO::getEnableStatus,0).eq(ShortLinkDO::getDelFlag,0);ShortLinkDOshortLinkDObaseMapper.selectOne(queryWrapper);if(shortLinkDO!null){response.sendRedirect(shortLinkDO.getOriginUrl());}}在基础的代码中我们不考虑任何功能只实现基础功能首先由request获取域名拼接上短链接然后根据拼接后的fullShortUrl去查找路由表如果为空的话直接返回。不为空的话就根据fullShortUrl去查找主表如果查找到的不为空就实现跳转。在这之前大家需要先配置下本地host可以去搜一下教程这边不做介绍就是充当Nginx的作用。4.缓存击穿缓存击穿指在高并发的系统中一个热点数据缓存过期或者在缓存中不存在导致大量并发请求直接访问数据库从而给数据库造成巨大压力甚至可能引起宕机。具体来说当某个热点数据在缓存中过期时如果此时有大量并发请求同时访问这个数据由于缓存中不存在所有请求都会直接访问数据库导致数据库负载急剧增加。这里使用基于Redisson的分布式锁来实现。/** * 短链接跳转 * param shortLink * param request * param response */SneakyThrowsOverridepublicvoidredirect(StringshortLink,HttpServletRequestrequest,HttpServletResponseresponse){StringserverNamerequest.getServerName();StringfullShortUrlserverName/shortLink;//缓存为空加锁RLocklockredissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY,fullShortUrl));try{lock.lock();originalLinkstringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl));if(StrUtil.isNotBlank(originalLink)){//缓存不为空直接跳转response.sendRedirect(originalLink);return;}LambdaQueryWrapperShortLinkGotoDOgotoQueryWrapperWrappers.lambdaQuery(ShortLinkGotoDO.class).eq(ShortLinkGotoDO::getFullShortUrl,fullShortUrl);ShortLinkGotoDOshortLinkGotoDOshortLinkGotoMapper.selectOne(gotoQueryWrapper);if(shortLinkGotoDOnull){return;}LambdaQueryWrapperShortLinkDOqueryWrapperWrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getGid,shortLinkGotoDO.getGid()).eq(ShortLinkDO::getFullShortUrl,fullShortUrl).eq(ShortLinkDO::getEnableStatus,0).eq(ShortLinkDO::getDelFlag,0);ShortLinkDOshortLinkDObaseMapper.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 */SneakyThrowsOverridepublicvoidredirect(StringshortLink,HttpServletRequestrequest,HttpServletResponseresponse){StringserverNamerequest.getServerName();StringfullShortUrlserverName/shortLink;//解决缓存穿透和缓存击穿/** * 阶段1查询缓存 *///从redis获取原始链接StringoriginalLinkstringRedisTemplate.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分布式锁数据库查询防击穿 *///缓存为空加锁RLocklockredissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY,fullShortUrl));lock.lock();try{originalLinkstringRedisTemplate.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.先查路由表LambdaQueryWrapperShortLinkGotoDOgotoQueryWrapperWrappers.lambdaQuery(ShortLinkGotoDO.class).eq(ShortLinkGotoDO::getFullShortUrl,fullShortUrl);ShortLinkGotoDOshortLinkGotoDOshortLinkGotoMapper.selectOne(gotoQueryWrapper);if(shortLinkGotoDOnull){//数据不存在需要缓存空值到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.查主表LambdaQueryWrapperShortLinkDOqueryWrapperWrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getGid,shortLinkGotoDO.getGid()).eq(ShortLinkDO::getFullShortUrl,fullShortUrl).eq(ShortLinkDO::getEnableStatus,0).eq(ShortLinkDO::getDelFlag,0);ShortLinkDOshortLinkDObaseMapper.selectOne(queryWrapper);if(shortLinkDO!null){//将数据新增进缓存StringtargetUrlshortLinkDO.getOriginUrl();if(!targetUrl.startsWith(http://)){targetUrlhttp://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();}}}