目录
- 引出
- Redis的问题
- 缓存雪崩:key不存在
- 缓存击穿:热点key
- 缓存穿透【重要】
- 穿透的解决方案:布隆过滤器
- 问题:如何存储100w纯数字
- 布隆过滤器
- 项目应用:布隆过滤器≈白名单
- htool工具包案例
- Redis项目应用(六)布隆过滤器---白名单
- 业务逻辑
- 布隆过滤器工具类 BitMapBloomFilter
- 缓存预热+布隆过滤器初始化@Scheduled(cron = "0 01 18 * * ?")
- service层:布隆-->缓存-->数据库
- controller层:布隆无,非法;布隆有-->service
- 测试结果模拟
- 1.布隆过滤器未通过,非法请求
- 2.布隆过滤器通过,缓存中查询成功
- 3.布隆过滤器通过,缓存没有,从数据库查询
- 总结
引出
1.Redis的问题,缓存雪崩,key不存在;击穿,热点key;穿透,redis没有,数据库没有;
2.布隆过滤器,返回没有,则结论一定正确;返回有,结论不一定正确;
3.布隆过滤器可以作为项目的白名单,htool工具包的使用;
4.项目应用BitMapBloomFilter;缓存预热+布隆过滤器初始化@Scheduled(cron = “0 01 18 * * ?”);
5.流程:布隆–>缓存–>数据库;
Redis的问题
缓存雪崩:key不存在
让key的失效时间随机
大量请求访问redis,redis的key不存在,数据大量请求发送到数据库,数据库压力瞬间变大;
当大量请求访问redis,redis的key不存在,大量请求打向数据库。
解决方案; 在过期时间设置为随机,防止同一时间消失。
redisTemplate.opsForValue().set("users", users,
new Random().nextInt(7) + 29, TimeUnit.MINUTES);
缓存击穿:热点key
让key永不过期
某个key访问量突然暴增,微博某个词条访问量瞬间很大,一直转圈
此时,数据库一直查询某个key,一直查询某个值;
解决方案:让key 永不过期
缓存穿透【重要】
指客户端请求的数据在缓存和数据库中都没有,这样缓存永远不会生效,这些请求都会打到数据库;
如果有人查询100w条数据都打到数据库,数据库就会瘫痪;
穿透的解决方案:布隆过滤器
布隆过滤器,就是一种数据结构,它是由一个长度为m bit的位数组与n个hash函数组成的数据结构,位数组中每个元素的初始值都是0。在初始化布隆过滤器时,会先将所有key进行n次hash运算,这样就可以得到n个位置,然后将这n个位置上的元素改为1。这样,就相当于把所有的key保存到了布隆过滤器中了。
结论:如果布隆过滤器中没有,则一定没有,常用作白名单;
即:布隆过滤器返回值:
如果布隆过滤器返回没有,则结论正确,阻止访问;
如果布隆过滤器返回有,则结论不一定正确;
项目应用:
如果白名单中没有,则对方恶意访问缓存,阻止访问;
问题:如何存储100w纯数字
在连续的空间存储100w个纯数字占用多少内存
package com.tianju.redisDemo.testDemo;
public class BigMapDemo {
public static void main(String[] args) {
int[] a = {1,2,3,4,5,6,7,8,9};
int[] b = new int[1000000]; // 存100w个数字
// 占用空间,1个int占用32 bits空间
System.out.println(32*1000000.0/1024/1024+"MB");
}
}
int— 32bits
100w x 32bits= 32,000,000(bits)
1kb = 1024 bits;
1M = 1024kb
结论:100w个纯数字占用了30MB的内存;
解决办法,用BigMap,1int,对应32个bits,可以用来表示0~31个数字,如下图所示,可以表示数字1,5;大大节省了空间
布隆过滤器
布隆过滤器(Bloom Filter)本质上是由长度为 m 的位向量或位列表(仅包含 0 或 1 位值的列表)组成,最初所有的值均设置为 0,如下图所示。
为了将数据项添加到布隆过滤器中,会提供 K 个不同的哈希函数,并将结果位置上对应位的值置为 “1”。
如上图所示,当输入 “semlinker” 时,预设的 3 个哈希函数将输出 2、4、6,我们把相应位置 1。假设另一个输入 ”kakuqo“,哈希函数输出 3、4 和 7。你可能已经注意到,索引位 4 已经被先前的 “semlinker” 标记了。此时,我们已经使用 “semlinker” 和 ”kakuqo“ 两个输入值,填充了位向量。当前位向量的标记状态为:
当对值进行搜索时,与哈希表类似,我们将使用 3 个哈希函数对 ”搜索的值“ 进行哈希运算,并查看其生成的索引值。假设,当我们搜索 ”fullstack“ 时,3 个哈希函数输出的 3 个索引值分别是 2、3 和 7:
从上图可以看出,相应的索引位都被置为 1,这意味着我们可以说 ”fullstack“ 可能已经插入到集合中。事实上这是误报的情形,产生的原因是由于哈希碰撞导致的巧合而将不同的元素存储在相同的比特位上。幸运的是,布隆过滤器有一个可预测的误判率(FPP):
- n 是已经添加元素的数量;
- k 哈希的次数;
- m 布隆过滤器的长度(如比特数组的大小);
极端情况下,当布隆过滤器没有空闲空间时(满),每一次查询都会返回 true 。这也就意味着 m 的选择取决于期望预计添加元素的数量 n ,并且 m 需要远远大于 n 。
实际情况中,布隆过滤器的长度 m 可以根据给定的误判率(FFP)的和期望添加的元素个数 n 的通过如下公式计算:
了解完上述的内容之后,我们可以得出一个结论,
当我们搜索一个值的时候,
若该值经过 K 个哈希函数运算后的任何一个索引位为 ”0“,那么该值肯定不在集合中。
但如果所有哈希索引值均为 ”1“,则只能说该搜索的值可能存在集合中。
项目应用:布隆过滤器≈白名单
如果布隆过滤器中没有,则一定没有,可以证明没有;
结论:
如果布隆过滤器返回没有,这个结论一定正确;
如果布隆过滤器返回有,这个结论不一定正确;
如果白名单中没有,则对方恶意访问缓存,则不通过,阻止访问;
htool工具包案例
https://www.bookstack.cn/read/hutool-5.6.0-zh
// 初始化
BitMapBloomFilter filter = new BitMapBloomFilter(10);
filter.add("123");
filter.add("abc");
filter.add("ddd");
// 查找
filter.contains("abc")
Redis项目应用(六)布隆过滤器—白名单
业务逻辑
后台查询用户,防止穿透缓存到数据库查询用户,导致数据库压力大;
- 预热的时候,用户信息加载到布隆过滤器中;
- 查询业务,比如查询用户时,先用布隆过滤器过滤,再去redis;
以一个根据用户名查询用户是否在数据库中案例为例;
1.先到布隆过滤器中,在controller层解决,拦阻非法请求;
2.通过布隆过滤器,在redis缓存中查找,查询成功返回;
3.通过布隆过滤器,在redis缓存中没找到,查询数据库,查询成功返回,并且更新缓存
布隆过滤器工具类 BitMapBloomFilter
WhiteListBloomFilter.java工具类
package com.tianju.redisDemo.util;
import cn.hutool.bloomfilter.BitMapBloomFilter;
/**
* 白名单的布隆过滤器
*/
public class WhiteListBloomFilter {
// Params: m – M值决定BitMap的大小
private static BitMapBloomFilter bloomFilter = new BitMapBloomFilter(10);
// TODO:布隆过滤器有,缓存没有,需要从数据库查询
/**
* 将str加入布隆过滤器中,作为白名单使用
* @param str
*/
public static void addBloom(String str){
bloomFilter.add(str);
}
/**
* 判断str是否在布隆过滤器中
* @param str
* @return true表示可能在布隆过滤器中;false表示一定不在布隆过滤器中
*/
public static Boolean isInBloom(String str){
return bloomFilter.contains(str);
}
}
缓存预热+布隆过滤器初始化@Scheduled(cron = “0 01 18 * * ?”)
UsernamesPreHot.java缓存预热
package com.tianju.redisDemo.job;
import com.tianju.redisDemo.dao.UserMapper;
import com.tianju.redisDemo.entity.User;
import com.tianju.redisDemo.util.WhiteListBloomFilter;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.Schedules;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* 采用 @Scheduled(cron = "0 54 21 * * ?")进行预热
*/
@Slf4j
@Component
public class UsernamesPreHot {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private UserMapper userMapper;
@Scheduled(cron = "0 01 18 * * ?")
public void preHot() {
// 1.清除缓存中的数据
stringRedisTemplate.delete("usernames");
// 2.更新缓存中的数据
// 3.也要加入到布隆过滤器中
List<User> userList = userMapper.selectList(null);
// TODO:布隆过滤器有,缓存没有,需要从数据库查询
WhiteListBloomFilter.addBloom("Arya");
userList.forEach(user ->{
stringRedisTemplate.opsForSet().add("usernames", user.getUsername());
// 放到布隆过滤器中
WhiteListBloomFilter.addBloom(user.getUsername());
}
);
log.debug("redis缓存预热 + 布隆过滤器预热,usernames");
}
}
service层:布隆–>缓存–>数据库
package com.tianju.redisDemo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tianju.redisDemo.dao.UserMapper;
import com.tianju.redisDemo.entity.User;
import com.tianju.redisDemo.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@Transactional
@Slf4j
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public String findByUsername(String username) {
// 1.先到布隆过滤器中,在controller层解决
// 2.然后到redis中
log.debug(">>>> 是否在redis缓存中");
Boolean isInRedis = stringRedisTemplate.opsForSet().isMember("usernames", username);
if (isInRedis){ // 缓存中有
log.debug(username+"在redis缓存中查找成功,,,");
Set<String> usernames = stringRedisTemplate.opsForSet().members("usernames");
return usernames.stream().filter(u -> username.equals(u)).collect(Collectors.toList()).get(0);
// 3.最后到数据库
}else { // 缓存中也没有,到数据库查询
log.debug("缓存中不存在,从数据库中进行读取....");
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", username); // 列名和参数
User user = userMapper.selectOne(wrapper);
log.debug("数据库查询成功,更新缓存....");
stringRedisTemplate.opsForSet().add("usernames", user.getUsername());
return user.getUsername();
}
}
}
controller层:布隆无,非法;布隆有–>service
package com.tianju.redisDemo.controller;
import com.tianju.redisDemo.dto.HttpResp;
import com.tianju.redisDemo.dto.ResultCode;
import com.tianju.redisDemo.service.IUserService;
import com.tianju.redisDemo.util.WhiteListBloomFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@RestController
@RequestMapping("/api/user")
@Slf4j
public class UserController {
@Autowired
private IUserService userService;
@GetMapping("findByUsername")
public HttpResp findByUsername(String username){
// 1.先过布隆过滤器
Boolean inBloom = WhiteListBloomFilter.isInBloom(username);
// 如果布隆过滤器里没有,则一定没有,非法请求
if (!inBloom){
log.debug(">>>>>非法请求。布隆过滤器未通过");
return HttpResp.results(ResultCode.USER_FIND_ERROR,new Date(),"非法用户禁止操作");
}
// 然后到缓存 --- 》 到数据库
String byUsername = userService.findByUsername(username);
return HttpResp.results(ResultCode.BOOK_RUSH_SUCCESS,new Date(),byUsername);
}
}
测试结果模拟
1.布隆过滤器未通过,非法请求
2.布隆过滤器通过,缓存中查询成功
3.布隆过滤器通过,缓存没有,从数据库查询
一开始数据库
现在布隆过滤器器里有,数据库里面有,缓存没有
数据库后台改动
总结
1.Redis的问题,缓存雪崩,key不存在;击穿,热点key;穿透,redis没有,数据库没有;
2.布隆过滤器,返回没有,则结论一定正确;返回有,结论不一定正确;
3.布隆过滤器可以作为项目的白名单,htool工具包的使用;
4.项目应用BitMapBloomFilter;缓存预热+布隆过滤器初始化@Scheduled(cron = “0 01 18 * * ?”);
5.流程:布隆–>缓存–>数据库;