Redis缓存穿透的几种解决方案

目录

缓存穿透原理:

缓存穿透一般有几种解决方案:

1.缓存空值

2.使用锁

3.布隆过滤器

 优缺点

布隆过滤器误判理解

布隆过滤器的简单使用流程

4.组合方案

那么当我们高并发的访问短链接或者人为的去穿透的时候呢?


最近做项目遇到了缓存的一些问题,总结一下解决方法

缓存穿透原理:

缓存穿透是指在缓存中查询一个一定不存在的数据,由于缓存不命中,导致请求直接访问数据库,这将导致大量的请求打到数据库上,可能会导致数据库压力过大。

具体原理:

缓存穿透一般有几种解决方案:
1.缓存空值

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

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

2.使用锁

当请求发现缓存不存在时,可以使用锁机制来避免多个相同的请求同时访问数据库,只让一个请求去加载数据,其他请求等待。

这种方式可以解决数据库压力过大问题,如果会出现“误杀”现象,那就是如果缓存中不存在但是数据库存在这种情况,也会等待获取锁,用户等待时间过长,不推荐使用

3.布隆过滤器

布隆过滤器是一种数据结构,用于快速判断一个元素是否存在于一个集合中。具体来说,布隆过滤器包含一个位数组和一组哈希函数。位数组的初始值全部置为 0。在插入一个元素时,将该元素经过多个哈希函数映射到位数组上的多个位置,并将这些位置的值置为 1。

查询一个元素是否存在时,会将该元素经过多个哈希函数映射到位数组上的多个位置,如果所有位置的值都为 1,则认为元素存在;如果存在任一位置的值为 0,则认为元素不存在。

 优缺点

优点:

  • 高效地判断一个元素是否属于一个大规模集合。
  • 节省内存。

缺点:

  • 可能存在一定的误判。
布隆过滤器误判理解
  • 布隆过滤器要设置初始容量。容量设置越大,冲突几率越低。
  • 布隆过滤器会设置预期的误判值。
布隆过滤器的特点
  • 查询是否存在,如果返回存在,可能数据是不存在的。
  • 查询是否存在,如果返回不存在,数据一定不存在。

布隆过滤器的简单使用流程

1.导入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
</dependency>

2.配置Redis参数

spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: 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 {

    /**
     * 防止用户注册查询数据库的布隆过滤器
     */
    @Bean
    public RBloomFilter<String> userRegisterCachePenetrationBloomFilter(RedissonClient redissonClient) {
        RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter("userRegisterCachePenetrationBloomFilter");
        cachePenetrationBloomFilter.tryInit(10000000L, 0.001);
        return cachePenetrationBloomFilter;
    }
}

注意:

tryInit 有两个核心参数:

  • expectedInsertions:预估布隆过滤器存储的元素长度。
  • falseProbability:运行的误判率。

错误率越低,位数组越长,布隆过滤器的内存占用越大。

错误率越低,散列 Hash 函数越多,计算耗时较长。

4.使用:

4.1先注入:private final RBloomFilter<String> 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.一直向向下乃至释放锁是正常访问的流程。

    @SneakyThrows
    @Override
    public 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 查找 shortLinkDO
            ShortLinkDO 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();
        }
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/476269.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

多聆听,少评判

当朋友来找你倾诉、吐槽、诉苦&#xff0c;或是表达情绪的时候&#xff0c;你是怎样回应的&#xff1f; 许多人总有这样的习惯&#xff1a;每当听到朋友的倾诉&#xff0c;或者在网上看到别人诉苦时&#xff0c;第一反应往往是提建议&#xff1a;为什么你不试试这样做呢&#x…

代码随想录算法训练营第二十九天|491. 非递减子序列,46.全排列

491. 非递减子序列 题目 给你一个整数数组 nums &#xff0c;找出并返回所有该数组中不同的递增子序列&#xff0c;递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。 数组中可能含有重复元素&#xff0c;如出现两个整数相等&#xff0c;也可以视作递增序列的一种…

Kubernetes中PV和PVC的几种状态类型

文章目录 1、PV和PVC概念1.1、PV1.2、PVC 2、PV / PVC的关系3、PV / PVC的状态类型3.1. Available&#xff08;可用&#xff09;3.2. Bound&#xff08;已绑定&#xff09;3.3. Released&#xff08;已释放&#xff09;3.4. Pending&#xff08;待定&#xff09;3.5. Failed&am…

关于UDP协议

UDP协议是基于非连接的发送数据就是把数据包简单封装一下&#xff0c;然后从网卡发出去就可以&#xff0c;数据包之间没有状态上的联系&#xff0c;UDP处理方式简单&#xff0c;所以性能损耗非常少&#xff0c;对于CPU、内存资源的占用远小于TCP&#xff0c;但是对于网络传输过…

设计编程网站集:生活部分:饮食+农业,植物(暂记)

这里写目录标题 植物相关综合教程**大型植物&#xff1a;****高大乔木&#xff08;Trees&#xff09;&#xff1a;** 具有坚硬的木质茎&#xff0c;通常高度超过6米。例如&#xff0c;橡树、松树、榉树等。松树梧桐 **灌木&#xff08;Shrubs&#xff09;&#xff1a;** 比乔木…

旧版本navicat更换颜色/护眼背景(利用regedit注册表编辑器 )

navicat默认的背景颜色是白色的&#xff0c;新版本可以如图直接在工具选项里面设置&#xff0c;可以先检查一下&#xff0c;如果没有相关设置&#xff0c;如果没有再往后看解决方法 另外&#xff0c;还可以安装其他护眼软件&#xff0c;但 若是设置里没有这个选项&#xff0c;…

webgl canvas系列——快速加背景、抠图、加水印并下载图片

文章目录 ⭐前言⭐canvas绘制图片&#x1f496;绘制csdn图片&#x1f496;给png图片加背景&#x1f496;cavans下载图片&#x1f496;cavans上传图片并抠图&#x1f496;cavans添加文字水印&#x1f496;inscode 完整代码块 ⭐结束 ⭐前言 大家好&#xff0c;我是yma16&#x…

mysql性能调优

mysql性能调优 sysbench压测调优到百万级别qps sysbench压测调优到百万级别qps 这篇文章https://www.percona.com/blog/millions-queries-per-second-postgresql-and-mysql-peaceful-battle-at-modern-demanding-workloads/#:~:textWe%20contacted%20SysBench%20author%20Alex…

抖音,剪映,TikTok,竖屏短视频转场pr模板视频素材

120个叠加效果视频转场过渡素材&#xff0c;抖音,剪映,TikTok,短视频转场pr模板项目工程文件。 效果&#xff1a;VHS、光效、胶片、霓虹灯闪光、X射线、信号、老电影等。 适用软件&#xff1a;Adobe Premiere Pro 2018 12.0或更高版本。 视频素材与大多数应用程序兼容&#xff…

ES高可用

分布式搜索引擎ES 分布式搜索引擎ES1.数据聚合1.1.聚合的种类1.2.DSL实现聚合1.3.RestAPI实现聚合 2.自动补全2.1.拼音分词器2.2.自定义分词器2.3.自动补全查询2.4.实现酒店搜索框自动补全 3.数据同步思路分析 4.集群4.1 ES集群相关概念4.2.集群脑裂问题4.3.集群分布式存储4.4.…

Diff算法详解

简要了解 Diff 算法目的就是找出新旧虚拟dom差异&#xff0c;最小化更新视图&#xff1b;即本质就是比较两个JS对象的差异&#xff1b;并不是页面上所有的更新都需要Diff算法。 在了解Diff算法之前&#xff0c;我们首先需要了解一下什么是虚拟DOM。 虚拟DOM 虚拟DOM是表示真实…

iSAM2 部分状态更新算法 (I - 原理解读)

Title: iSAM2 部分状态更新算法 (I-原理解读) 文章目录 I. 前言II. 部分状态的更新 (Partial State Update)III. 因子图的线性化 (Linearization of Factor Grahps)1. 简单实例的设定2. 一个线性化计算3. 其他线性化计算4. 状态更新量说明 IV. 部分 QR 分解实现变量消元 (Elimi…

基于傅里叶描述子的手势动作识别,Matlab实现

博主简介&#xff1a; 专注、专一于Matlab图像处理学习、交流&#xff0c;matlab图像代码代做/项目合作可以联系&#xff08;QQ:3249726188&#xff09; 个人主页&#xff1a;Matlab_ImagePro-CSDN博客 原则&#xff1a;代码均由本人编写完成&#xff0c;非中介&#xff0c;提供…

什么是智慧公厕?智慧公厕打造公共厕所信息化应用基座

公共厕所一直以来都是城市管理的一项重要工作&#xff0c;而随着科技的发展&#xff0c;智慧公厕成为了城市管理的新方向。智慧公厕应用基座是利用物联网、互联网、大数据、云计算和自动化控制等技术&#xff0c;将公共厕所进行全方位的信息化、数字化和智慧化升级&#xff0c;…

训练YOLOv9-S

1. YOLOv9-S网络结构 1.1 改前改后的网络结构&#xff08;参数量、计算量&#xff09;对比 修改前调用的yolo.py测试的yolov9.yaml的打印网络情况&#xff0c;包含参数量、计算量 修改后调用的yolo.py测试的yolov9.yaml的打印网络情况&#xff0c;包含参数量、计算量 1.2 …

JAVA入门第一步

学习总结&#xff1a; 打开CMD常见的CMD命令 一、打开CMD CMD的概念 CMD是Windows操作系统中的命令提示符(Command Prompt)程序&#xff0c;它是一种命令行工具&#xff0c;可以让用户通过键入命令来与计算机进行交互。CMD是Windows中一个基本的系统组件&#xff0c;它提供了一…

Python学习:元组

Python 元组概念 Python 中的元组&#xff08;tuple&#xff09;是不可变的有序集合。它是一种数据类型&#xff0c;类似于列表&#xff08;list&#xff09;&#xff0c;但在创建后不能被修改。元组使用圆括号 () 来表示&#xff0c;其中的元素可以是任意类型&#xff0c;并且…

【C++ STL】string类最全解析(什么是string?string类的常用接口有哪些?)

目录 一、前言 二、什么是 string ? &#x1f4a6; string 类的基本概念 &#x1f4a6; string 类与 char * 的区别 &#x1f4a6; string 类的作用 &#x1f4a6; 总结 三、string 的常用接口详解 &#x1f4a6;string 类对象的默认成员函数 ① 构造函数(初始化) ② 赋值…

详解python中函数的参数传递

在这个用例中&#xff0c;我们要讨论的是关于函数的传参问题 我所使用的python版本为3.3.2 对于函数: def fun(arg):print(arg)def main():fun(hello,Hongten)if __name__ __main__:main() 当我们传递一个参数给fun()函数&#xff0c;即可打印出传递的参数值信息。 这里打印…

扫码签到效果如何制作?二维码签到表的制作技巧

一般参加活动或者会议时&#xff0c;都会需要在入口处签到登记之后才可进入&#xff0c;这种方式需要耗费大量的时间&#xff0c;而且带给参与者的体验也不好。面对这个问题&#xff0c;现在会通过签到二维码的方式来解决&#xff0c;只需要扫描二维码就可以在手机上登记信息&a…