本文将讲解bitmap的概念、命令、应用场景,布隆过滤器的概念和使用bitmap实现布隆过滤器。
bitmap
bitmap概念
bitmap,一种用于处理位操作的特殊数据结构,是一个由二进制位组成的字符串(即二进制数组,每一位都只能是0或1),常用于二值统计场景。
bitmap命令
- SETBIT key offset value:将指定偏移量处的位设置为指定的值(0或1)。
- GETBIT key offset:获取指定偏移量处的位的值。
- BITCOUNT key [start end]:统计指定范围内位为1的数量。
- BITOP operation destkey key [key ...]:对多个Bitmap进行位操作,将结果存储在目标键中,支持的位操作有AND、OR、XOR和NOT。
- BITPOS key bit [start [end]]:查找指定位值(0或1)在Bitmap中的位置范围。
bitmap应用场景
任何二值统计场景都可以使用bitmap,如:
- 签到、打卡应用。
- 用户在线状态统计。
布隆过滤器
布隆过滤器是一种用于判断某个元素是否属于某个集合的特殊数据结构,其具有判断为无则不存在,判断为有则不一定存在的特性。
思想
当有变量被加入集合时,通过N个映射函数将这个变量映射成位图中的N个点, 把它们置为 1(假定有两个变量都通过 3 个映射函数)。
查询某个变量的时候我们只要看看这些点是不是都是 1, 就可以大概率知道集合中有没有它了:
- 如果这些点,有任何一个为零则被查询变量一定不在。
- 如果都是 1,则被查询变量很可能存在(因为存在哈希碰撞的情况)。
应用场景
布隆过滤器常用于预防缓存穿透,其存储已存在数据的key,当有新的请求时,先到布隆过滤器中查询是否存在:
- 如果布隆过滤器中不存在该条数据则直接返回。
- 如果布隆过滤器中已存在,才去查询缓存redis,如果redis里没查询到则再查询Mysql数据库。
另外也可以根据布隆过滤器数据结构的特点,用于进行黑名单、白名单校验等场景。
缺陷
布隆过滤器有两方面的缺陷,都是由哈希碰撞导致的:
- 如果判断为有,可能为无。
- 无法删除元素,否则会导致存在的其他元素被删除而判断为无。
代码实现
首先声明依赖:
<dependencies>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</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-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.5</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
本次将使用mybatis-plus-generator反向生成service,以便快速开始bloomfilter内容的展示。
在预估bitmap大小时,可以使用公式m = (-n * ln(p)) / (ln(2)^2),m为位图的大小,n是要存储的对象数量,p是期望的假阳性率。
如1000000数据下,1%假阳性需要9,585,058bit,即≈2^23,需要对hash结果mod2^23来控制位图大小。
BloomUtils.class:
@Component
public class BloomUtils {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
IEmployeesService service;
public static final String KEY_NAME = "WhitelistEmployees";
public void init() {
//白名单客户预加载到布隆过滤器
List<Employees> employees = service.getBaseMapper().selectList(new QueryWrapper<>());
employees.forEach(employees1 -> {
try {
addToWhitelist(String.valueOf(employees1.getEmployeeId()));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
});
}
//根据m = (-n * ln(p)) / (ln(2)^2)公式(m为位图的大小,n是要存储的对象数量,p是期望的假阳性率)。
// 1000000数据下,1%假阳性需要9,585,058bit,即≈2^23,需要对hash结果mod2^23来控制位图大小。
public void addToWhitelist(String value) throws NoSuchAlgorithmException {
long md5Hash = (long) (hashToPositiveInt(value, "MD5") % Math.pow(2, 23));
long sha1Hash = (long) (hashToPositiveInt(value, "SHA-1") % Math.pow(2, 23));
long sha256Hash = (long) (hashToPositiveInt(value, "SHA-256") % Math.pow(2, 23));
stringRedisTemplate.opsForValue().setBit(KEY_NAME, md5Hash, true);
stringRedisTemplate.opsForValue().setBit(KEY_NAME, sha1Hash, true);
stringRedisTemplate.opsForValue().setBit(KEY_NAME, sha256Hash, true);
}
public boolean checkInWhitelist(String value) throws NoSuchAlgorithmException {
long md5Hash = (long) (hashToPositiveInt(value, "MD5") % Math.pow(2, 23));
long sha1Hash = (long) (hashToPositiveInt(value, "SHA-1") % Math.pow(2, 23));
long sha256Hash = (long) (hashToPositiveInt(value, "SHA-256") % Math.pow(2, 23));
return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().getBit(KEY_NAME, md5Hash)) &&
Boolean.TRUE.equals(stringRedisTemplate.opsForValue().getBit(KEY_NAME, sha1Hash)) &&
Boolean.TRUE.equals(stringRedisTemplate.opsForValue().getBit(KEY_NAME, sha256Hash));
}
public static int hashToPositiveInt(String input, String algorithm) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance(algorithm);
byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
// 使用BigInteger将字节数组表示的哈希值转换为正整数
BigInteger bigIntegerHash = new BigInteger(1, hashBytes);
// 获取正整数表示的哈希值
return bigIntegerHash.intValue();
}
}
然后即可在Controller中直接调用:
@RestController
@RequestMapping("/employees")
public class EmployeesController {
@Autowired
IEmployeesService service;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
BloomUtils bloomUtils;
@GetMapping("/init")
public void initRedisWhitelist() {
bloomUtils.init();
}
@GetMapping("/check")
public boolean checkWhitelist(@RequestParam String employeesID) throws NoSuchAlgorithmException {
return bloomUtils.checkInWhitelist(employeesID);
}
@GetMapping("/find")
public Employees findByEmployeeById(@RequestParam String employeesID) throws NoSuchAlgorithmException, JsonProcessingException {
if (bloomUtils.checkInWhitelist(employeesID)) {//先过布隆过滤器
String Semployees = stringRedisTemplate.opsForValue().get(employeesID);
Employees employees = null;
if (Semployees == null) {
synchronized (this) {
Semployees = stringRedisTemplate.opsForValue().get(employeesID);// 双检查,检查缓存中是否有数据
if (Semployees == null) {
employees = service.getById(employeesID);// 从数据库获取数据
stringRedisTemplate.opsForValue().set(employeesID, new ObjectMapper().writeValueAsString(employees));// 将数据写入缓存
}
}
return employees;
} else {
return new ObjectMapper().readValue(Semployees, Employees.class);
}
} else {//布隆过滤器不通过则直接返回null
return null;
}
}
@PostMapping("/add")
public void addEmployees(@RequestBody Employees employees) throws JsonProcessingException, NoSuchAlgorithmException {
service.save(employees);
Employees employees1 = service.getById(employees.getEmployeeId());
stringRedisTemplate.opsForValue().set(String.valueOf(employees1.getEmployeeId()),new ObjectMapper().writeValueAsString(employees1));
bloomUtils.addToWhitelist(String.valueOf(employees1.getEmployeeId()));
}
}