通過Lua指令碼批量插入資料到布隆過濾器
有關布隆過濾器的原理之前寫過一篇部落格: 演算法(3)---布隆過濾器原理
在實際開發過程中經常會做的一步操作,就是判斷當前的key是否存在。
那這篇部落格主要分為三部分:
1、幾種方式判斷當前key是否存在的效能進行比較。
2、Redis實現布隆過濾器並批量插入資料,並判斷當前key值是否存在。
3、針對以上做一個總結。
一、效能對比
主要對以下方法進行效能測試比較:
1、List的 contains 方法
2、Map的 containsKey 方法
3、Google布隆過濾器 mightContain 方法
前提準備
在SpringBoot專案啟動的時候,向 List集合、Map集合、Google布隆過濾器 分佈儲存500萬條
,長度為32位的String
字串。
1、演示程式碼
@Slf4j
@RestController
public class PerformanceController {
/**
* 儲存500萬條資料
*/
public static final int SIZE = 5000000;
/**
* list集合儲存資料
*/
public static List<String> list = Lists.newArrayListWithCapacity(SIZE);
/**
* map集合儲存資料
*/
public static Map<String, Integer> map = Maps.newHashMapWithExpectedSize(SIZE);
/**
* guava 布隆過濾器
*/
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.unencodedCharsFunnel(), SIZE);
/**
* 用來校驗的集合
*/
public static List<String> exist = Lists.newArrayList();
/**
* 計時工具類
*/
public static Stopwatch stopwatch = Stopwatch.createUnstarted();
/**
* 初始化資料
*/
@PostConstruct
public void insertData() {
for (int i = 0; i < SIZE; i++) {
String data = UUID.randomUUID().toString();
data = data.replace("-", "");
//1、存入list
list.add(data);
//2、存入map
map.put(data, 0);
//3、存入本地布隆過濾器
bloomFilter.put(data);
//校驗資料 相當於從這500萬條資料,儲存5條到這個集合中
if (i % 1000000 == 0) {
exist.add(data);
}
}
}
/**
* 1、list 檢視value是否存在 執行時間
*/
@RequestMapping("/list")
public void existsList() {
//計時開始
stopwatch.start();
for (String s : exist) {
if (list.contains(s)) {
log.info("list集合存在該資料=============資料{}", s);
}
}
//計時結束
stopwatch.stop();
log.info("list集合測試,判斷該元素集合中是否存在用時:{}", stopwatch.elapsed(MILLISECONDS));
stopwatch.reset();
}
/**
* 2、檢視map 判斷k值是否存在 執行時間
*/
@RequestMapping("/map")
public void existsMap() {
//計時開始
stopwatch.start();
for (String s : exist) {
if (map.containsKey(s)) {
log.info("map集合存在該資料=============資料{}", s);
}
}
//計時結束
stopwatch.stop();
//獲取時間差
log.info("map集合測試,判斷該元素集合中是否存在用時:{}", stopwatch.elapsed(MILLISECONDS));
stopwatch.reset();
}
/**
* 3、檢視guava布隆過濾器 判斷value值是否存在 執行時間
*/
@RequestMapping("/bloom")
public void existsBloom() {
//計時開始
stopwatch.start();
for (String s : exist) {
if (bloomFilter.mightContain(s)) {
log.info("guava布隆過濾器存在該資料=============資料{}", s);
}
}
//計時結束
stopwatch.stop();
//獲取時間差
log.info("bloom集合測試,判斷該元素集合中是否存在用時:{}", stopwatch.elapsed(MILLISECONDS));
stopwatch.reset();
}
}
2、測試輸出結果
測試結果
這裡其實對每一個校驗是否存在的方法都執行了5次,如果算單次的話那麼,那麼在500萬條資料,且每條資料長度為32位的String型別情況下
,可以大概得出。
1、List的contains方法執行所需時間,大概80毫秒左右。
2、Map的containsKey方法執行所需時間,不超過1毫秒。
3、Google布隆過濾器 mightContain 方法,不超過1毫秒。
總結
Map比List效率高的原因這裡就不用多說,沒有想到的是它們速度都這麼快。我還測了100萬條資料通過list遍歷key時間竟然也不超過1毫秒
。這說明在實際開發過程中,如果資料
量不大的話,用哪裡其實都差不多。
3、佔用記憶體分析
從上面的執行效率來看,Google布隆過濾器 其實沒什麼優勢可言,確實如果資料量小,完全通過上面就可以解決,不需要考慮布隆過濾器,但如果資料量巨大,千萬甚至億級
別那種,用集合肯定不行,不是說執行效率不能接受,而是佔記憶體不能接受。
我們來算下key值為32位元組的500萬條條資料,存放在List集合需要佔多少記憶體。
500萬 * 32 = 16000000位元組 ≈ 152MB
一個集合就佔這麼大記憶體,這點顯然無法接受的。
那我們來算算布隆過濾器所需要佔記憶體
設bit陣列大小為m,樣本數量為n,失誤率為p。
由題可知 n = 500萬,p = 3%(Google布隆過濾器預設為3%,我們也可以修改)
通過公式求得:
m ≈ 16.7MB
是不是可以接收多了。
那麼Google布隆過濾器也有很大缺點
1、每次專案啟動都要重新將資料存入Google布隆過濾器,消費額外的資源。
2、分散式叢集部署架構中,需要在每個叢集節點都要儲存一份相同資料到布隆過濾器中。
3、隨著資料量的加大,布隆過濾器也會佔比較大的JVM記憶體,顯然也不夠合理。
那麼有個更好的解決辦法,就是用redis作為分散式叢集的布隆過濾器。
二、Redis布隆過濾器
1、Redis伺服器搭建
如果你不是用docker,那麼你需要先在伺服器上部署redis,然後單獨安裝支援redis布隆過濾器的外掛rebloom
。
如果你用過docker那麼部署就非常簡單了,只需以下命令:
docker pull redislabs/rebloom # 拉取映象
docker run -p 6379:6379 redislabs/rebloom # 執行容器
這樣就安裝成功了。
2、Lua批量插入指令碼
SpringBoot完整程式碼我這裡就不貼上出來了,文章最後我會把整個專案的github地址附上,這裡就只講下指令碼的含義:
bloomFilter-inster.lua
local values = KEYS
local bloomName = ARGV[1]
local result_1
for k,v in ipairs(values) do
result_1 = redis.call('BF.ADD',bloomName,v)
end
return result_1
1)引數說明
這裡的 KEYS
和 ARGV[1]
都是需要我們在java程式碼中傳入,redisTemplate有個方法
execute(RedisScript<T> script, List<K> keys, Object... args)
- script實體中中封裝批量插入的lua指令碼。
- keys 對於指令碼的 KEYS。
- ARGV[1]對於可變引數第一個,如果輸入多個可變引數,可以可以通過ARGV[2].....去獲取。
2)遍歷
Lua遍歷指令碼有兩種方式一個是ipairs
,另一個是pairs
它們還是有差別的。這裡也不做展開,下面有篇部落格可以參考。
注意
Lua的遍歷和java中遍歷還有有點區別的,我們java中是從0開始,而對於Lua指令碼 k是從1開始的。
3)插入命令
BF.ADD
是往布隆過濾器中插入資料的命令,插入成功返回 true。
3、判斷布隆過濾器元素是否存在Lua指令碼
bloomFilter-exist.lua
local bloomName = KEYS[1]
local value = KEYS[2]
-- bloomFilter
local result_1 = redis.call('BF.EXISTS', bloomName, value)
return result_1
從這裡我們可以很明顯看到, KEYS[1]對於的是keys集合的get(0)位置,所以說Lua遍歷是從1開始的。
BF.EXISTS
是判斷布隆過濾器中是否存在該資料命令,存在返回true。
4、測試
我們來測下是否成功。
@Slf4j
@RestController
public class RedisBloomFilterController {
@Autowired
private RedisService redisService;
public static final String FILTER_NAME = "isMember";
/**
* 儲存 資料到redis布隆過濾器
*/
@RequestMapping("/save-redis-bloom")
public Object saveReidsBloom() {
//資料插入布隆過濾器
List<String> exist = Lists.newArrayList("11111", "22222");
Object object = redisService.addsLuaBloomFilter(FILTER_NAME, exist);
log.info("儲存是否成功====object:{}",object);
return object;
}
/**
* 查詢 當前資料redis布隆過濾器是否存在
*/
@RequestMapping("/exists-redis-bloom")
public void existsReidsBloom() {
//不存在輸出
if (!redisService.existsLuabloomFilter(FILTER_NAME, "00000")) {
log.info("redis布隆過濾器不存在該資料=============資料{}", "00000");
}
//存在輸出
if (redisService.existsLuabloomFilter(FILTER_NAME, "11111")) {
log.info("redis布隆過濾器存在該資料=============資料{}", "11111");
}
}
}
這裡先調插入介面,插入兩條資料,如果返回true則說明成功,如果是同一個資料第一次插入返回成功,第二次插入就會返回false,說明重複插入相同值會失敗。
然後調查詢介面,這裡應該兩條日誌都會輸出,因為上面"00000"是取反的,多了個!號。
我們來看最終結果。
符合我們的預期,說明,redis布隆過濾器從部署到整合SpringBoot都是成功的。
三、總結
下面個人對整個做一個總結吧。主要是思考下,在什麼環境下可以考慮用以上哪種方式來判斷該元素是否存在。
1、資料量不大,且不能有誤差。
那麼用List或者Map都可以,雖然說List判斷該元素是否存在採用的是遍歷集合的方式,在效能在會比Map差,但就像上面測試一樣,100萬的資料,
List遍歷和Map都不超過1毫秒,選誰不都一樣,何必在乎那0.幾毫秒的差異。
2、資料量不大,且允許有誤差。
這就可以考慮用Google布隆過濾器
了,儘管查詢資料效率都差不多,但關鍵是它可以減少記憶體的開銷,這就很關鍵。
3、資料量大,且不能有誤差。
如果說數量大,為了提升查詢元素是否存在的效率,而選用Map的話,我覺得也不對,因為如果資料量大,所佔記憶體也會更大,所以我更推薦用
Redis的map資料結構來儲存資料,這樣可以大大減少JVM記憶體開銷,而且不需要每次重啟都要往集合中儲存資料。
4、資料量大,且允許有誤差。
如果是單體應用,資料量記憶體也可以接收,那麼可以考慮Google布隆過濾器,因為它的查詢速度會比redis要快。畢竟它不需要網路IO開銷。
如果是分散式叢集架構,或者資料量非常大,那麼還是考慮用redis布隆過濾器吧,畢竟它不需要往每一節點都儲存資料,而且不佔用JVM虛擬機器記憶體。
Github地址
:https://github.com/yudiandemingzi/spring-boot-redis-lua
參考
3、Lua泛型for遍歷table時ipairs與pairs的區別
只要自己變優秀了,其他的事情才會跟著好起來(上將10)