Redis缓存穿透——实战代码教学,亲身体验高并发如何解决

news2024/9/22 17:25:32
  • 🚀 注重版权,转载请注明原作者和原文链接

  • 🥭 作者:全栈小袁

  • 🍎 原创个人开源博客项目(目前V2.0微服务版本):https://github.com/yuanprogrammer/xiaoyuanboke

  • 🍉 开源项目觉得还行的话点点star,有什么需要完善或者点子欢迎提issue

小袁有话说

众所周知,Redis三大问题,缓存穿透缓存击穿缓存雪崩,也是最常见的缓存问题,在面试当中也是经常被问到,今天我们就先来讲讲 缓存穿透 问题的解决以及如何编写代码

之前我也是看过很多相关的知识,这篇文章是结合自己所学总结的一篇文章,如果什么地方有问题或者不足之处可以评论区留言告诉我

缓存击穿和缓存雪崩,后续出~

一、前期准备

数据表

随便创建一个表,这里以用户表作为演示

CREATE TABLE `user`  (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
  `name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
  `mobile` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '号码',
  `email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `gmt_create` datetime(0) NULL DEFAULT NULL COMMENT '时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1001 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

使用在线工具随机生成一千条数据,工具地址:https://datum.codedefault.com/

由于SQL比较长,就不放在这里展示了,打开我的笔记即可复制SQL语句https://note.youdao.com/s/13OsjC3d

在这里插入图片描述

下面这些网上都搜得到的,用解压版即可,如果你们找不到可以文章留言(邮箱+工具),我看到会一起打包发给你

Redis

安装好redis,并成功连接上

Redis Desktop Manager

redis客户端工具,跟navicat这种作用相似,方便查看数据情况,当然你不用也行

在这里插入图片描述

Postman

http请求工具

在这里插入图片描述

Jmeter

高并发测试工具

在这里插入图片描述

二、构建Maven项目

创建一个Maven项目,

依赖

<dependencies>
    <!-- Web启动依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.3.12.RELEASE</version>
    </dependency>
    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <version>2.3.12.RELEASE</version>
    </dependency>
    <!-- MyBatis-Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.0</version>
    </dependency>
    <!-- 数据库驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.22</version>
    </dependency>
    <!-- hutool -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <optional>true</optional>
        <version>5.7.7</version>
    </dependency>
</dependencies>

application.yml

防止端口冲突,修改端口号

server:
  port: 8085
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/redis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: xiaoyuan
    password: root
  redis:
    port: 6379
    host: localhost
    database: 0

实体类

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.util.Date;

@Data
@TableName(value = "user")
public class User {

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    private String username;

    private String password;

    private String name;

    private String mobile;

    private String email;

    private Date gmtCreate;
}

Mapper接口

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.redis.entity.User;
import org.springframework.stereotype.Repository;

@Repository
public interface UserMapper extends BaseMapper<User> {

}

service接口

import com.baomidou.mybatisplus.extension.service.IService;
import com.redis.entity.User;

import java.util.List;

public interface UserService extends IService<User> {

    // 用户查询, 用name字段来模拟查询不存在的用户
    List<User> queryUser(String name);
}

impl实现类

一个简单的查询

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.redis.entity.User;
import com.redis.mapper.UserMapper;
import com.redis.service.UserService;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Override
    public List<User> queryUser(String name) {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getName, name);
        return this.baseMapper.selectList(wrapper);
    }
}

controller

我们先写一个一般业务写法

redisMap 存储每个key成功获取缓存次数的情况
mysqlMap 存储每个key访问数据库次数的情况

clear接口 —— 情况Map结果集,getMap接口 —— 查看Map结果集,query接口 用户查询接口,以name 字段为例简单模拟场景

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.redis.entity.User;
import com.redis.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/user/api")
public class UserController {

    // 存储每个key走了多少次缓存
    private ConcurrentHashMap<String, Integer> redisMap = new ConcurrentHashMap<>();
    // 存储每个key走了多少次数据库
    private ConcurrentHashMap<String, Integer> mysqlMap = new ConcurrentHashMap<>();

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    private UserService userService;

    @GetMapping("clear")
    public void clear() {
        this.redisMap.clear();
        this.mysqlMap.clear();
    }

    @GetMapping("getMap")
    public Object getMap() {
        JSONObject json = new JSONObject();
        json.set("redisMap", this.redisMap);
        json.set("mysqlMap", this.mysqlMap);
        return json;
    }

    @GetMapping("query")
    public Object queryUser(@RequestParam("name") String key) {
        String cache = redisTemplate.opsForValue().get(key);

        // 是否存在缓存
        if (cache != null) {
            this.redisMap.put(key, (this.redisMap.get(key) == null ? 0 : this.redisMap.get(key)) + 1);
            return JSONUtil.parse(cache);
        } else {
            // 不存在缓存, 查询数据库
            this.mysqlMap.put(key, (this.mysqlMap.get(key) == null ? 0 : this.mysqlMap.get(key)) + 1);
            List<User> users = userService.queryUser(key);

            // 有数据, 丢入缓存
            if (users != null && users.size() > 0) {
                redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(users), 60 * 50, TimeUnit.SECONDS);
            }
            return users;
        }
    }
}

在这里插入图片描述

三、正式开始(重点)

普通测试

我们用 postman 先普通测试一下接口,测试一个存在的数据,正常显示

在这里插入图片描述

jmeter压测

接下来进入正题,用 jmeter 测试不存在的数据,并发1000个线程,循环10次,设置随机变量,控制在 全栈小袁001 ~ 全栈小袁100 之间

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

执行jemter,接着使用postmant查看一下结果,从结果可以看到全部访问了数据库

如果并发非常大,是不是会给数据库造成压力,甚至导致数据库宕奔溃?

在这里插入图片描述

四、空值缓存(方案一)

在这里插入图片描述

加入这一段代码

在这里插入图片描述

// 缓存空值或者默认值
JSONObject json = new JSONObject();
json.set("res", null);
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(json), 10, TimeUnit.SECONDS);
return json;

重新启动项目,进行jemeter压测,测试结果可以看到大部分走了redis缓存的默认值,访问mysql的次数大幅度降低了

在这里插入图片描述

在这里插入图片描述

五、白名单(方案二)

方案一中 对所有的非法key都做了缓存,而且都是同样的value,这样的操作造成了数据冗余,而且key的数量非常多

我们可以完善一下,利用redis中的 set 集合,设置 黑名单列表

在这里插入图片描述

@GetMapping("query")
public Object queryUser(@RequestParam("name") String key) {
    // 是否在黑名单中
    if (redisTemplate.opsForSet().isMember("NullSet", key)) {
        this.redisMap.put(key, (this.redisMap.get(key) == null ? 0 : this.redisMap.get(key)) + 1);
        JSONObject json = new JSONObject();
        json.set("res", null);
        // 返回空值
        return json;
    }

    String cache = redisTemplate.opsForValue().get(key);
    if (cache != null) {
        // 存在缓存
        return JSONUtil.parse(cache);
    }else {
        // 查询数据库
        this.mysqlMap.put(key, (this.mysqlMap.get(key) == null ? 0 : this.mysqlMap.get(key)) + 1);
        List<User> users = userService.queryUser(key);

        if (users != null && users.size() > 0) {
            // 有数据, 丢入缓存
            redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(users), 60 * 5, TimeUnit.SECONDS);
            return users;
        }else {
            // 无数据, 加入黑名单列表
            redisTemplate.opsForSet().add("NullSet", key);
            JSONObject json = new JSONObject();
            json.set("res", null);
            // 返回空值
            return json;
        }
    }
}

启动进行jmeter压测,测试结果可以也是大部分走了redis,减少mysql访问的次数,同时set集合也方便管理,减少数据的冗余

在这里插入图片描述

在这里插入图片描述


六、布隆过滤器(方案三)

无论是 方案一 还是 方案二,数据量大起来对空间消耗还是非常大的,所以就有了第三种方案—— 布隆过滤器

布隆过滤器 我就不不详细介绍了,网上也有很多详细的解释,这里我就大概说一下就行

(1)首先,布隆过滤器的结构是由 二进制 0 1 组成的数组,0是不存在,1是存在

(2)拥有 k 个独立的 哈希函数映射 ,通过要判断的字符分别计算出 哈希值 计算出下标位置,当 k 个下标获取到的值都为 1 时,则认为当前字符存在,否则不存在

(3)优点:速度非常快,占用空间极少,操作的是机器底层二进制向量;缺点:一是存在 误判,不同的字符会出现相同的哈希值,二是 删除困难

(4)这里插一点:上面说到删除困难,于是衍生出了 布谷过滤器 ,可以删除操作,有兴趣的可以自己去看看

在这里插入图片描述

这里我采用的是 hutool 工具库已经封装好的布隆过滤器,当然你也可以使用其他的工具库或者自己封装一个,原理都是一样的

import cn.hutool.bloomfilter.BloomFilter;
import cn.hutool.bloomfilter.BloomFilterUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.redis.entity.User;
import com.redis.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/user/api")
public class UserController {

    // 存储每个key走了多少次布隆过滤器
    private ConcurrentHashMap<String, Integer> bloomMap = new ConcurrentHashMap<>();
    // 存储每个key走了多少次数据库
    private ConcurrentHashMap<String, Integer> mysqlMap = new ConcurrentHashMap<>();

    // 布隆过滤器, 设置大约1000个数据
    private BloomFilter bloomFilter = BloomFilterUtil.createBitMap(1000);

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    private UserService userService;

    @GetMapping("clear")
    public void clear() {
        this.bloomMap.clear();
        this.mysqlMap.clear();
    }

    @GetMapping("getMap")
    public Object getMap() {
        JSONObject json = new JSONObject();
        json.set("bloomMap", this.bloomMap);
        json.set("mysqlMap", this.mysqlMap);
        return json;
    }

    @GetMapping("query")
    public Object queryUser(@RequestParam("name") String key) {
        // 布隆过滤器过滤, 判断是否出现在过滤器里
        if (bloomFilter.contains(key)) {
            this.bloomMap.put(key, (this.bloomMap.get(key) == null ? 0 : this.bloomMap.get(key)) + 1);
            JSONObject json = new JSONObject();
            json.set("res", null);
            // 返回空值
            return json;
        }

        String cache = redisTemplate.opsForValue().get(key);
        if (cache != null) {
            // 返回缓存
            return JSONUtil.parse(cache);
        }else {
            // 查询数据库
            this.mysqlMap.put(key, (this.mysqlMap.get(key) == null ? 0 : this.mysqlMap.get(key)) + 1);
            List<User> users = userService.queryUser(key);

            if (users != null && users.size() > 0) {
                // 有数据, 丢入缓存
                redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(users), 60 * 5, TimeUnit.SECONDS);
                return users;
            }else {
                // 无数据, 添加到过滤器里
                bloomFilter.add(key);
                JSONObject json = new JSONObject();
                json.set("res", null);
                // 返回空值
                return json;
            }
        }
    }
}

重新启动,压测,测试结果可以看出大部分都被 布隆过滤器 给过滤掉了

在这里插入图片描述

总结

好了,整篇文章到这里就结束了,做个总结,也是我的个人习惯之一

问题产生

客户端发送请求获取数据的时候,在redis中未命中,接着查询数据库也未命中,如果这时候大量请求这些不存在的数据,那么就会给数据库造成一定的压力甚至宕机,这就是 缓存穿透 问题的产生

解决方案

  • 缓存空值或者默认值
    • 对不存在的数据key构建缓存,下次请求时直接返回缓存,对于空间占用问题可以设置缓存时间短一些(5s/10s/20s)
  • 黑名单
    • 利用Set集合,设置空值黑名单列表,遇到存在黑名单中的非法请求直接返回空值或默认值,同时可以自定义操作set集合数据
  • 布隆过滤器
    • 0 1 组长的二进制向量,空间占用极小,速度也非常快,但删除比较困难,且存在一定的误判(不同对象可能存在相同哈希值)

选择

我自己在项目中一般都是用第一种方案,方便刷新,有可能这次这个查询是不存在数据,下次就存在了,那二三方案就比较不好实现刷新

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

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

相关文章

Drain3改进的日志解析方法+代码

这本项目继承于https://github.com/logpai/Drain3 在此项目的基础上进行了改进&#xff0c;目前代码在PR阶段&#xff0c;感兴趣的可以从PR上拉取&#xff1a; 前言&#xff1a; 本项目继承于https://github.com/logpai/Drain3 在此项目的基础上进行了改进&#xff0c;目前代码…

【nv12 格式转换】不同图像数据格式之间转换代码实操

文章目录1 问题先行2 nv12介绍2.1 YUV格式2.2 NV12排布3 不同数据格式之间转换实操4 参考链接1 问题先行 nv12是什么格式&#xff1f;和常见的rgb/bgr有什么关系吗&#xff1f;他们之间能互相转换吗&#xff1f;如何读取一张图片&#xff0c;然后把图片转换成nv12格式&#xf…

liunx Reids哨兵模式+1主三从部署6.2.x

1.下载 wget https://download.redis.io/releases/redis-6.2.6.tar.gz2.解压 tar xzf redis-6.2.6.tar.gz 3.安装redis cd redis-6.2.6make && make install redis安装异常 安装gcc yum install gcc 检查gcc环境是否安装好rpm -qa|grep gcc 最后在安装redis m…

情人节适合送礼的数码好物有哪些?心意满满的数码好物清单

2023的情人节就快到了&#xff0c;各大数码品牌都已经摩拳擦掌。那么&#xff0c;情人节适合送礼的数码好物有哪些&#xff1f;下面&#xff0c;我来给大家推荐几款心意满满的数码好物&#xff0c;一起来看看吧。 一、蓝牙耳机 推荐产品&#xff1a;南卡小音舱 推荐理由&…

u盘为什么被写保护?u盘数据写保护如何恢复

u盘作为目前主流的数据存储辅助工具&#xff0c;能够存放各种重要数据。虽然u盘体积小巧使用方便&#xff0c;但是使用环节中也会存在各类问题&#xff0c;例如提示u盘写保护的情况。那么出现这种情况的原因是什么&#xff0c;如何解决u盘写保护问题以及写保护的u盘如何恢复数据…

Spring Cloud Gateway断言及过滤器使用

目录 1. 引入Spring Cloud Gateway 2. 核心概念 3. 工作原理 4. 配置路由断言工厂和网关过滤器工厂 5. 路由断言工厂 ​5.1 断言某个时间之后 5.2 断言某个时间之前 5.3 断言某个时间之间 5.4 根据cookie值断言 5.5 根据请求的标头断言 5.6 根据主机名来断言 5.7 根…

Java集合常见面试题(五)

Map 接口 ConcurrentHashMap 的实现原理 JDK 1.7 JDK1.7中的ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成&#xff0c;即ConcurrentHashMap 把哈希桶切分成小数组&#xff08;Segment &#xff09;&#xff0c;每个小数组有 n 个 HashEntry 组成。 其…

贴息贷款政策下,高校建立大数据实验室新思路

高校实验室建设总目标是搭建一站式教学服务平台&#xff0c;一站式教学服务平台概念是在深刻理解高校学科建设及存在的若干问题上提出。围绕着学科建设的各个方面&#xff0c;从专业开设、课程设置、师资培养、教学资源、实验环境、学生实训实习及就业创业等环节&#xff0c;提…

Hive UDF开发

Hive中&#xff0c;除了提供丰富的内置函数&#xff08;见Hive函数大全–完整版(二)&#xff09;之外&#xff0c;还允许用户使用Java开发自定义的UDF函数。 开发自定义UDF函数有两种方式&#xff0c;一个是继承org.apache.hadoop.hive.ql.exec.UDF&#xff0c;另一个是继承or…

windows系统下安装伪分布式Hadoop3.x

1.下载 1.1下载Hadoop3.1.3 官网地址&#xff1a;https://archive.apache.org/dist/hadoop/common/hadoop-3.1.3/ 选择hadoop-3.1.3.tar.gz 1.2下载工具集winutils 由于Hadoop不直接支持Windows系统&#xff0c;因此需要使用工具集winutils进行支持。 下载网址&#xff1a;…

can‘t be used as a mixin because it extends a class other than ‘Object‘.

程序员如果敲一会就停半天&#xff0c;抱着一杯茶&#xff0c;表情拧巴&#xff0c;那才是在编程 Flutter 项目开发指导 从基础入门到精通使用目录 前言 - 基础关键字 class&#xff1a;声明一个类&#xff0c;提供具体的成员变量和方法实现。abstract class&#xff1a;声明一…

27.日志技术、XML

目录 一.日志技术 1.1 什么是日志 1.2 目前记录日志的方案 1.3 日志技术的优势 1.4 日志技术体系 1.5 Logback框架 1.5.1 下载地址 1.5.2 模块组成 1.5.3 Logback的使用 二.XML 2.1 XML概述 2.2 XML的特点 2.3 XML文件的使用场景 2.4 XMl文件的创建 2.5 XML的语法…

江瀚新材登陆上交所主板:募资约24亿元,甘书官父子为实际控制人

1月31日&#xff0c;湖北江瀚新材料股份有限公司&#xff08;下称“江瀚新材”&#xff0c;SH:603281&#xff09;在上海证券交易所主板上市。本次上市&#xff0c;江瀚新材公开发行股票66,666,667股&#xff0c;发行价格为35.59元/股&#xff0c;发行市盈率为14.80倍。 按发行…

更高性能表现、更低资源占用,高精度计算数据类型 DecimalV3 揭秘

数值运算是数据库中十分常见的需求&#xff0c;例如计算数量、重量、价格等&#xff0c;为了适应多样化运算场景&#xff0c;数据库系统通常支持精准的数字类型和近似的数字类型&#xff0c;当我们需要精确地表示小数并计算小数时&#xff0c;通常会考虑使用 Decimal 数据类型。…

低代码编程核心技术概念

从技术概念来讲&#xff0c;低代码编程跟通用编程是完全一致的。要利用好低代码编程工具&#xff0c;至少要掌握下面的技术概念。低代码编程核心技术概念一、数据结构这里的数据结构&#xff0c;指一般意义上的数据表和数据字段。 类似于数据库中的表及字段的概念&#xff0c;也…

蓝桥杯2022 A组 python

蓝桥杯2022 A组 python A组相对于B组就五道题不一样 第一题&#xff1a;裁纸刀 就先把四个边剪一下&#xff0c;然后先行后列&#xff0c;蛮简单的 # 20行 横着19刀&#xff0c;竖着21*20 print(1921*204)第三题&#xff1a;质因数个数 这题我感觉就是跟一个约数个数的模板一…

【python】Twisted网络编程

Twisted什么是Twisted&#xff1f;为什么使用twisted&#xff1f;Twisted 写TCP通信基本实例- TCP服务端- TCP客户端Twisted的Deferred机制Why Deferred?Deferred TCP-ECHO客户端实现- TCP client为例&#xff0c;什么是Twisted&#xff1f; Twisted是一种非阻塞的网络服务器…

useEffect 依赖项为对象或数组时,引发不断重渲染问题的原因及解法

转载自 https://www.izhaoo.com/2021/11/01/useEffect-object-dependent/背景今天封了个轮子&#xff0c;对组件传入参数生成对应的动画实例&#xff0c;当入参变化时重新渲染新实例。自然而然想到的就是 useEffect 监听 props 依赖了&#xff0c;如&#xff1a;useEffect(() &…

ESD器件(TVS)选型考虑

参考&#xff1a;TOSHIBA《Basics of ESD Protection (TVS) Diodes》①VRWM: Working peak reverse voltage工作峰值反向电压&#xff1a;ESD保护二极管显示非常高的阻抗。高于这个电压&#xff0c;会通过指定大小漏电流。设计的时候&#xff0c;信号的最大出现电压要在这个电压…

Java基础漏洞(六)

巩固Java基础&#xff0c;基于韩顺平老师的课程&#xff0c;Java的封装、继承、多态、super()、this()、重载与重写的区别......&#xff0c;下面我们一点点来巩固练习。 &#xff08;1&#xff09;继承 继承细节1&#xff1a;子类无法继承父类的私有 图1 我们定义一个父类f…