[小布去面试系列]如何检查几十亿用户中是否存在某个用户名
共 6133字,需浏览 13分钟
·
2024-12-02 14:11
小布去面试,面试官问:我们在注册一个应用程序时,经常发现自己喜欢的用户名已被他人使用了这种情况。假如我的游戏中总共有 10 亿用户,如何快速判断你的用户名是否已经在这些用户中呢?
小布:玩个游戏也这么多事儿?那我可要发挥了。
虽然这看起来不起眼的一个功能,但对于处理大量用户的应用程序来说,却是一个重大的技术挑战。确定用户名是否可用的过程有几种方法,每种方法都有其优缺点。我这里跟你探讨三种方法:传统的数据库查询、使用 Redis 的缓存策略以及使用 Bloom 过滤器的优化方法。
方法 1:直接查数据库
检查用户名是否存在的最直接方法是查询数据库。但是,当用户数量增长到数百万或数十亿时,这种方法就会变得效率低下。
下面是这种方式的主要缺点:
-
高延迟:
在查询大型数据库时,由于延迟增加,性能可能会受到影响。每次查询都涉及应用程序和数据库服务器之间的网络通信,从而增加了延迟。
-
数据库压力大:
检查用户名唯一性的定期 SELECT 查询会消耗大量数据库资源,包括 CPU 和 I/O。这可能会导致瓶颈,尤其是在高峰期。
-
可扩展性问题:
数据库在处理大量并发连接方面存在固有的局限性。随着用户注册数的增加,数据库可能难以跟上,从而导致性能下降。纵向扩展数据库(增加更多资源)的成本可能很高,而且有其局限性。
方法 2:Redis 缓存解决方案
为了缓解数据库查询的性能问题,可以引入 Redis 等缓存层。这包括将用户名存储在 Redis 哈希映射中,从而加快检查速度。
实现
package main
import (
"fmt"
"github.com/go-redis/redis/v8"
"context"
)
const (
USERNAME_HASH_MAP = "username_records" // Redis hash map name to store user information
)
func main() {
ctx := context.Background()
// Create a Redis client
client := initializeRedisClient()
// Add a username to the hash map
err := client.HSet(ctx, USERNAME_HASH_MAP, "john_doe", "userInfo").Err() // "userInfo" could be a JSON string, UUID, etc.
if err != nil {
fmt.Println("Error adding username:", err)
return
}
// Check if a username exists
isPresent, err := client.HExists(ctx, USERNAME_HASH_MAP, "john_doe").Result()
if err != nil {
fmt.Println("Error checking username existence:", err)
return
}
fmt.Printf("Username 'john_doe' exists? %v\n", isPresent)
// Check for a non-existent username
isPresent, err = client.HExists(ctx, USERNAME_HASH_MAP, "jane_doe").Result()
if err != nil {
fmt.Println("Error checking username existence:", err)
return
}
fmt.Printf("Username 'jane_doe' exists? %v\n", isPresent)
// Close the Redis client connection
err = client.Close()
if err != nil {
fmt.Println("Error closing Redis client:", err)
}
}
// Helper function to create a Redis client
func initializeRedisClient() *redis.Client {
return redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379", // Adjust to your Redis address
Password: "yourpassword", // Provide your Redis password if any
DB: 0, // use default DB
})
}
Redis 缓存方法面临的挑战
内存消耗:Redis 中存储的每个用户名大约占用 15 个字节。对于 10 亿个用户名而言,这将需要约 15GB 的内存。
可扩展性:虽然比直接数据库查询更快,但在内存中存储大型数据集的成本很高,可能需要谨慎管理以避免资源耗尽。
方法 3:Bloom 过滤器技术
当内存效率至关重要时,布鲁姆过滤器(Bloom Filter)提供了一种极具吸引力的解决方案。Bloom 过滤器是一种节省空间的概率数据结构,可以快速检查元素(如用户名)是否属于一个集合。代价是它偶尔会产生误报--在用户名不存在的情况下显示该用户名存在。
Bloom 过滤器简化解释
Bloom 过滤器是一种智能、节省空间的工具,用于检查某个项目是否属于一个集合。当你想避免存储大量数据时,它尤其有用。缺点是什么?它可能偶尔会告诉你某个项目在集合中,但实际上不在(假阳性),但它绝不会漏掉实际上在集合中的项目(无假阴性)。
如何使用 Bloom 过滤器
-
Bloom 过滤器使用一个比特数组和几个散列函数。 -
添加项目(如用户名)时,过滤器会使用哈希函数将数组中的某些位翻转为 1。 -
要检查一个项目是否存在,它会通过相同的哈希函数运行该项目。如果所有相应的位都是 1,则项目可能在集合中。如果任何一位为 0,则该项目肯定不在集合中。
为什么建议使用 Bloom 过滤器
-
效率高:它们可以节省内存,并能快速检查是否有东西可能在集合中。 -
应用:它们对于减少不必要的数据库查询或防止重复检查网络服务器非常有用。
简而言之,在需要快速、节省内存的测试时,只要能处理偶尔出现的误报,Bloom 过滤器就是一个强大的工具。
下面介绍如何使用 bloom 软件包在 Go 中实现 Bloom 过滤器:
package main
import (
"fmt"
// https://pkg.go.dev/github.com/bits-and-blooms/bloom/v3#section-readme
"github.com/bits-and-blooms/bloom/v3"
)
func main() {
// Initialize a Bloom Filter
filter := bloom.New(20*1000*1000, 5) // Capacity: 20 million, 5 hash functions
// Add a username to the Bloom Filter
filter.AddString("john_doe")
// Check if a username exists
exists := filter.TestString("john_doe")
fmt.Printf("Username 'john_doe' exists? %v\n", exists)
// Check for a non-existent username
exists = filter.TestString("jane_doe")
fmt.Printf("Username 'jane_doe' exists? %v\n", exists)
}
输出:
Username 'john_doe' exists? true
Username 'jane_doe' exists? false
Bloom 过滤器的可视化解释
下图直观地解释了 Bloom 过滤器的工作原理:
a. 第(a)部分:插入序列
-
序列 "ACCGTAG":试想一下,我们要检查我们的数据集中是否有这个序列。
-
k-mers:序列被分解成更小的部分,称为 "k-mers"(类似于块或片段)。例如,"ACCG"、"CCGT"、"CGTA "和 "GTAG"。
-
散列 k-mers:每个 k-mers 都要经过一组散列函数。这些散列函数会将 k-mers 映射到比特数组中的特定位置。
-
设置位对于每个 k 单词,比特数组中的相应比特都会被设置为 1。比特数组最初都是 0,但随着 k 单词的增加,特定比特会被打开(设置为 1)。
b. 第(b)部分:查询序列
-
查询 "CGTAT":现在,假设我们要检查 "CGTAT "是否在我们的集合中。
-
k-mers:与之前一样,该序列被分解成 k-聚合物,如 "CGTA "和 "GTAT"。
-
检查位这些 k-mers 已散列,我们将检查比特数组中的相应比特:
-
如果所有位都置 1(如 "CGTA"),则表明序列可能在集合中。
-
如果哪怕有一位为 0(如 "GTAT"),也意味着该序列肯定不在集合中。
总结
Bloom 过滤器内存利用率高,可快速检查集合中是否有可能存在的内容。有时,它可能会错误地显示某个项目在集合中,而实际上不在(这就是 "误报")。
Bloom 过滤器在有效检查大规模数据集中是否存在数据是非常好的一种方式,在过滤或加快数据库查询等许多情况下都非常有用。
小布能否通过本次面试拿到 offer 呢?且听下回分解。
