在我们的日常编码中,用户名是不能重复的,因为在项目中,用户名的唯一性很重要,很多时候我们都需要依据用户名来确保整个链路上的参与者的唯一性,虽然用户 id 也是唯一的,但是页面上展示的时候不能用 id 呀。
实际编码中为了实现用户在注册,改名的时候的用户名的唯一性,我们一般是先从数据库查询是否存在,不存在则让用户注册。
但是为了考虑到性能,我们会把用户名存储到缓存中,一般使用 redis 缓存。那既然都用了缓存了,那还不如干脆直接就用布隆过滤器来存储,既可以做重复校验,又能节省空间。
我们先来简单介绍一下BloomFilter。
布隆过滤器是一种数据结构,用于快速检索一个元素是否可能存在于一个集合(bit 数组)中。它的基本原理是利用多个哈希函数,将一个元素映射成多个位,然后将这些位设置为 1。当查询一个元素时,如果这些位都被设置为 1,则认为元素可能存在于集合中,否则肯定不存在。
所以,布隆过滤器可以准确的判断一个元素是否一定不存在,但是因为哈希冲突的存在,所以他没办法判断一个元素一定存在。只能判断可能存在。
给大家举个例子,我们定义了两个方法,userNameExist()用于判断是否存在,addUserName()用于向布隆过滤器中添加已注册的用户名。
public boolean userNameExist(String userName) {
//如果布隆过滤器中存在,再进行数据库二次判断
if (this.bloomFilter.contains(userName)) {
return userMapper.findByUserName(userName) != null;
}
return false;
}
private boolean addUserName(String userName) {
return this.bloomFilter.add(userName);
}
因为布隆过滤器有误判的,存在一定的误判率,他会把不存在的用户判断为存在,所以当检查结果是存在的时候,需要查询数据库再次判断一次。因为用户注册的时候,大多数情况下都是不重复的,所以我们可以用布隆过滤器进行不存在的快速判断。如果某个使用场景中需要判断的对象存在重复的机率比较大,则不建议使用布隆过滤器。
布隆过滤器如何初始化?
private RBloomFilter<String> bloomFilter;
@Override
public void afterPropertiesSet() throws Exception {
this.bloomFilter = redissonClient.getBloomFilter("userName");
if (!bloomFilter.isExists()) {
this.bloomFilter.tryInit(10000000L, 0.01);
}
}
我们设置了10000000的容量,误判率是0.01。这里需要在类上implements InitializingBean。InitializingBean
接口是Spring框架提供的一个扩展点,它允许开发者在Bean的属性设置完成后进行额外的初始化操作。当一个Bean实现了InitializingBean
接口,Spring容器在初始化该Bean时会自动调用其afterPropertiesSet()
方法。
@CustomLock(keyExpression = "#telephone", scene = "USER_REGISTER")
public UserResponse register(String telephone) {
String defaultUserName;
String randomString;
do {
randomString = RandomUtil.randomString(6).toUpperCase();
//前缀 + 6位随机数 + 手机号后四位
defaultUserName = DEFAULT_USER_NAME_PREFIX + randomString + telephone.substring(7, 11);
} while (userNameExist(defaultUserName));
User user = register(telephone, defaultUserName, telephone, randomString);
Assert.notNull(user, UserErrorCode.USER_OPERATE_FAILED.getCode());
addUserName(defaultUserName);
updateUserCache(user.getId().toString(), user);
//加入流水
long streamResult = userOperateStreamService.insertStream(user, UserOperateTypeEnum.REGISTER);
Assert.notNull(streamResult, () -> new BizException(RepoErrorCode.UPDATE_FAILED));
UserResponse userResponse = new UserResponse();
userResponse.setSuccess(true);
return userResponse;
}
如果用户名修改了怎么办?
比如原来用户名是user001,被放到布隆过滤器了,但是后面我改成user002了,那么意味着 user001已经没有了,那么如何从布隆过滤器删除呢?
因为布隆过滤器某一位二进制位可能被多个编号Hash引用,因此布隆过滤器无法直接处理删除数据的情况。
常见的解决方案如下:
解决方案1:定时异步重建布隆过滤器
解决方案2: 计数Bloom Fliter