缓存击穿
概述
请求去查询一条记录,先查 Redis 无,后查 MySQL 无,都查不到该条记录。但请求每次都会打到数据库上面去,导致后台数据库压力暴增,这种现象我们称为缓存穿透。
解决方案
比较差的情况:恶意攻击;
缓存穿透 → 空对象缓存 → BloomFilter
方案一:空对象缓存或者缺省值
- 第一种解决方案:回写增强
如果发生了缓存穿透,我们可以针对要查询的数据,在 Redis 里存一个和业务部门商量后确定的缺省值(比如0,负数,defaultNull 等)。
比如,键 uid:abcdxxx,值 defaultNull 作为本案例的 key 和 value。先去 Redis 查键 uid:abcdxxx 没有,再去 MySQL 查没有获得,这就发生了一次穿透现象。
增强回写机制,MySQL 查不到也让 Redis 存入刚刚查不到的 Key 并保护 MySQL。第一次来查询 uid:abcdxxx,Redis 和 MySQL 都没有,返回 null 给调用者,但是增强回写后第二次来查 uid:abcdxxx,此时 Redis 就有值了。可以直接从 Redis 中读取 default 缺省值返回给业务应用程序,避免了把大量请求发送给 MySQL 处理,打爆 MySQL。
但是,此方法顶不住黑客的恶意攻击,有缺陷......,只能解决 Key 相同的情况- 黑客会对你的系统进行攻击,拿一个不存在的 id 去查询数据,会产生大量的请求到数据库去查询。可能会导致你的数据库由于压力过大而宕掉。
Key 相同打你系统:第一次打到 MySQL,空对象缓存后第二次就返回 defaultNull 缺省值,避免 MySQL 被攻击,不用再到数据库中走一圈;
Key 不同打你系统:由于存在空对象缓存和缓存回写(具体看自己业务),Redis 中的无关要紧的 Key 也会越写越多(记得设置 Redis 过期时间)
方案二:Google 布隆过滤器 Guava 解决缓存穿透
Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们可以直接使用 Guava 布隆过滤器。
Guava's BloomFilter 源码出处 https://github.com/google/guava/blob/master/guava/src/com/google/common/hash/BloomFilter.java (opens in a new tab)
案例:白名单过滤器
- 白名单架构说明
-
让布隆过滤器作白名单使用:白名单里面有的才让通过,没有直接返回。但是存在误判,由于误判率很小,1% 的打到 MySQL,可以接受。
-
使用注意:所有 Key 都需要往 Redis 和 BloomFilter 里面放入。
-
误判问题,但是概率小可以接收,不能从布隆过滤器删除
-
全部合法的 Key 都需要放入 Guava 版布隆过滤器 + Redis 里面,不然数据就是返回 null。
-
代码实战
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.matrixlab.redis7</groupId>
<artifactId>redis7_study</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.10</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
</properties>
<dependencies>
<!--guava Google 开源的 Guava 中自带的布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>
<!--lettuce-->
<!--<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.2.1.RELEASE</version>
</dependency>-->
<!--SpringBoot与Redis整合依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--Mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.2.3</version>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0.2</version>
</dependency>
<!--通用Mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!--通用基础配置junit/devtools/test/log4j/lombok/-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
server.port=7777
spring.application.name=redis7_study
# ========================logging=====================
logging.level.root=info
logging.level.dev.matrixlab.redis7=info
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n
logging.file.name=D:/mylogs2023/redis7_study.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n
# ========================swagger=====================
spring.swagger2.enabled=true
#在springboot2.6.X结合swagger2.9.X会提示documentationPluginsBootstrapper空指针异常,
#原因是在springboot2.6.X中将SpringMVC默认路径匹配策略从AntPathMatcher更改为PathPatternParser,
# 导致出错,解决办法是matching-strategy切换回之前ant_path_matcher
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
# ========================redis单机=====================
spring.redis.database=0
# 修改为自己真实IP
spring.redis.host=192.168.111.185
spring.redis.port=6379
spring.redis.password=111111
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
# ========================alibaba.druid=====================
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/bigdata?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.druid.test-while-idle=false
# ========================mybatis===================
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=dev.matrixlab.redis7.entities
# ========================redis集群=====================
#spring.redis.password=111111
## 获取失败 最大重定向次数
#spring.redis.cluster.max-redirects=3
#spring.redis.lettuce.pool.max-active=8
#spring.redis.lettuce.pool.max-wait=-1ms
#spring.redis.lettuce.pool.max-idle=8
#spring.redis.lettuce.pool.min-idle=0
##支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
#spring.redis.lettuce.cluster.refresh.adaptive=true
##定时刷新
#spring.redis.lettuce.cluster.refresh.period=2000
#spring.redis.cluster.nodes=192.168.111.185:6381,192.168.111.185:6382,192.168.111.172:6383,192.168.111.172:6384,192.168.111.184:6385,192.168.111.184:6386
package dev.matrixlab.redis7;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@MapperScan("dev.matrixlab.redis7.mapper")
//import tk.mybatis.spring.annotation.MapperScan;
public class Redis7Study7777 {
public static void main(String[] args) {
SpringApplication.run(Redis7Study7777.class,args);
}
}
// 测试案例
@Test
public void testGuavaWithBloomFilter() {
// 创建布隆过滤器对象
BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 100);
// 判断指定元素是否存在
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
// 将元素添加进布隆过滤器
filter.put(1);
filter.put(2);
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
}
package dev.matrixlab.redis7.controller;
import dev.matrixlab.redis7.service.GuavaBloomFilterService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@Api(tags = "google工具Guava处理布隆过滤器")
@RestController
@Slf4j
public class GuavaBloomFilterController {
@Resource
private GuavaBloomFilterService guavaBloomFilterService;
@ApiOperation("guava布隆过滤器插入100万样本数据并额外10W测试是否存在")
@RequestMapping(value = "/guavafilter",method = RequestMethod.GET)
public void guavaBloomFilter() {
guavaBloomFilterService.guavaBloomFilter();
}
}
package dev.matrixlab.redis7.service;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@Slf4j
public class GuavaBloomFilterService {
public static final int _1W = 10000;
//布隆过滤器里预计要插入多少数据
public static int size = 100 * _1W;
//误判率,它越小误判的个数也就越少(思考,是不是可以设置的无限小,没有误判岂不更好)
//fpp the desired false positive probability
public static double fpp = 0.03;
// 构建布隆过滤器
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size,fpp);
public void guavaBloomFilter() {
//1 先往布隆过滤器里面插入100万的样本数据
for (int i = 1; i <=size; i++) {
bloomFilter.put(i);
}
//故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里
List<Integer> list = new ArrayList<>(10 * _1W);
for (int i = size + 1; i <= size + (10 * _1W); i++) {
if (bloomFilter.mightContain(i)) {
log.info("被误判了:{}",i);
list.add(i);
}
}
log.info("误判的总数量::{}",list.size());
}
}
当我们修改 fpp
时,不同的 fpp
对应的 Guava
过滤器的 hash
函数的数量不同(可以使用 debug
模式,追踪 numBits
和 numHashFunctions
)
黑名单使用
让布隆过滤器作黑名单使用
抖音防止推荐重复视频;饿了么防止推荐重复优惠券,推荐过的尽量别在重复推荐。推荐时先去布隆过滤器判断。
- 存在:说明在黑名单里面,已经推荐过不再重复推荐。
- 不存在:新视频推荐给用户并更新进布隆过滤器,防止下次重复推荐。