場景題:假設有40億QQ號,但只有1G內存,如何實現判重?
當數據量比較大時,使用常規(guī)的方式來判重就不行了。例如,使用 MySQL 數據庫判重,或使用 List.contains() 或 Set.contains() 判重就不行了,因為數據量太大會導致內存放不下,或查詢速度太慢等問題。
1.空間占用量預測
正常情況下,如果將 40 億 QQ 號存儲在 Java 中的 int 類型的話,一個 int 占 4 字節(jié)(byte)那么 40 億占用空間大小為:
4000000000*4/1024/1024/1024=14.9 GB
1GB=1024MB,1MB=1024KB,1KB=1024B(byte)
所以,我們無法使用正常的手段進行 40 億 QQ 號的存儲和去重判斷,那怎么實現呢?
2.解決方案
此問題的常見解決方案有兩種:
- 使用位數組 BitMap 實現判重。
- 使用布隆過濾器實現判重。
具體來說。
(1)位數組實現判重
位數組是指使用位(bit)組成的數組,每個 QQ 號使用 1 位(bit)來存儲,如下圖所示:
其中下標用來標識具體的數字,例如以上圖片標識 1、3 數字存在,如果值為 0 表示不存在,這樣的話 40 億占用的位數組空間位 40 億 bit,也就是 4000000000/1024/1024/1024/8=0.465 GB,不到 1G 的內存就可以存儲 40 億 QQ 號了,查詢某個 QQ 號是否在線,只需要看這個 QQ 下標對應的位置是否為 1,1 表示存在,0 表示不存在。
位數組代碼實現
位數組可以使用 Java 自帶的 BitSet 來實現,它位于 java.util 包中,具體實現代碼如下:
import java.util.BitSet;
public class BitmapExample {
public static void main(String[] args) {
// 創(chuàng)建一個BitSet實例
BitSet bitmap = new BitSet();
// 設置第5個位置為1,表示第5個元素存在
bitmap.set(5);
// 檢查第5個位置是否已設置
boolean exists = bitmap.get(5);
System.out.println("Element exists: " + exists); // 輸出: Element exists: true
// 設置從索引10到20的所有位置為1
bitmap.set(10, 21); // 參數是包含起始點和不包含終點的區(qū)間
// 計算bitset中所有值為1的位的數量,相當于計算設置了的元素個數
int count = bitmap.cardinality();
System.out.println("Number of set bits: " + count);
// 清除第5個位置
bitmap.clear(5);
// 判斷位圖是否為空
boolean isEmpty = bitmap.isEmpty();
System.out.println("Is the bitset empty? " + isEmpty);
}
}
(2)布隆過器實現
布隆過濾器是基于位數組實現的,它是一種高效的數據結構,由布隆在 1970 年提出。它主要用于判斷一個元素可能是否存在于集合中,其核心特性包括高效的插入和查詢操作,但存在一定的假陽性(False Positives)可能性。
布隆過濾器實現如下圖所示:
根據 key 值計算出它的存儲位置,然后將此位置標識全部標識為 1(未存放數據的位置全部為 0),查詢時也是查詢對應的位置是否全部為 1,如果全部為 1,則說明數據是可能存在的,否則一定不存在。
布隆過器特性:如果布隆過濾器說一個元素不在集合中,那么它一定不在這個集合中;但如果它說一個元素在集合中,則有可能是不存在的(存在誤差,假陽性)。
布隆過器代碼實現
布隆過濾器的常見實現有以下幾種方式:
使用 Google Guava BloomFilter 實現布隆過濾器,具體實現代碼如下:
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class BloomFilterExample {
public static void main(String[] args) {
// 創(chuàng)建一個布隆過濾器,設置期望插入的數據量為10000,期望的誤判率為0.01
BloomFilter<String> bloomFilter =
BloomFilter.create(Funnels.unencodedCharsFunnel(), 10000, 0.01);
// 向布隆過濾器中插入數據
bloomFilter.put("data1");
bloomFilter.put("data2");
bloomFilter.put("data3");
// 查詢元素是否存在于布隆過濾器中
System.out.println(bloomFilter.mightContain("data1")); // true
System.out.println(bloomFilter.mightContain("data4")); // false
}
}
使用 Hutool 框架 BitMapBloomFilter 實現布隆過濾器,如下代碼所示:
// 初始化
BitMapBloomFilter filter = new BitMapBloomFilter(10);
// 存放數據
filter.add("123");
filter.add("abc");
filter.add("ddd");
// 查找
filter.contains("abc");
使用 Redisson 框架中的 RBloomFilter 實現布隆過濾器,如下代碼所示:
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
// 創(chuàng)建布隆過濾器,設置名稱和期望容量與誤報率
RBloomFilter<String> bloomFilter =
redissonClient.getBloomFilter("myBloomFilter");
bloomFilter.tryInit(10000, 0.03); // 期望容量 10000,誤報率 3%
// 添加元素到布隆過濾器
String element1 = "element1";
bloomFilter.add(element1);
// 判斷元素是否存在
boolean mightExist = bloomFilter.contains(element1);
System.out.println("元素 " + element1 + " 可能存在: " + mightExist);
String element2 = "element2";
boolean mightExist2 = bloomFilter.contains(element2);
System.out.println("元素 " + element2 + " 可能存在: " + mightExist2);
其中 Google Guava BloomFilter 和 Hutool 框架 BitMapBloomFilter 為單機版的布隆過濾器實現,不適用分布式環(huán)境。分布式環(huán)境要使用 Redisson 框架中的 RBloomFilter 來實現布隆過濾器,因為它的數據是保存在 Redis 中間件的,而中間件天生支持分布式系統。
小結
位數組和布隆過濾器的區(qū)別如下:
- 位數組:沒有誤判,但空間利用率低。
- 布隆過濾器:空間利用率高,但存在對已經存在的數據的誤判(不存在的數據沒有誤判)。
因此,如果對精準度要求高可以使用位數組;如果對空間要求苛刻,可以考慮布隆過濾器。