> 文章列表 > redis缓存穿透、案例

redis缓存穿透、案例

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 {@Beanpublic 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">selectid,name,price,commentsfrom goods</select><select id="getById" resultType="com.shuizhu.domain.Goods">selectid,name,price,commentsfrom goodswhere 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 {@AutowiredIGoodsMapper dao;@AutowiredRedisTemplate redisTemplate;//存储商品集合的keypublic 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,返回nullreturn 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

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