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

news2025/2/25 6:43:25

目录

缓存穿透原理:

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

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();
        }
    }

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot自动配置

EnableAutoConfiguration源码解析SpringBoot常用条件注解源码解析SpringBoot之Mybatis自动配置源码解析SpringBoot之AOP自动配置源码解析SpringBoot Jar包启动过程源码解析 SpringBoot2.6.6源码地址&#xff1a;SpringBoot-2.6.6: SpringBoot2.6.6 注解详解 SpringBootConfig…

【技术栈】Spring Cache 简化 Redis 缓存使用

​ SueWakeup 个人主页&#xff1a;SueWakeup 系列专栏&#xff1a;学习技术栈 个性签名&#xff1a;保留赤子之心也许是种幸运吧 ​ 本文封面由 凯楠&#x1f4f8; 友情提供 目录 本栏传送门 1. Spring Cache 介绍 2. Spring Cache 常用注解 注&#xff1a;手机端浏览本文章…

java入门 -输入和输出

输入输出 开发中大量会使用输入和输出&#xff0c;今天来总结一下Java语法阶段常使用的输入和输出。 输出 System.out 控制台输出信息。 不换行显示一行&#xff1a; System.out.print( ); System.out.print("hello "); System.out.print("java!");运行结…

如何强健“伙伴+华为”体系?华为用六大升级给出答案

聚拢企业内部资源&#xff0c;可以成事&#xff1b;而聚拢企业内外资源&#xff0c;则可成势。 华为如何在NA、商业和分销三大赛道聚拢伙伴之力成势&#xff1f;伙伴又如何与华为一起顺势而为&#xff0c;获得发展和收益&#xff1f;这是在一年一度的“华为中国合作伙伴大会”…

MyBatis3源码深度解析(十六)SqlSession的创建与执行(三)Mapper方法的调用过程

文章目录 前言5.9 Mapper方法的调用过程5.10 小结 前言 上一节【MyBatis3源码深度解析(十五)SqlSession的创建与执行(二)Mapper接口和XML配置文件的注册与获取】已经知道&#xff0c;调用SqlSession对象的getMapper(Class)方法&#xff0c;传入指定的Mapper接口对应的Class对象…

BUU [MRCTF2020]套娃

BUU [MRCTF2020]套娃 开题&#xff0c;啥也没有。 查看网页源代码发现后端源代码&#xff1a; <?php //1st $query $_SERVER[QUERY_STRING];if( substr_count($query, _) ! 0 || substr_count($query, %5f) ! 0 ){die(Y0u are So cutE!); }if($_GET[b_u_p_t] ! 23333 &am…

Vue核心知识点 -Vue2响应式系统是基于什么实现的、以及会产生什么问题和解决方案

一、概念 在Vue 2中&#xff0c;响应式系统是基于Object.defineProperty实现的。它通过劫持对象的属性来实现数据的响应式更新。 当你将一个对象传递给Vue实例的data选项时&#xff0c;Vue会遍历对象的每个属性&#xff0c;并使用Object.defineProperty方法将其转换为getter和s…

项目总结报告-word

2 项目工作成果 2.1 交付给用户的产品 2.2 交付给研发中心的产品 2.2.1 代码部分 2.2.2 文档部分 2.3 需求完成情况与功能及性能符合性统计 2.3.1 需求完成情况统计 2.3.2 功能符合性分析 2.3.3 性能符合性分析 3 项目工作分析 3.1 项目计划与进度实施分析 3.1.1 开发进度 3.1.…

2000-2021年各省研发强度数据(原始数据+计算结果)(无缺失)

2000-2021年各省研发强度数据&#xff08;原始数据计算结果&#xff09;&#xff08;无缺失&#xff09; 1、时间&#xff1a;2000-2021年 2、指标&#xff1a;RD经费内部支出&#xff08;万元&#xff09;、国内生产总值、研发强度 3、范围&#xff1a;31省 4、来源&#…

【源码阅读】EVMⅢ

参考[link](https://blog.csdn.net/weixin_43563956/article/details/127725385 大致流程如下&#xff1a; 编写合约 > 生成abi > 解析abi得出指令集 > 指令通过opcode来映射成操作码集 > 生成一个operation 以太坊虚拟机的工作流程&#xff1a; 由solidity语言编…

数据库系统概论-练手题集合【期末复习|考研复习】

前言 总结整理不易&#xff0c;希望大家点赞收藏。 给大家整理了一下数据库系统概论中的练手题&#xff0c;以供大家期末复习和考研复习的时候使用。 数据库系统概论系列文章传送门&#xff1a; 第一章 绪论 第二/三章 关系数据库和标准语言SQL 第四/五章 数据库安全性和完整性…

linux:线程互斥

个人主页 &#xff1a; 个人主页 个人专栏 &#xff1a; 《数据结构》 《C语言》《C》《Linux》 文章目录 前言一、线程互斥问题解释互斥量的接口 二、加锁的原理三、 死锁死锁四个必要条件避免死锁 总结 前言 本文是对于线程互斥的知识总结 一、线程互斥 问题 我们先看下面…

Division by Invariant Integers using Multiplication

在处理器中&#xff0c;整数除法的成本通常是整数乘法的几倍&#xff1a; 流水线式的组合乘法器通常在不到10个周期内完成操作&#xff1b;而对于整数除法则没有硬件支持&#xff0c;或者使用的迭代除法器比乘法器慢几倍。 表 1.1 比较了一些处理器上乘法和除法的时间。这张表…

c++多长时间会被Python或者其他语言取代?

c多长时间会被Python或者其他语言取代&#xff1f; 如果不考虑市场因素&#xff0c;C#今天就可以取代C。 自.NET跨平台至今&#xff0c;C能做的工作&#xff0c;C#都能做了&#xff0c;且性能差别不大。 在C最有优势的嵌入式UI方面&#xff0c;C#可以拿出Avalonia替代QT。用 …

存储器的层次结构和局部性原理

前言 大家好我是jiantaoyab&#xff0c;这是我所总结作为学习的笔记第19篇&#xff0c;在这里分享给大家&#xff0c;这篇文章讲存储器的一部分内容。 存储器的层次结构 SRAM 静态随机存取存储器的芯片&#xff0c;SRAM 之所以被称为“静态”存储器&#xff0c;是因为只要处…

MYSQL概念和编译安装

目录 一、数据库概述 1.1数据 1.2表 1.3数据库 总结&#xff1a; 2.数据库管理系统&#xff08;DBMS&#xff09; 3.DBMS工作模式 4.数据库系统原理 二、数据库发展史 三、主流数据库 四、关系型数据库和非关系型数据库 1.关系型数据库 2.非关系数据库 MYSQL数据…

输出菱形(*)--c语言

//输出菱形 #include<stdio.h>int main(){//上int line0;scanf("%d",&line);int i0;for(i0;i<line;i){int j0;//输出空格for(j0;j<line-1-i;j){printf(" ");}//输出*号for(j0;j<2*i1;j){printf("*");}printf("\n")…

Redisson 分布式锁原理分析

Redisson 分布式锁原理分析 示例程序 示例程序&#xff1a; public class RedissonTest {public static void main(String[] args) {Config config new Config();config.useSingleServer().setPassword("123456").setAddress("redis://127.0.0.1:6379"…

【开发环境】Ubuntu 18.04 搭建 QT编译环境详细步骤 【亲测有效】

目录 1 查看Ubuntu系统中Qt版本 2 下载Ubuntu系统Qt版本安装包 3 Qt安装 3.1 Qt 安装步骤 3.2 安装qt发现Ubuntu空间不足&#xff0c;怎么去扩容呢&#xff1f; 3.2.1 硬盘操作步骤&#xff08;需要关闭虚拟机进行操作&#xff09; 3.2.2 Ubuntu命令操作&#xff1a;安装…