面試官問,如何在十億級別用戶中檢查用戶名是否存在?
前言
不知道大家有沒有留意過,在使用一些app注冊的時候,提示你用戶名已經被占用了,需要更換一個,這是如何實現的呢?你可能想這不是很簡單嗎,去數據庫里查一下有沒有不就行了嗎,那么假如用戶數量很多,達到數億級別呢,這又該如何是好?
數據庫方案
第一種方案就是查數據庫的方案,大家都能夠想到,代碼如下:
public class UsernameUniquenessChecker {
private static final String DB_URL = "jdbc:mysql://localhost:3306/your_database";
private static final String DB_USER = "your_username";
private static final String DB_PASSWORD = "your_password";
public static boolean isUsernameUnique(String username) {
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD)) {
String sql = "SELECT COUNT(*) FROM users WHERE username = ?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, username);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
int count = rs.getInt(1);
return count == 0; // If count is 0, username is unique
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return false; // In case of an error, consider the username as non-unique
}
public static void main(String[] args) {
String desiredUsername = "new_user";
boolean isUnique = isUsernameUnique(desiredUsername);
if (isUnique) {
System.out.println("Username '" + desiredUsername + "' is unique. Proceed with registration.");
} else {
System.out.println("Username '" + desiredUsername + "' is already in use. Choose a different one.");
}
}
}
這種方法會帶來如下問題:
- 性能問題,延遲高 。 如果數據量很大,查詢速度慢。另外,數據庫查詢涉及應用程序服務器和數據庫服務器之間的網絡通信。建立連接、發送查詢和接收響應所需的時間也會導致延遲。
- 數據庫負載過高。頻繁執行 SELECT 查詢來檢查用戶名唯一性,每個查詢需要數據庫資源,包括CPU和I/O。
- 可擴展性差。數據庫對并發連接和資源有限制。如果注冊率繼續增長,數據庫服務器可能難以處理數量增加的傳入請求。垂直擴展數據庫(向單個服務器添加更多資源)可能成本高昂并且可能有限制。
緩存方案
為了解決數據庫調用用戶名唯一性檢查的性能問題,引入了高效的Redis緩存。
圖片
public class UsernameCache {
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
private static final int CACHE_EXPIRATION_SECONDS = 3600;
private static JedisPool jedisPool;
// Initialize the Redis connection pool
static {
JedisPoolConfig poolConfig = new JedisPoolConfig();
jedisPool = new JedisPool(poolConfig, REDIS_HOST, REDIS_PORT);
}
// Method to check if a username is unique using the Redis cache
public static boolean isUsernameUnique(String username) {
try (Jedis jedis = jedisPool.getResource()) {
// Check if the username exists in the Redis cache
if (jedis.sismember("usernames", username)) {
return false; // Username is not unique
}
} catch (Exception e) {
e.printStackTrace();
// Handle exceptions or fallback to database query if Redis is unavailable
}
return true; // Username is unique (not found in cache)
}
// Method to add a username to the Redis cache
public static void addToCache(String username) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.sadd("usernames", username); // Add the username to the cache set
jedis.expire("usernames", CACHE_EXPIRATION_SECONDS); // Set expiration time for the cache
} catch (Exception e) {
e.printStackTrace();
// Handle exceptions if Redis cache update fails
}
}
// Cleanup and close the Redis connection pool
public static void close() {
jedisPool.close();
}
}
這個方案最大的問題就是內存占用過大,假如每個用戶名需要大約 20 字節的內存。你想要存儲10億個用戶名的話,就需要20G的內存。
總內存 = 每條記錄的內存使用量 * 記錄數 = 20 字節/記錄 * 1,000,000,000 條記錄 = 20,000,000,000 字節 = 20,000,000 KB = 20,000 MB = 20 GB
布隆過濾器方案
直接緩存判斷內存占用過大,有沒有什么更好的辦法呢?布隆過濾器就是很好的一個選擇。
那究竟什么布隆過濾器呢?
布隆過濾器(Bloom Filter)是一種數據結構,用于快速檢查一個元素是否存在于一個大型數據集中,通常用于在某些情況下快速過濾掉不可能存在的元素,以減少后續更昂貴的查詢操作。布隆過濾器的主要優點是它可以提供快速的查找和插入操作,并且在內存占用方面非常高效。
具體的實現原理和數據結構如下圖所示:
圖片
布隆過濾器的核心思想是使用一個位數組(bit array)和一組哈希函數。
- 位數組(Bit Array) :布隆過濾器使用一個包含大量位的數組,通常初始化為全0。每個位可以存儲兩個值,通常是0或1。這些位被用來表示元素的存在或可能的存在。
- 哈希函數(Hash Functions) :布隆過濾器使用多個哈希函數,每個哈希函數可以將輸入元素映射到位數組的一個或多個位置。這些哈希函數必須是獨立且具有均勻分布特性。
那么具體是怎么做的呢?
- 添加元素:如上圖所示,當將字符串“xuyang”,“alvin”插入布隆過濾器時,通過多個哈希函數將元素映射到位數組的多個位置,然后將這些位置的位設置為1。
- 查詢元素:當要檢查一個元素是否存在于布隆過濾器中時,通過相同的哈希函數將元素映射到位數組的相應位置,然后檢查這些位置的位是否都為1。如果有任何一個位為0,那么可以確定元素不存在于數據集中。但如果所有位都是1,元素可能存在于數據集中,但也可能是誤判。
本身redis支持布隆過濾器的數據結構,我們用代碼簡單實現了解一下:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class BloomFilterExample {
public static void main(String[] args) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
JedisPool jedisPool = new JedisPool(poolConfig, "localhost", 6379);
try (Jedis jedis = jedisPool.getResource()) {
// 創建一個名為 "usernameFilter" 的布隆過濾器,需要指定預計的元素數量和期望的誤差率
jedis.bfCreate("usernameFilter", 10000000, 0.01);
// 將用戶名添加到布隆過濾器
jedis.bfAdd("usernameFilter", "alvin");
// 檢查用戶名是否已經存在
boolean exists = jedis.bfExists("usernameFilter", "alvin");
System.out.println("Username exists: " + exists);
}
}
}
在上述示例中,我們首先創建一個名為 "usernameFilter" 的布隆過濾器,然后使用 bfAdd 將用戶名添加到布隆過濾器中。最后,使用 bfExists 檢查用戶名是否已經存在。
優點:
- 節約內存空間,相比使用哈希表等數據結構,布隆過濾器通常需要更少的內存空間,因為它不存儲實際元素,而只存儲元素的哈希值。如果以 0.001 誤差概率存儲 10 億條記錄,只需要 1.67 GB 內存,對比原來的20G,大大的減少了。
- 高效的查找, 布隆過濾器可以在常數時間內(O(1))快速查找一個元素是否存在于集合中,無需遍歷整個集合。
缺點:
- 誤判率存在:布隆過濾器在判斷元素是否存在時,有一定的誤判率。這意味著在某些情況下,它可能會錯誤地報告元素存在,但不會錯誤地報告元素不存在。
- 不能刪除元素:布隆過濾器通常不支持從集合中刪除元素,因為刪除一個元素會影響其他元素的哈希值,增加了誤判率。
總結
Redis 布隆過濾器的方案為大數據量下唯一性驗證提供了一種基于內存的高效解決方案,它需要在內存消耗和錯誤率之間取得一個平衡點。當然布隆過濾器還有更多應用場景,比如防止緩存穿透、防止惡意訪問等。