我想知道大家是否注意到,在注册一些应用时,会提示您用户名已被占用,需要更改。
现在让我们逐个查看每种方案的优缺点。
这种方法带来以下问题:
-
存在相对较高延迟的性能问题。如果数据量巨大,查询速度将变慢。此外,数据库查询涉及应用服务器和数据库服务器之间的网络通信。建立连接、发送查询和接收响应所需的时间也会导致延迟。
-
数据库负载过重。频繁执行 SELECT 查询以检查用户名的唯一性,每个查询都会消耗数据库资源,包括 CPU 和 I/O 资源。
-
可扩展性差。数据库对并发连接和资源有限制。如果注册率持续上升,数据库服务器可能难以处理不断增加的请求。对数据库进行纵向扩展(向单个服务器添加更多资源)可能成本高昂且存在限制。
为了解决数据库调用性能问题,检查用户名唯一性引入了一个高效的 Redis 缓存。
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.api.RMap;
public class UserExistenceChecker {
// Redis 哈希映射名称,用于存储用户信息
private static final String USER_HASH_NAME = "users";
public static void main(String[] args) {
// 创建一个 Redisson 客户端
RedissonClient redisson = createRedissonClient();
// 检索用于存储用户信息的哈希映射
RMap<String, String> users = redisson.getMap(USER_HASH_NAME);
// 向哈希映射中添加用户
users.put("user123", "someUserInfo"); // 这里的 "someUserInfo" 可以是 JSON 字符串、UUID 等
// 检查用户是否存在
boolean exists = users.containsKey("user123");
System.out.println("用户 'user123' 是否存在?" + exists);
// 检查一个不存在的用户
exists = users.containsKey("user456");
System.out.println("用户 'user456' 是否存在?" + exists);
// 关闭 Redisson 客户端
redisson.shutdown();
}
// 创建 Redisson 客户端的辅助方法
private static RedissonClient createRedissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379") // 根据您的 Redis 地址进行调整
.setPassword("yourpassword"); // 如果有密码,请提供您的 Redis 密码
return Redisson.create(config);
}
}
这种解决方案的最大问题是内存使用过多。假设每个用户名需要大约 15 字节的内存。如果要存储十亿个用户名,您将需要 15GB 的内存。
总内存 = 每条记录的内存使用 * 记录数 = 15 字节/记录 * 10,000,000,000 条记录 = 15,000,000,000 字节 ≈ 15,000,000 KB ≈ 15,000 MB ≈ 15 GB
如果直接缓存判断结果导致内存使用过多,是否有更好的方法?布隆过滤器是一个非常好的选择。
布隆过滤器是一种高度空间效率的随机数据结构。它使用位数组来简洁地表示一个集合,并可以确定一个元素是否属于这个集合。布隆过滤器的高效性是以一定的代价为代价的:在确定一个元素是否属于某个集合时,有可能错误地将一个不属于该集合的元素视为属于该集合(误报)。因此,布隆过滤器不适用于“零错误”的应用场景。然而,在可以容忍低错误率的应用场景中,布隆过滤器通过极少的错误实现了存储空间的显著节省。
从上述分析可以知道,布隆过滤器的核心思想是使用位数组(bit array
)和一组哈希函数。
位数组,每个位最初为 0
在插入值 x 时,分别使用 k 个哈希函数(图中为 3 个)对值 x 进行哈希,取哈希值与布隆过滤器容量的余数,并将结果表示的相应位的值设置为 1。
搜索过程类似于插入过程。同样,使用 k 个哈希函数对要搜索的值进行哈希。只有当哈希得到的每个位的值都为 1 时,才表明该值“可能”真实存在;反之,如果任何位的值为 0,则表明该值必定不存在。例如,y1 必定不存在;而 y2 可能存在。
Redis 本身支持布隆过滤器的数据结构。让我们简单地使用 Redisson 客户端实现代码:
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class UserExistenceChecker {
// Redis 中布隆过滤器的名称
private static final String BLOOM_FILTER_NAME = "user_existence_filter";
public static void main(String[] args) {
// 创建一个 Redisson 客户端
RedissonClient redisson = createRedissonClient();
// 检索或创建一个布隆过滤器实例
// 预期元素数量和误报概率是参数
RBloomFilter<String> bloomFilter = redisson.getBloomFilter(BLOOM_FILTER_NAME);
bloomFilter.tryInit(100000L, 0.001); // 使用预期元素和误报率初始化布隆过滤器
// 向布隆过滤器中添加用户
bloomFilter.add("user123");
// 检查用户是否存在
boolean exists = bloomFilter.contains("user123"); // 应返回 true
System.out.println("用户 'user123' 是否存在?" + exists);
// 检查一个不存在的用户(由于布隆过滤器的特性,可能错误地报告为 true)
exists = bloomFilter.contains("user456"); // 假设未添加,理想情况下应返回 false,但可能是误报
System.out.println("用户 'user456' 是否存在?" + exists);
// 关闭 Redisson 客户端
redisson.shutdown();
}
// 创建 Redisson 客户端的辅助方法
private static RedissonClient createRedissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379"); // 根据您的 Redis 地址进行调整
// .setPassword("yourpassword"); // 如果有密码,请提供您的 Redis 密码
return Redisson.create(config);
}
}
优点:
-
节省内存空间: 与使用哈希表等数据结构相比,布隆过滤器通常需要更少的内存空间,因为它不存储实际元素,而只存储元素的哈希值。如果使用误差概率为
0.001
存储 10 亿条记录,只需要1.67 GB
的内存。与原始的15G
相比,大大减少了。 -
高效查找: 布隆过滤器可以在常数时间
(O(1))
内快速确定集合中是否存在元素,而无需遍历整个集合。
缺点:
-
存在误报率: 当布隆过滤器确定元素是否存在时,存在一定的误报率。这意味着在某些情况下,它可能错误地报告某个元素存在,但不会错误地报告某个元素不存在。然而,这通常影响不大。
-
无法删除元素: 布隆过滤器通常不支持从集合中删除元素,因为删除一个元素会影响其他元素的哈希值,增加误报率。
Redis 布隆过滤器方案为大数据量下的唯一性验证提供了高效的基于内存的解决方案,但需要消耗内存。