問題引入
問題一:原本有10億個號碼,現在又來了10萬個號碼,要快速準確判斷這10萬個號碼是否在10億個號碼庫中?
問題二:接觸過爬蟲的,應該有這麼一個需求,需要爬蟲的網站千千萬萬,對於一個新的網站url,我們如何判斷這個url我們是否已經爬過了?
問題三:一個郵件系統,有上億的郵件數量,我們要檢測某一個郵箱是否正確傳送了郵件資訊?
問題四:提到Redis做快取查詢,我們需要考慮幾個問題,快取穿透、快取擊穿和快取雪崩。我們該如何解決快取這種快取問題呢?
布隆過濾
布隆過濾器其實就是,一種資料結構,是由一串很長的二進位制向量組成,可以將其看成一個二進位制陣列。既然是二進位制,那麼裡面存放的不是0,就是1,但是初始預設值都是0。
大致的資料結構如下圖:
新增資料:
向布隆過濾器中新增 key 時,會使用多個 hash 函式對 key 進行 hash 算得一個整數索引值然後對位陣列長度進行取模運算得到一個位置,每個 hash 函式都會算得一個不同的位置。再把位陣列的這幾個位置都置為 1 就完成了 add 操作。
獲取資料時:
只需要將這個新的資料通過上面自定義的幾個雜湊函式,分別算出各個值,然後看其對應的地方是否都是1,如果存在一個不是1的情況,那麼我們可以說,該新資料一定不存在於這個布隆過濾器中。
Redis配置
在Redis中要使用布隆過濾器,可以直接參照該文件,文件地址
推薦使用docker使用方式,如果要編譯成so動態庫,則需要執行在Linux環境中。
// 安裝 docker run -p 6377:6379 --name redis-redisbloom redislabs/rebloom:latest
安裝完之後,檢視docker容器。
進入Redis容器,並檢視容器模組狀態。
docker exec -it 4a695ead6577 /bin/sh
redis-cli
127.0.0.1:6379> info Modules
module:name=bf,ver=20205,api=1,filters=0,usedby=[],using=[],options=[]
## 操作演示
新增資料
```shell
// 單個新增
127.0.0.1:6379> bf.add blkey 1
(integer) 1
127.0.0.1:6379> bf.add blkey 2
(integer) 1
127.0.0.1:6379> bf.add blkey 2
(integer) 0
127.0.0.1:6379> bf.add blkey 3
(integer) 1
127.0.0.1:6379> bf.add blkey 4
(integer) 1
// 批量新增
127.0.0.1:6379> bf.madd blkey 5 6 7 8 4
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
5) (integer) 0
通過新增會發現,如果元素已經存在,則返回的是0值。
檢測資料
// 檢測單個值
127.0.0.1:6379> bf.exists blkey 1
(integer) 1
127.0.0.1:6379> bf.exists blkey 2
(integer) 1
127.0.0.1:6379> bf.exists blkey 3
(integer) 1
// 批量檢測
127.0.0.1:6379> bf.mexists blkey 1 2 3 4 5 10
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
5) (integer) 1
6) (integer) 0
通過演示會發現,如果元素不存在,則返回的是0值。
程式碼演示
這裡用composer來對Redis布隆過濾器進行操作。官方也羅列了幾種程式語言的客戶端。
文件地址
composer require palicao/php-rebloom
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Redis;
use Illuminate\Http\Request;
use Palicao\PhpRebloom\BloomFilter;
use Palicao\PhpRebloom\RedisClient;
use Palicao\PhpRebloom\RedisConnectionParams;
use Redis;
/**
* Redis布隆過濾器
* Class BloomFilterController
* @package App\Http\Controllers\Redis
*/
class BloomFilterController
{
private $request;
private $host = '192.168.0.112';
private $port = 6377;
private $bloomFilter;
public function __construct(Request $request)
{
$this->request = $request->all();
$this->bloomFilter = new BloomFilter(
new RedisClient(
new Redis(),
new RedisConnectionParams($this->host, $this->port)
)
);
}
/**
* 新增刪除資料
* @throws \RedisException
* @author kert
*/
public function index()
{
// 文章:https://www.cnblogs.com/ysocean/p/12594982.html
/** @var string $cacheKey 快取key */
$cacheKey = 'bloom';
/** @var int $cacheValue 快取value */
$cacheValue = mt_rand(0, 100);
// 單個新增快取
var_dump('插入快取', $this->bloomFilter->insert((string)$cacheKey, (string)$cacheValue));
// 單個查詢快取
var_dump('驗證快取', $this->bloomFilter->exists((string)$cacheKey, (string)$cacheValue));
/** @var array $batchCacheValue 批量快取value */
$batchCacheValue = [mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100)];
// 批量新增快取
var_dump('批量插入快取', $this->bloomFilter->insertMany((string)$cacheKey, $batchCacheValue));
// 批量獲取快取
var_dump('批量驗證快取', $this->bloomFilter->manyExist((string)$cacheKey, $batchCacheValue));
}
}
記憶體對比
這裡我們通過模擬郵件傳送來對比布隆過濾器和集合各自佔用的記憶體對比。
布隆過濾器
public function email()
{
/** @var string $cacheKey 快取key */
$cacheKey = 'bloom:email';
/** @var array $email 快取郵箱資料 */
$emailArray = [];
for ($i = 0; $i < 1000; $i++) {
array_push($emailArray, $i . 'wangyi@163.com');
}
/** @var array $insertResult 插入結果 */
$insertResult = $this->bloomFilter->insertMany((string)$cacheKey, $emailArray);
foreach ($insertResult as $value) {
if ($value === false) {
echo '插入失敗' . PHP_EOL;
}
}
/** @var array $queryResult 查詢結果 */
$queryResult = $this->bloomFilter->manyExist((string)$cacheKey, $emailArray);
foreach ($queryResult as $value) {
if ($value === false) {
echo '查詢失敗' . PHP_EOL;
}
}
}
集合
public function emailSet()
{
/** @var string $cacheKey 快取key */
$cacheKey = 'set:email';
/** @var array $email 快取郵箱資料 */
$emailArray = [];
for ($i = 0; $i < 1000; $i++) {
array_push($emailArray, $i . 'wangyi@163.com');
}
$redis = new Redis();
$redis->connect($this->host, $this->port);
var_dump($redis->sAddArray($cacheKey, $emailArray));
}
記憶體對比
/**
* 初始記憶體:854.40K
* 布隆過濾器:857.50K ~3k
* 集合:912.52K ~55k
*/
通過對比發現,同樣的郵箱數量,使用set的方式比使用過濾器的方式,記憶體至少多使用18倍多。
案例解決
在文章開頭,我們引入了幾個問題?首先我們想到的第一個技術方案就是通過資料庫查詢。這樣資料更加準確。但是我們需要考慮一個問題,如果資料量很大,沒查詢一次都走資料庫,無疑是給資料庫增加了負擔。
如果我們通過布隆過濾器來實現,既能解決我們實際的需求,也能解決資料庫壓力過重的情況。
下面演示程式碼實現邏輯。
/**
* 檢測某一個手機號是否已經傳送簡訊內容
* @author kert
*/
public function filterMobile()
{
/** @var string $cacheKey 快取key */
$cacheKey = 'bloom:mobile';
/** @var array $email 快取手機號資料(模擬傳送過的手機號) */
$mobileArray = [];
for ($i = 0; $i < 1000; $i++) {
array_push($mobileArray, substr(md5((string)$i), 0, 11));
}
// 插入布隆過濾器
$this->bloomFilter->insertMany((string)$cacheKey, $mobileArray);
// 檢測某一個值是否存在
var_dump($this->bloomFilter->exists((string)$cacheKey, (string)substr(md5((string)100), 0, 11)));
// output bool(true)
}
通過上面的演示,我們不難看出,布隆過濾在對資料檢測是否存在的情況,要比走資料庫好很多。
優缺點分析
通過上面記憶體對比的內容,以及對布隆過濾器實現原理、儲存資料格式的瞭解,我們可以得出布隆過濾器可以節省記憶體,尤其是資料大的情況下。
布隆過濾器是不支援刪除資料的,如果需要刪除資料則需要重建快取資訊。
布隆過濾器使用多次hash計算,也會存在hash衝突情況。這幾會導致一個問題,當檢測過濾器是否存在資料時,檢測到存在,實際不一定存在。同時檢測到不存在,則快取中一定不存在。
總結
布隆過濾器節省記憶體,但是也存在一種誤差。對於開篇提到的幾個案例場景是一種非常不錯的選擇。
本作品採用《CC 協議》,轉載必須註明作者和本文連結