文章目录
Redis学习笔记:Bitmap结合SpringBoot自研布隆过滤器案例
Bitmap的概念
Bitmap
由0和1状态表现的二进制位的bit数组
Bitmap的作用:
用于状态统计
使用Bitmap的需求
用户是否登陆过Y、N,比如京东每日签到送京东
电影、广告是否被点击播放过
钉钉打卡上下班,签到统计
京东每日签到送京豆案例
需求说明
签到日历仅展示当月签到数据
签到日历需展示最近连续签到天数
假设当前日期是20210618,且20210616未签到
若20210617已签到且0618未签到,则连续签到天数为1
若20210617已签到且0618已签到,则连续签到天数为2
连续签到天数越多,奖励越大
所有用户均可签到
截至2020年3月31日的12个月,京东年度活跃用户数3.87亿,同比增长24.8%,环比增长超2500万,此外,2020年3月移动端日均活跃用户数同比增长46%假设10%左右的用户参与签到,签到用户也高达3千万,用数据库实现不现实
解决方案:
在签到统计时,每个用户一天的签到用1个bit位就能表示,
一个月(假设是31天)的签到情况用31个bit位就可以,一年的签到也只需要用365个bit位,根本不用太复杂的集合类型
常用命令
#setbit 键 偏移位 只能零或者一
setbit key offset value
#getbit 获取指定的key的偏移量的value
getbit key offset
#strlen 统计字节数占用多少
strlen key
#bitcount 全部键里面含有1的有多少个
bitcount key
#bitop 在位图中查找第一个被设置为指定值的二进制位(只能是0或者1),并返回这个二进制位的偏移量。
bitop key value
布隆过滤器的概念
布隆过滤器是什么
由一个初始值都为0的bit数组和多个哈希函数构成,用来快速判断集合中是否存在某个元素
布隆过滤器的作用
高效地插入和查询,占用空间少,返回的结果是不确定性+不够完美
一个元素如果判断结果存在,元素不一定存在
一个元素如果判断结果不存在,则元素一定不存在
布隆过滤器可以添加元素,不能删除元素
布隆过滤器的原理
一个大型位数组和几个不同的无偏hash函数(无偏表示分布均匀)。由一个初值都为零的bit数组和多个个哈希函数构成,用来快速判断某个数据是否存在。
添加key时,使用多个hash函数对key进行hash运算得到一个整数索引值,对位数组长度进行取模运算得到一个位置,每个hash函数都会得到一个不同的位置,将这几个位置都置1就完成了add操作。
查询key时,只要有其中一位是零就表示这个key不存在,但如果都是1,则不一定存在对应的key。
布隆过滤器不能删除的原因
布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,
因此误判的根源在于相同的 bit 位被多次映射且置 1。
这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。
如果我们直接删除这一位的话,会影响其他的元素
布隆过滤器的使用场景
解决缓存穿透的问题,和redis结合bitmap使用
黑名单校验,识别垃圾邮件
安全连接网址,全球上10亿的网址判断
SpringBoot结合redis的bitmap自研布隆过滤器,解决缓存穿透
BloomFilterInit(白名单初始化):
@Component
@Slf4j
public class BloomFilterInit {
@Resource
private RedisTemplate redisTemplate;
@PostConstruct//初始化布隆过滤器白名单
public void init() {
// 1.白名单客户加载到布隆过滤器
String key = "customer:0";
// 2.计算hashValue,由于存在计算出来负数的可能,我们取绝对值
int hashValue = Math.abs(key.hashCode());
// 3.通过hashValue和2的32次方取余,获得对应的下标坑位
long index = (long) (hashValue % Math.pow(2, 32));
// 4.设置redis里面的bitmap对应类型白名单:whitelistCustomer的坑位,将值设置为1
redisTemplate.opsForValue().setBit("whitelistCustomer", index, true);
log.info("customer:0,在whitelistCustomer的坑位是:" + index);
}
}
CheckUtils类:
@Component
@Slf4j
public class CheckUtils {
@Autowired
private RedisTemplate redisTemplate;
public boolean checkWithBloomFilter(String checkItem, String key) {
int hashValue = Math.abs(key.hashCode());
long index = (long) (hashValue % Math.pow(2, 32));
Boolean result = redisTemplate.opsForValue().getBit(checkItem, index);
log.info("--->key:" + key + "对应坑位下标index:" + index + "是否存在:" + result);
return result;
}
}
CustomerService类:
@Service
@Slf4j
public class CustomerServiceImpl implements CustomerService {
public static final String CACHE_KEY_CUSTOMER = "customer:";
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private CustomerMapper customerMapper;
@Autowired
private CheckUtils checkUtils;
@Override
public void addCustomer(Customer customer) {
int result = customerMapper.insertOneCustomer(customer);
if (result > 0) {
//mysql插入成功,需要重新查询一次,将数据捞出来,写进redis
Customer customerResult = customerMapper.queryOneCustomerById(customer.getId());
//redis缓存key
String key = CACHE_KEY_CUSTOMER + customer.getId();
//捞出来的数据写进redis
redisTemplate.opsForValue().set(key, customerResult);
}
}
@Override
public Customer queryCustomerById(Integer customerId) {
Customer customer = null;
//缓存redis的key名称
String key = CACHE_KEY_CUSTOMER + customerId;
// 1.先去redis查询
customer = (Customer) redisTemplate.opsForValue().get(key);
// 2.redis有直接返回,没有再进去查询mysql
if (customer == null) {
// 3.查询mysql
customer = customerMapper.queryOneCustomerById(customerId);
// 3.1mysql有,redis无
if (customer != null) {
// 3.2把mysql查询出来的数据放进 redis
redisTemplate.opsForValue().set(key, customer);
}
}
return customer;
}
@Override
public Customer queryCustomerByIdWithBloomFilter(Integer customerId) {
Customer customer = null;
//缓存redis的key名称
String key = CACHE_KEY_CUSTOMER + customerId;
// 布隆过滤器check,无是绝对无,有是可能有
//=================================
if (!checkUtils.checkWithBloomFilter("whitelistCustomer", key)) {
log.info("白名单无此顾客,不可访问:" + key);
//执行停止操作
}
// 1.先去redis查询
customer = (Customer) redisTemplate.opsForValue().get(key);
// 2.redis有直接返回,没有再进去查询mysql
if (customer == null) {
// 3.查询mysql
customer = customerMapper.queryOneCustomerById(customerId);
// 3.1mysql有,redis无
if (customer != null) {
// 3.2把mysql查询出来的数据放进 redis
redisTemplate.opsForValue().set(key, customer);
}
}
return customer;
}
}
CustomerController类:
@RestController
@RequestMapping("/customer")
public class CustomerController {
@Autowired
private CustomerService customerService;
@ApiOperation("数据库初始化2条Customer记录插入")
@PostMapping("/add")
public void addCustomer() {
for (int i = 0; i < 2; i++) {
Customer customer = new Customer();
customer.setId(i);
customer.setCname("customer" + i);
customer.setAge(new Random().nextInt(30) + 1);
customer.setPhone("1371111XXXX");
customer.setSex((byte) new Random().nextInt(2));
customer.setBirth(Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()));
customerService.addCustomer(customer);
}
}
@ApiOperation("单个用户查询,按照id查询")
@GetMapping("/queryOne/{customerId}")
public Customer queryOneCustomerById(@PathVariable int customerId) {
return customerService.queryCustomerById(customerId);
}
@ApiOperation("布隆过滤器,按照id查询")
@GetMapping("/customerBloomFilter/{customerId}")
public Customer queryOneCustomerByIdWithBloomFilter(@PathVariable int customerId) {
Customer customer = customerService.queryCustomerByIdWithBloomFilter(customerId);
return customer;
}
}
测试结果:
mysql中的数据
queryOneCustomerByIdWithBloomFilter:
白名单内只有customer0,所以customer1是无法访问的。