
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| @SneakyThrows @Override public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) { String serverName = request.getServerName(); String serverPort = Optional.of(request.getServerPort()) .filter(each -> !Objects.equals(each, 80)) .map(String::valueOf) .map(each -> ":" + each) .orElse(""); String fullShortUrl = serverName + serverPort + "/" + shortUri; 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; } boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl); if (!contains) { ((HttpServletResponse) response).sendRedirect("/page/notfound"); return; } String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl)); if (StrUtil.isNotBlank(gotoIsNullShortLink)) { ((HttpServletResponse) response).sendRedirect("/page/notfound"); 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)) { 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; } 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(); } }
|
1. 通过短链接缓存获取对应的原始连接
1. 从缓存尝试获取原始链接
如果找到了缓存中的长链接,则跳转到该 URL,并记录访问统计信息
1
| String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
|
2. 缓存中没有,缓存穿透:检查是否是有效的短链接
避免缓存穿透
如果缓存中没有对应的长链接,接下来使用布隆过滤器检查该短链接是否存在,不存在返回404
但可能存在布隆过滤器误判,所以再次检查缓存中是否有缓存空值,如果有返回404
1 2 3 4 5
| boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl); if (!contains) { ((HttpServletResponse) response).sendRedirect("/page/notfound"); return; }
|
如果布隆过滤器没有,检查缓存有没有缓存空值,如果有返回404
1 2 3 4 5
| String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl)); if (StrUtil.isNotBlank(gotoIsNullShortLink)) { ((HttpServletResponse) response).sendRedirect("/page/notfound"); return; }
|
2. 如果原始连接缓存失效,通过数据库进行查询并再放入缓存
1. 双重检查锁,避免缓存击穿
在获取到分布式锁之后,再次查询一次缓存是否存在。如果缓存中存在数据,就直接返回;如果不存在,才继续执行查询数据库的操作。
避免获取到分布式锁后都去查询数据库,只有第一次拿到锁的线程去查询数据库,其他线程直接返回缓存中的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| 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; } 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(); }
|
2. 查询数据库并缓存到 Redis
如果数据库中不存在对应短链接,缓存空值并返回404
如果数据库中存在,将原始链接缓存到 Redis,并跳转到原始链接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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; } 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());
|