网站结构模板,成武网站建设,宁波网站推广工具,汽车行业网站建设比较目录
缓存穿透原理#xff1a;
缓存穿透一般有几种解决方案#xff1a;
1.缓存空值
2.使用锁
3.布隆过滤器 优缺点
布隆过滤器误判理解
布隆过滤器的简单使用流程
4.组合方案
那么当我们高并发的访问短链接或者人为的去穿透的时候呢#xff1f; 最近做项目遇到了缓…目录
缓存穿透原理
缓存穿透一般有几种解决方案
1.缓存空值
2.使用锁
3.布隆过滤器 优缺点
布隆过滤器误判理解
布隆过滤器的简单使用流程
4.组合方案
那么当我们高并发的访问短链接或者人为的去穿透的时候呢 最近做项目遇到了缓存的一些问题总结一下解决方法
缓存穿透原理
缓存穿透是指在缓存中查询一个一定不存在的数据由于缓存不命中导致请求直接访问数据库这将导致大量的请求打到数据库上可能会导致数据库压力过大。
具体原理 缓存穿透一般有几种解决方案
1.缓存空值
当查询结果为空时也将结果进行缓存但是设置一个较短的过期时间。这样在接下来的一段时间内如果再次请求相同的数据就可以直接从缓存中获取而不是再次访问数据库。
这种方式是比较简单的一种实现方案可以很好解决缓存穿透问题但是会存在一些弊端。那就是当短时间内存在大量恶意请求缓存系统会存在大量的内存占用。如果要解决这种海量恶意请求带来的内存占用问题需要搭配一套风控系统对用户请求缓存不存在数据进行统计进而封禁用户。整体设计就较为复杂不推荐使用。 2.使用锁
当请求发现缓存不存在时可以使用锁机制来避免多个相同的请求同时访问数据库只让一个请求去加载数据其他请求等待。
这种方式可以解决数据库压力过大问题如果会出现“误杀”现象那就是如果缓存中不存在但是数据库存在这种情况也会等待获取锁用户等待时间过长不推荐使用。 3.布隆过滤器
布隆过滤器是一种数据结构用于快速判断一个元素是否存在于一个集合中。具体来说布隆过滤器包含一个位数组和一组哈希函数。位数组的初始值全部置为 0。在插入一个元素时将该元素经过多个哈希函数映射到位数组上的多个位置并将这些位置的值置为 1。
在查询一个元素是否存在时会将该元素经过多个哈希函数映射到位数组上的多个位置如果所有位置的值都为 1则认为元素存在如果存在任一位置的值为 0则认为元素不存在。 优缺点
优点
高效地判断一个元素是否属于一个大规模集合。节省内存。
缺点
可能存在一定的误判。
布隆过滤器误判理解
布隆过滤器要设置初始容量。容量设置越大冲突几率越低。布隆过滤器会设置预期的误判值。
布隆过滤器的特点
查询是否存在如果返回存在可能数据是不存在的。查询是否存在如果返回不存在数据一定不存在。 布隆过滤器的简单使用流程
1.导入依赖
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId
/dependencydependencygroupIdorg.redisson/groupIdartifactIdredisson-spring-boot-starter/artifactId
/dependency
2.配置Redis参数
spring:data:redis:host: 127.0.0.1port: 6379password: 123456
3.配置布隆过滤器
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 布隆过滤器配置*/
Configuration
public class RBloomFilterConfiguration {/*** 防止用户注册查询数据库的布隆过滤器*/Beanpublic RBloomFilterString userRegisterCachePenetrationBloomFilter(RedissonClient redissonClient) {RBloomFilterString cachePenetrationBloomFilter redissonClient.getBloomFilter(userRegisterCachePenetrationBloomFilter);cachePenetrationBloomFilter.tryInit(10000000L, 0.001);return cachePenetrationBloomFilter;}
}
注意
tryInit 有两个核心参数
expectedInsertions预估布隆过滤器存储的元素长度。falseProbability运行的误判率。 错误率越低位数组越长布隆过滤器的内存占用越大。 错误率越低散列 Hash 函数越多计算耗时较长。 4.使用
4.1先注入private final RBloomFilterString userRegisterCachePenetrationBloomFilter; 4.2 userRegisterCachePenetrationBloomFilter.contains(xxx) 这个判断元素是否存在布隆过滤器 userRegisterCachePenetrationBloomFilter.add(xxx) 把元素加入布隆过滤器 4.组合方案
上面的这些方案或多或少都会有些问题应该用三者进行组合用来解决缓存穿透问题。如果说缓存不存在那么就通过布隆过滤器进行初步筛选然后判断是否存在缓存空值如果存在直接返回失败。如果不存在缓存空值使用锁机制避免多个相同请求同时访问数据库。最后如果请求数据库为空那么将为空的 Key 进行空对象值缓存。
1.当我们生成短链接的时候因为完整短链接是唯一的我们用布隆过滤器判断不存在才生成。
private String generateSuffix(ShortLinkCreateReqDTO shortLinkCreateReqDTO) {int count 0;String shortUri;while (true) {if (count 10) {throw new ServiceException(短链接创造频繁请稍后再试);}//加上当前毫秒数减少重复可能shortUri HashUtil.hashToBase62(shortLinkCreateReqDTO.getOriginUrl() System.currentTimeMillis());if (!shortUriCreateCachePenetrationBloomFilter.contains(shortLinkCreateReqDTO.getDomain() / shortUri)) {break;}count;}return shortUri;}
2.当上一步的布隆过滤器误判了明明存在但判断不存在。当我们插入短链接的时候去查一次数据库。如果存在数据证明布隆过滤器误判。 try {baseMapper.insert(shortLinkDO);shortLinkGotoMapper.insert(shortLinkGotoDO);// basemapper的插入 这个异常是 插入mysql 是 key重复了 因为布隆过滤器误判才会如此// 存在的 判断为 不存在} catch (DuplicateKeyException ex) {// TODO 布隆过滤器误判咋办// 那就去数据库查在判断一次ShortLinkDO ifExit this.baseMapper.selectOne(Wrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getFullShortUrl, full_short_link));if (ifExit ! null) {log.warn(短链接{} 重复入库, full_short_link);throw new ServiceException(短链接存在了);}}
3.成功插入之后把完整短链接加入布隆过滤器。同时缓存预热key为原始连接 stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY,full_short_link),shortLinkCreateReqDTO.getOriginUrl(),LinkUtil.getLinCacheValidDate(shortLinkCreateReqDTO.getValidDate()),TimeUnit.MILLISECONDS);shortUriCreateCachePenetrationBloomFilter.add(full_short_link);
以上是插入一条短链接的判断大致流程 那么当我们高并发的访问短链接或者人为的去穿透的时候呢
比如下面有人恶意请求毫秒级触发大量请求去一个插入的短链接
1.先从缓存中拿原始链接这个访问当时是之前我们已经通过插入mysql时候预热到缓存中的拿到就跳转这里跳转不了
2.布隆过滤器判断是否包含完整的短链接明显没有如果误判的话逻辑下走
3.这个要调回头再看第一次明显不走
4.分布式锁双检加锁策略。
5.因为数据本就不存在所以shortLinkGotoDO null存入信息到IS_NO_SHORK_LINK对应了第3步
以上步骤我们判断了布隆过滤器查看了是否为空值加了分布式锁。
6.一直向向下乃至释放锁是正常访问的流程。 SneakyThrowsOverridepublic void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {String serverName request.getServerName();String full_short_url serverName / shortUri;//1.先从缓存中那 跳转的原始链接 拿到的话直接跳转String origin_url stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, full_short_url));if (StrUtil.isNotBlank(origin_url)) {HttpServletResponse response1 (HttpServletResponse) response;response1.sendRedirect(origin_url);return;}// 判断布隆过滤器是否存在 完整短连接 这个full_short_url 在添加短连接的时候就添加到布隆过滤器里面了// 这个避免了 穿透 乱输入的链接地址 PS : 误判的话逻辑向下走 通过redis的路由表 在判断一次boolean contains shortUriCreateCachePenetrationBloomFilter.contains(full_short_url);if(!contains) {((HttpServletResponse) response).sendRedirect(/page/notfound);return;}//缓存的路由信息 存在String isNoShortGotoLink stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NOLL_SHORT_LINK_KEY, full_short_url));if(StrUtil.isNotBlank(isNoShortGotoLink)) {((HttpServletResponse) response).sendRedirect(/page/notfound);return;}//分布式锁RLock lock redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, full_short_url));lock.lock();try {origin_url stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, full_short_url));if(StrUtil.isNotBlank(origin_url)) {HttpServletResponse response1 (HttpServletResponse) response;response1.sendRedirect(origin_url);return;}//根据 full_short_url 查找路由表ShortLinkGotoDO shortLinkGotoDO shortLinkGotoMapper.selectOne(Wrappers.lambdaQuery(ShortLinkGotoDO.class).eq(ShortLinkGotoDO::getFullShortUrl, full_short_url));if (shortLinkGotoDO null) {// 这个旨在解决布隆过滤器误判stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NOLL_SHORT_LINK_KEY, full_short_url),-,30, TimeUnit.MINUTES);((HttpServletResponse) response).sendRedirect(/page/notfound);return;}//根据路由表的Gid 和 full_short_url 查找 shortLinkDOShortLinkDO shortLinkDO baseMapper.selectOne(Wrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid()).eq(ShortLinkDO::getEnableStatus, 0).eq(ShortLinkDO::getDelFlag, 0).eq(ShortLinkDO::getFullShortUrl, shortLinkGotoDO.getFullShortUrl()));if (shortLinkDO ! null) {//这是解决短链接 已经过期的问题if(shortLinkDO.getValidDate() !null shortLinkDO.getValidDate().before(new Date())) {stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NOLL_SHORT_LINK_KEY, full_short_url),-,30, TimeUnit.MINUTES);((HttpServletResponse) response).sendRedirect(/page/notfound);return;}stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, full_short_url),shortLinkDO.getOriginUrl(),LinkUtil.getLinCacheValidDate(shortLinkDO.getValidDate()),TimeUnit.MILLISECONDS);HttpServletResponse response1 (HttpServletResponse) response;response1.sendRedirect(shortLinkDO.getOriginUrl());}} finally {lock.unlock();}}