上次说了redis穿透的问题,现在我们来解决一下吧,首先redis穿透是因为一个key在redis中未查询到,而频繁查询mysql导致mysql崩溃的问题。解决方案一是可以在未查询到的redis中添加一个空的key但这种做法还是不太妥当,解决方案二使用集合存储进行判断该key是否存在,而不是频繁查询mysql导致mysql崩溃,在使用map集合时,我们得想到map集合如果存储的数据量过多占用的资源会很大,这个也是不妥当的,接下来我们可以使用布隆过滤器:
布隆过滤器
布隆过滤器它采用的数据结构是位图的形式,他是一个有序的数组,只有两个值,0 和 1。0代表不存在,1代表存在。
首先我们知道了布隆过滤器的存储结构,我们还需要一个映射关系,我们需要知道某个元素在哪个位置上,然后在去看这个位置上是0还是1是否存在,这个使用我们可以使用哈希函数,哈希函数的好处,第一是哈希函数无论输入值的长度是多少,得到的输出值长度是固定的,通过不同的值去映射到布隆过滤器的位置即可。但是我们在计算布隆过滤器位置的时候容易产生哈希碰撞,哈希碰撞产生的原因就是在计算哈希值的时候可能计算出来的两个不同元素但他们所映射的位图位置相同,其中A元素在该位图的位置是存在的,B元素实际上是不存在的,但是计算过程中计算的位置跟A元素的位置相同,它的值为1显示存在,这种情况就叫做假阳性。
在布隆过滤器中如果布隆过滤器判断元素在集合中存在,不一定存在,如果布隆过滤器判断不存在,一定不存在,因为布隆过滤器有假阳性的情况出现。
下面我们开始使用一下布隆过滤器吧:
首先导入pom,这边有两个布隆过滤器的工具:
<!--布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
<!--带计数器的布隆过滤器-->
<dependency>
<groupId>com.baqend</groupId>
<artifactId>bloom-filter</artifactId>
<version>1.0.7</version>
</dependency>
代码:
@RestController
public class BloomFilterController {
@Autowired
private RedisTestService redisTestService;
@Autowired
private RedisUtil redisUtil;
@RequestMapping("test")
public String test(){
//初始化数据到布隆过滤器
long start = System.currentTimeMillis();
BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 5000000, 0.03);
for (RedisTest redisTest : redisTestService.allList()) {
//将数据添加到布隆过滤器中 500万条
bf.put(redisTest.getName());
}
long end = System.currentTimeMillis();
System.out.println("初始化共耗时" + (end - start) + "毫秒");
//创建一千个线程同时访问
/**
* 说明CyclicBarrier中参数为有多少个线程参与,如果是1000个那么我就等待1000个线程准备后直接执行。
*/
int threadNum = 10000;
CyclicBarrier barrier = new CyclicBarrier(threadNum);
for (int i = 0; i < 10000; i++) {
new TaskThread(barrier,bf).start();
}
return "ok";
}
public class TaskThread extends Thread {
CyclicBarrier barrier;
BloomFilter<String> bf;
public TaskThread(CyclicBarrier barrier, BloomFilter<String> bf ) {
this.barrier = barrier;
this.bf = bf;
}
@Override
public void run() {
try {
System.out.println(getName() + "正在等待中");
barrier.await();
String uid = UUID.randomUUID().toString();
//存在
if(bf.mightContain(uid)){
//redis查询成功
if (redisUtil.get(uid) != null) {
System.out.println("缓存命中----------------"+uid);
return;
}else{
System.out.println("redis查询失败-----------"+uid);
//redis查询失败查询数据库
if(redisTestService.getRedisTest(uid) !=null){
System.out.println("数据库查询成功,uid============="+uid);
}else{
System.out.println("数据库查询失败,uid============="+uid);
}
}
}else{
//不存在
System.out.println("布隆过滤器查询失败");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
@Table(name = "redis_test")
public class RedisTest {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
思路:首先我在数据库中准备了500万条数据,在代码中先将500万条数据put到布隆过滤器中然后开启1000个线程同时访问布隆过滤器,布隆过滤器判断该元素存在的时候:redis查询该key是否存在,不存在则直接查询mysql,如果布隆过滤器判断该元素不存在时可以直接返回一个空或者其它业务,我们确定的时候布隆过滤器里面的元素都是来自于mysql的数据,所以布隆过滤器查询失败不需要再次查询数据库。这样我们可以很好的去解决redis穿透的问题。如何删除一个元素呢,在位图中删除一个元素是很麻烦的,因此我们可以使用计数器布隆过滤器
@RestController
public class CountingBloomController {
@Autowired
private RedisTestService redisTestService;
@Autowired
private RedisUtil redisUtil;
@RequestMapping("CountingBloomTest")
public String test() {
//初始化数据到布隆过滤器
long start = System.currentTimeMillis();
CountingBloomFilter<String> bf = new FilterBuilder(5000000, 0.03).buildCountingBloomFilter();
for (RedisTest redisTest : redisTestService.allList()) {
bf.add(redisTest.getName());
}
long end = System.currentTimeMillis();
System.out.println("初始化共耗时" + (end - start) + "毫秒");
new TaskThread( bf).start();
return "ok";
}
public class TaskThread extends Thread {
CountingBloomFilter<String> bf;
public TaskThread( CountingBloomFilter<String> bf) {
this.bf = bf;
}
@Override
public void run() {
try {
System.out.println(getName() + "正在等待中");
// barrier.await();
String uid = "77f84dd2-281f-419d-9864-ebbfdde1ded2";
//存在
if (bf.contains(uid)) {
//redis查询成功
if (redisUtil.get(uid) != null) {
System.out.println("缓存命中----------------" + uid);
//删除
bf.remove(uid);
System.out.println("进行删除--------------------" + uid);
if (bf.contains(uid)) {
System.out.println("删除失败");
}else{
System.out.println("删除成功");
}
return;
} else {
System.out.println("redis查询失败-----------" + uid);
//redis查询失败查询数据库
if (redisTestService.getRedisTest(uid) != null) {
System.out.println("数据库查询成功,uid=============" + uid);
} else {
System.out.println("数据库查询失败,uid=============" + uid);
}
}
} else {
//不存在
System.out.println("布隆过滤器查询失败");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
大家可以根据不同的业务使用不同的过滤器,源代码在码云上,大家可以测试使用地址:https://gitee.com/dai-wu/watched 里面有一个sql文件是几十万条数据,应该够测试