redis缓存穿透、案例

1、缓存穿透是什么

        缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。

  • 其实:就是黑客利用不存在的数据(如:数据库id自增的情况下,黑客传递id为负数的参数),拼接在原接口上,对服务器进行大量的请求,从而加大数据库压力直至服务器崩溃

2、缓存穿透案例

需要使用的技术栈:

  • mysql
  • redis
  • springboot
  • Jmeter

2.1、模拟场景

  1. 先把mysql最大访问量设置为3
  2. 编写java接口获取商品详情数据,当redis有数据直接从redis获取,当redis无数据,从mysql获取,同时重新设置缓存
  3. 使用jmeter压测工具,同时发送id小于0的请求(非正常请求),所有的请求都会直接打到mysql,
  4. 此时,mysql最大访问量只有3,很快mysql就会报:too many connection,从而mysql无法正常工作,导致后端服务器无法获取数据直至无法正常运行
  5. 后端接口响应时间最好调至10s,当一个接口10s后未返回数据,证明服务器已崩溃

2.2、把mysql的最大访问量设置为3

目的:模拟当恶意用户使用大量非法参数的请求时,能快速增大mysql的压力,从而实现服务器性能下降

步骤如下:

<1>在连接mysql的客户端中(我使用的是IDEA),执行SQL语句:

-- 设置mysql最大连接数为3,当超出3时,mysql会报错too many connection
SET GLOBAL max_connections=3;

 <2>检查mysql当前最大连接数

SHOW VARIABLES LIKE 'max_connections';

如下:(注意:上述设置只是临时的,当mysql重启后,最大连接数会重置至默认状态)

2.3、初始化测试表

<1>DDL

create table goods
(
    id   int auto_increment  primary key,
    name     varchar(100) null,
    price    double       null,
    comments varchar(100) null
)
comment '商品表';

<2>添加表数据

INSERT INTO goods (id, name, price, comments) VALUES (1, '小米14', 4999, '【买即送199好礼 24期免息】Xiaomi 13Pro新品手机徕卡影像/2K屏/骁龙8 Gen2官方旗舰店官网正品小米13pro');
INSERT INTO goods (id, name, price, comments) VALUES (2, '苹果14', 5778, '顺丰速发【24期免息】iPhone/苹果14 Pro/Pro Max 5G新款手机官方旗舰店国行正品plus官网13直降的分期12');
INSERT INTO goods (id, name, price, comments) VALUES (3, '华为Mate50', 5449, '现货Huawei/华为Mate50Pro 手机原装正品旗舰华为mate50pro鸿蒙');

2.4、编写测试代码

<1>目录结构如下 

<2>所用依赖如下

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
	<version>2.2.12.RELEASE</version>
</dependency>
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus</artifactId>
	<version>3.3.1</version>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
</dependency>
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid</artifactId>
	<version>1.2.5</version>
</dependency>

<!-- redis所需的连接池 -->
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
</dependency>

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
	<version>1.2.75</version>
</dependency>

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

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>

<3>application.properties

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/taobao
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.redis.host=192.168.101.8
spring.redis.port=6379
spring.redis.timeout=
#默认使用第一个数据库,一共16个
spring.redis.database=0
#关闭超时时间
spring.redis.lettuce.shutdown-timeout=18000
#连接池最大的连接数(使用负数表示无限制)
spring.redis.lettuce.pool.max-active=8
#最大阻塞等待时间(使用负数表示无限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
#设置过期时间为10s
spring.mvc.async.request-timeout=1000

<4>redis配置类

package com.shuizhu.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.net.UnknownHostException;

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        // 创建模板
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 设置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 设置序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer =
                new GenericJackson2JsonRedisSerializer();
        // key和 hashKey采用 string序列化
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        // value和 hashValue采用 JSON序列化
        redisTemplate.setValueSerializer(jsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jsonRedisSerializer);
        return redisTemplate;
    }
}

 <5>数据源配置类

package com.shuizhu.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;

@Configuration
@MapperScan(basePackages = "com.shuizhu.dao", sqlSessionFactoryRef = "db1SqlSessionFactory")
public class DataSourceConfig {

    @Primary
    @Bean(name = "db1DataSource")
    @ConfigurationProperties("spring.datasource")
    public DataSource db1DataSource() {
        return DataSourceBuilder.create().type(DruidDataSource.class).build();
    }

    @Bean(name = "db1SqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        //bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResource("classpath:mapper/db1/Demo.xml"));
        PathMatchingResourcePatternResolver resource = new PathMatchingResourcePatternResolver();
        bean.setMapperLocations(resource.getResources("classpath:mybatis/*.xml"));
        return bean.getObject();
    }
}

<6>商品表实体类代码

package com.shuizhu.domain;
import lombok.Data;

@Data
public class Goods {
    private Integer id;
    private String name;
    private Double price;
    private String comments;
}

<7>dao层代码

package com.shuizhu.dao;

import com.shuizhu.domain.Goods;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface IGoodsMapper {
    //查询所有商品
    List<Goods> getAll();

    //根据商品id获取对应商品
    Goods getById(Integer id);
}

<8>dao映射文件xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shuizhu.dao.IGoodsMapper">

    <select id="getAll" resultType="com.shuizhu.domain.Goods">
        select
            id,name,price,comments
        from goods
    </select>

    <select id="getById" resultType="com.shuizhu.domain.Goods">
        select
            id,name,price,comments
        from goods
        where id = #{id}
    </select>

</mapper>

<9>controller层测试类

这里模拟的是正常的业务逻辑(未对缓存穿透做限制)

package com.shuizhu.controller;

import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.shuizhu.dao.IGoodsMapper;
import com.shuizhu.domain.Goods;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@Log4j2
public class TestController {

    @Autowired
    IGoodsMapper dao;
    @Autowired
    RedisTemplate redisTemplate;

    //存储商品集合的key
    public static final String GOODS_KEY = "goods_key";

    //当缓存不存在时,重新设置商品缓存
    @RequestMapping("/goods/setCacheForGoods")
    public String setCacheForGoods(){
        Long i = redisTemplate.opsForList().leftPushAll(GOODS_KEY, dao.getAll());
        return String.format("本次redis更新:%d条数据", i);
    }

    //模拟根据商品ID获取商品数据
    @RequestMapping("/goods/{id}")
    public Goods getById(@PathVariable("id") int id) {
        log.warn("当前访问的商品id为:{}", id);
        //1、访问redis获取所有数据    0,-1这个区间表示获取所有的数据
        List<Goods> range = redisTemplate.opsForList().range(GOODS_KEY, 0, -1);
        //2、判断range是否为null,为null则表示缓存中没有,需要从redis获取
        if (ObjectUtils.isEmpty(range)) {
            //3、读取数据库,更新缓存
            setCacheForGoods();
            List<Goods> nowGoods = dao.getAll().stream().filter(goods -> goods.getId() == id).collect(Collectors.toList());
            //4、当该id查询不到数据时,返回null,存在数据,直接返回该数据
            return ObjectUtils.isEmpty(nowGoods) ? null : nowGoods.get(0);
        }
        //5、rangenull,则在range中,根据当前id获取对应商品
        List<Goods> goodsList = range.stream().filter(goods -> goods.getId() == id).collect(Collectors.toList());
        //6、判断当前goodsList是否存在
        if (ObjectUtils.isNotEmpty(goodsList)) {
            //7、存在则直接返回
            return goodsList.get(0);
        }
        //8、缓存中有商品数据,但是该id对应的商品数据不存在,则需要去查询数据库,看是否存在
        Goods byId = dao.getById(id);
        if (ObjectUtils.isEmpty(byId)) {
            //9、该id没有对应的商品,证明是恶意id,返回null
            return null;
        }
        //10、数据库存在数据,证明是新的商品,先更新缓存,再返回数据
        setCacheForGoods();
        return byId;
    }
}

注意:

  • http://localhost:8080/goods/setCacheForGoods 请求:设置redis缓存所有的商品数据
  • http://localhost:8080/goods/商品id 请求:获取id下的详细信息,如传递3,则获取ID为3下的所有数据,若传递-1,则获取不到数据,需要查询数据库

2.5、jmeter压测

<1>使用浏览器访问接口

 试下id为-1的请求:


一切没有问题,下面我们开始使用压测工具,发送大量的非法请求,看下服务器最终状态: 

<2>简单配置下jmeter: 

<3>开始测试

点击开始:

 我们直接去看idea控制台打印内容,如下:

这时,我们直接使用浏览器,发送正常参数的请求,看下效果:

发现: 

访问正常参数的请求时,服务器无法正常响应! 

3、缓存穿透解决方案

大致3种:

方案1:对于mysql不存在的数据,redis直接缓存null,并设置过期时间(短时间)

缺点:随着不存在的数据缓存越多,redis内存也就越大

方案2:添加访问黑名单,当某个ip出现多次非法请求时,直接拉黑

缺点:攻击者可能会一直更换ip

方案3:使用布隆过滤器

  • 布隆过滤器其实就是一个白名单/黑名单的拦截器

白名单:把数据库查询的数据,同步到redis时,再把数据同步至布隆过滤器

缺点:mysql数据需要同步两份,一份到redis,一份到布隆过滤器

黑名单:初始化一个布隆过滤器,当存在非法请求时,把请求参数加入到黑名单,下次不允许查询redis合mysql

缺点:初始化的布隆过滤器中是没有数据的,也就意味着没有黑名单

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

睡竹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值