目錄
設定
獨特的使用者名稱
新鮮內容
過時資料
结论
首頁 CMS教程 &#&按 使用 Node.js 和 Redis 探索 Bloom Filter 的魅力

使用 Node.js 和 Redis 探索 Bloom Filter 的魅力

Sep 01, 2023 pm 10:53 PM

使用 Node.js 和 Redis 探索 Bloom Filter 的魅力

在正確的用例中,布隆過濾器看起來就像魔法。這是一個大膽的說法,但在本教程中,我們將探討這種奇怪的資料結構、如何最好地使用它,以及一些使用 Redis 和 Node.js 的實際範例。

布隆過濾器是一種機率性、單向資料結構。在這種情況下,「過濾器」一詞可能會令人困惑;過濾器意味著它是一個活躍的事物,一個動詞,但將它視為存儲,一個名詞可能更容易。使用簡單的布隆過濾器,您可以做兩件事:

  1. 新增一個項目。
  2. 檢查之前是否新增過某個項目。

這些是需要理解的重要限制 - 您無法刪除項目,也無法在布隆過濾器中列出項目。此外,您無法確定過去是否已將某個項目新增至篩選器。這就是布隆過濾器的機率性質發揮作用的地方——誤報是可能的,但誤報則不然。如果過濾器設定正確,誤報的可能性會非常小。

存在布隆過濾器的變體,它們添加了其他功能,例如刪除或縮放,但它們也增加了複雜性和限制。在繼續了解變體之前,首先了解簡單的布隆過濾器非常重要。本文僅介紹簡單的布隆過濾器。

有了這些限制,您可以獲得許多好處:固定大小、基於哈希的加密和快速查找。

當您設定布隆過濾器時,您需要為其指定一個大小。此大小是固定的,因此如果過濾器中有一項或十億項,它永遠不會增長超過指定的大小。當您在篩選器中新增更多項目時,誤報的可能性就會增加。如果您指定了較小的過濾器,則與使用較大的過濾器相比,誤報率會增加得更快。

布隆濾鏡建立在單向雜湊的概念上。與正確儲存密碼非常相似,布隆過濾器使用雜湊演算法來確定傳入其中的項目的唯一識別碼。哈希本質上是無法逆轉的,並且由看似隨機的字串表示。因此,如果有人獲得了布隆過濾器的存取權限,它不會直接洩露任何內容。

最後,布隆濾鏡速度很快。與其他方法相比,此操作涉及的比較次數要少得多,並且可以輕鬆儲存在記憶體中,從而防止影響效能的資料庫命中。

現在您已經了解了布隆過濾器的限制和優點,讓我們來看看可以使用它們的一些情況。

設定

我們將使用 Redis 和 Node.js 來說明 Bloom 過濾器。 Redis 是 Bloom 過濾器的儲存媒體;它速度快,位於記憶體中,並且有一些特定的命令(GETBITSETBIT),可以提高實作效率。我假設您的系統上安裝了 Node.js、npm 和 Redis。您的 Redis 伺服器應該在 localhost 上的預設連接埠上運行,以便我們的範例正常運作。

在本教學中,我們不會從頭開始實作篩選器;而是從頭開始實作篩選器。相反,我們將重點放在 npm 中預先建置模組的實際用途:bloom-redis。 bloom-redis 有一組非常簡潔的方法:addcontainsclear

如前所述,布隆過濾器需要哈希演算法來產生項目的唯一識別碼。 bloom-redis 使用眾所周知的 MD5 演算法,儘管它可能不適合 Bloom 過濾器(有點慢,有點過大),但可以正常工作。

獨特的使用者名稱

用戶名,尤其是在 URL 中標識用戶的用戶名,需要是唯一的。如果您建立一個允許用戶更改用戶名的應用程序,那麼您可能需要一個從未使用過的用戶名,以避免用戶名混淆和被攻擊。

如果沒有布隆過濾器,您需要引用一個包含曾經使用過的每個使用者名稱的表,而大規模時這可能會非常昂貴。布隆過濾器可讓您在使用者每次採用新名稱時新增一個項目。當使用者檢查使用者名稱是否被佔用時,您所需要做的就是檢查布隆過濾器。它將能夠絕對確定地告訴您所要求的使用者名稱是否先前已新增。過濾器可能會錯誤地返回用戶名已被使用,而實際上用戶名尚未被使用,但這只是為了謹慎起見,不會造成任何真正的傷害(除了用戶可能無法聲明“k3w1d00d47”) .

為了說明這一點,讓我們使用 Express 建立一個快速的 REST 伺服器。首先,建立 package.json 文件,然後執行以下終端命令。

npm 安裝bloom-redis --save

#

npm install express --save

#

npm install redis --save

#

bloom-redis 的預設選項大小設定為 2 MB。出於謹慎考慮,這是錯誤的,但它相當大。設定布隆過濾器的大小至關重要:太大會浪費內存,太小則誤報率會太高。確定大小所涉及的數學非常複雜,超出了本教程的範圍,但幸運的是,有一個布隆過濾器大小計算器可以完成這項工作,而無需破解教科書。

現在,建立 app.js 如下:

var
  Bloom         =   require('bloom-redis'),
  express       =   require('express'),
  redis         =   require('redis'),
  
  app,
  client,
  filter;

//setup our Express server
app = express();

//create the connection to Redis
client = redis.createClient();


filter = new Bloom.BloomFilter({ 
  client    : client, //make sure the Bloom module uses our newly created connection to Redis
  key       : 'username-bloom-filter', //the Redis key
  
  //calculated size of the Bloom filter.
  //This is where your size / probability trade-offs are made
  //http://hur.st/bloomfilter?n=100000&p=1.0E-6
  size      : 2875518, // ~350kb
  numHashes : 20
});

app.get('/check', function(req,res,next) {
  //check to make sure the query string has 'username'
  if (typeof req.query.username === 'undefined') {
    //skip this route, go to the next one - will result in a 404 / not found
    next('route');
  } else {
   filter.contains(
     req.query.username, // the username from the query string
     function(err, result) {
       if (err) { 
        next(err); //if an error is encountered, send it to the client
        } else {
          res.send({ 
            username : req.query.username, 
            //if the result is false, then we know the item has *not* been used
            //if the result is true, then we can assume that the item has been used
            status : result ? 'used' : 'free' 
          });
        }
      }
    );
  }
});


app.get('/save',function(req,res,next) {
  if (typeof req.query.username === 'undefined') {
    next('route');
  } else {
    //first, we need to make sure that it's not yet in the filter
    filter.contains(req.query.username, function(err, result) {
      if (err) { next(err); } else {
        if (result) {
          //true result means it already exists, so tell the user
          res.send({ username : req.query.username, status : 'not-created' });
        } else {
          //we'll add the username passed in the query string to the filter
          filter.add(
            req.query.username, 
            function(err) {
              //The callback arguments to `add` provides no useful information, so we'll just check to make sure that no error was passed
              if (err) { next(err); } else {
                res.send({ 
                  username : req.query.username, status : 'created' 
                });
              }
            }
          );
        }
      }
    });
  }
});

app.listen(8010);
登入後複製

要執行此伺服器:node app.js。前往瀏覽器並將其指向:https://localhost:8010/check?username=kyle。回應應該是:{"username":"kyle","status":"free"}

現在,讓我們透過將瀏覽器指向 http://localhost:8010/save?username=kyle 來儲存該使用者名稱。回應將是:{"username":"kyle","status":"created"}。如果回傳位址http://localhost:8010/check?username=kyle,回應將會是{"username":"kyle","status ":"已使用"} .同樣,返回http://localhost:8010/save?username=kyle 將導致{"username":"kyle","status":"not -建立「}

從終端機中,您可以看到過濾器的大小: redis-cli strlen 使用者名稱-bloom-filter

現在,對於一項,它應該顯示 338622

現在,繼續嘗試使用 /save 路由新增更多使用者名稱。您想嘗試多少就嘗試多少。

如果您再次檢查尺寸,您可能會發現尺寸略有增加,但並非每次添加都會增加。很好奇,對吧?在內部,布隆過濾器在保存在 username-bloom 的字串中的不同位置設定各個位元(1/0)。然而,這些並不是連續的,因此如果您在索引 0 處設定一位,然後在索引 10,000 處設定一位,則兩者之間的所有內容都將為 0。對於實際用途,一開始了解每個操作的精確機制並不重要,只需知道這一點即可這是正常的,您在 Redis 中的儲存永遠不會超過您指定的值。

新鮮內容

網站上的新鮮內容可以吸引用戶回頭客,那麼如何每次都向用戶展示新的內容呢?使用傳統的資料庫方法,您可以在表中新增一個包含使用者識別碼和故事識別碼的新行,然後在決定顯示一段內容時查詢該表。正如您可能想像的那樣,您的資料庫將成長得非常快,尤其是隨著使用者和內容的成長。

在這種情況下,漏報(例如不顯示看不見的內容)的後果非常小,這使得布隆過濾器成為一個可行的選擇。乍一看,您可能認為每個使用者都需要一個布隆過濾器,但我們將使用使用者識別碼和內容標識符的簡單串聯,然後將該字串插入到我們的過濾器中。這樣我們就可以對所有使用者使用單一過濾器。

在此範例中,讓我們建立另一個顯示內容的基本 Express 伺服器。每次您造訪路由 /show-content/any-usernameany-username 是任何 URL 安全值)時,都會顯示一個新內容,直到網站沒有內容。在範例中,內容是古騰堡計畫前十名書籍的第一行。

我們需要再安裝一個 npm 模組。從終端運行: npm install async --save

您的新 app.js 檔案:

var
  async         =   require('async'),
  Bloom         =   require('bloom-redis'),
  express       =   require('express'),
  redis         =   require('redis'),
  
  app,
  client,
  filter,
  
  // From Project Gutenberg - opening lines of the top 10 public domain ebooks
  // https://www.gutenberg.org/browse/scores/top
  openingLines = {
    'pride-and-prejudice' : 
      'It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.',
    'alices-adventures-in-wonderland' : 
      'Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it' }
      
登入後複製

如果您仔細注意開發工具中的往返時間,您會發現使用使用者名稱請求單一路徑的次數越多,所需的時間就越長。雖然檢查過濾器需要固定的時間,但在本例中,我們正在檢查是否有更多項目。布隆過濾器能夠告訴您的資訊有限,因此您正在測試每個項目是否存在。當然,在我們的範例中,它相當簡單,但測試數百個專案效率很低。

過時資料

在此範例中,我們將建立一個小型 Express 伺服器,它將執行兩件事:透過 POST 接受新數據,並顯示當前數據(使用 GET 請求)。當新資料被 POST 到伺服器時,應用程式將檢查它是否存在於過濾器中。如果它不存在,我們會將其新增到 Redis 中的集合中,否則我們將傳回 null。 GET 請求將從 Redis 獲取它並將其發送到客戶端。

這與前兩種情況不同,誤報是不行的。我們將使用布隆過濾器作為第一道防線。考慮到布隆過濾器的屬性,我們只能確定某些東西不在過濾器中,因此在這種情況下,我們可以繼續讓資料進入。如果布隆過濾器傳回的資料可能在過濾器中,我們將根據實際資料來源進行檢查。

那么,我们得到了什么?我们获得了不必每次都检查实际来源的速度。在数据源速度较慢的情况下(外部 API、小型数据库、平面文件的中间),确实需要提高速度。为了演示速度,我们在示例中添加 150 毫秒的实际延迟。我们还将使用 console.time / console.timeEnd 来记录 Bloom 过滤器检查和非 Bloom 过滤器检查之间的差异。

在此示例中,我们还将使用极其有限的位数:仅 1024。它很快就会填满。当它填满时,它将显示越来越多的误报 - 您会看到响应时间随着误报率的填满而增加。

该服务器使用与之前相同的模块,因此将 app.js 文件设置为:

var
  async           =   require('async'),
  Bloom           =   require('bloom-redis'),
  bodyParser      =   require('body-parser'),
  express         =   require('express'),
  redis           =   require('redis'),
  
  app,
  client,
  filter,
  
  currentDataKey  = 'current-data',
  usedDataKey     = 'used-data';
  
app = express();
client = redis.createClient();

filter = new Bloom.BloomFilter({ 
  client    : client,
  key       : 'stale-bloom-filter',
  //for illustration purposes, this is a super small filter. It should fill up at around 500 items, so for a production load, you'd need something much larger!
  size      : 1024,
  numHashes : 20
});

app.post(
  '/',
  bodyParser.text(),
  function(req,res,next) {
    var
      used;
      
    console.log('POST -', req.body); //log the current data being posted
    console.time('post'); //start measuring the time it takes to complete our filter and conditional verification process
    
    //async.series is used to manage multiple asynchronous function calls.
    async.series([
      function(cb) {
        filter.contains(req.body, function(err,filterStatus) {
          if (err) { cb(err); } else {
            used = filterStatus;
            cb(err);
          }
        });
      },
      function(cb) {
        if (used === false) {
          //Bloom filters do not have false negatives, so we need no further verification
          cb(null);
        } else {
          //it *may* be in the filter, so we need to do a follow up check
          //for the purposes of the tutorial, we'll add a 150ms delay in here since Redis can be fast enough to make it difficult to measure and the delay will simulate a slow database or API call
          setTimeout(function() {
            console.log('possible false positive');
            client.sismember(usedDataKey, req.body, function(err, membership) {
              if (err) { cb(err); } else {
                //sismember returns 0 if an member is not part of the set and 1 if it is.
                //This transforms those results into booleans for consistent logic comparison
                used = membership === 0 ? false : true;
                cb(err);
              }
            });
          }, 150);
        }
      },
      function(cb) {
        if (used === false) {
          console.log('Adding to filter');
          filter.add(req.body,cb);
        } else {
          console.log('Skipped filter addition, [false] positive');
          cb(null);
        }
      },
      function(cb) {
        if (used === false) {
          client.multi()
            .set(currentDataKey,req.body) //unused data is set for easy access to the 'current-data' key
            .sadd(usedDataKey,req.body) //and added to a set for easy verification later
            .exec(cb); 
        } else {
          cb(null);
        }
      }
      ],
      function(err, cb) {
        if (err) { next(err); } else {
          console.timeEnd('post'); //logs the amount of time since the console.time call above
          res.send({ saved : !used }); //returns if the item was saved, true for fresh data, false for stale data.
        }
      }
    );
});

app.get('/',function(req,res,next) {
  //just return the fresh data
  client.get(currentDataKey, function(err,data) {
    if (err) { next(err); } else {
      res.send(data);
    }
  });
});

app.listen(8012);
登入後複製

由于使用浏览器 POST 到服务器可能会很棘手,所以让我们使用curl 来测试。

curl --data“您的数据放在这里”--header“内容类型:text/plain”http://localhost:8012/

可以使用快速 bash 脚本来显示填充整个过滤器的外观:

#!/bin/bash
for i in `seq 1 500`;
do
  curl --data “data $i" --header "Content-Type: text/plain" http://localhost:8012/
done   
登入後複製

观察填充或完整的过滤器很有趣。由于这个很小,你可以使用 redis-cli 轻松查看。通过在添加项目之间从终端运行 redis-cli get stale-filter ,您将看到各个字节增加。完整的过滤器将为每个字节 \xff 。此时,过滤器将始终返回正值。

结论

布隆过滤器并不是万能的解决方案,但在适当的情况下,布隆过滤器可以为其他数据结构提供快速、有效的补充。

如果您仔细注意开发工具中的往返时间,您会发现使用用户名请求单个路径的次数越多,所需的时间就越长。虽然检查过滤器需要固定的时间,但在本例中,我们正在检查是否存在更多项目。布隆过滤器能够告诉您的信息有限,因此您正在测试每个项目是否存在。当然,在我们的示例中,它相当简单,但测试数百个项目效率很低。

以上是使用 Node.js 和 Redis 探索 Bloom Filter 的魅力的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1664
14
CakePHP 教程
1422
52
Laravel 教程
1316
25
PHP教程
1267
29
C# 教程
1239
24
wordpress文章列表怎麼調 wordpress文章列表怎麼調 Apr 20, 2025 am 10:48 AM

有四種方法可以調整 WordPress 文章列表:使用主題選項、使用插件(如 Post Types Order、WP Post List、Boxy Stuff)、使用代碼(在 functions.php 文件中添加設置)或直接修改 WordPress 數據庫。

如何開始WordPress博客:初學者的分步指南 如何開始WordPress博客:初學者的分步指南 Apr 17, 2025 am 08:25 AM

博客是人們在網上表達觀點、意見和見解的理想平台。許多新手渴望建立自己的網站,卻因擔心技術障礙或成本問題而猶豫不決。然而,隨著平台不斷發展以滿足初學者的能力和需求,現在開始變得比以往任何時候都更容易。 本文將逐步指導您如何建立一個WordPress博客,從主題選擇到使用插件提升安全性和性能,助您輕鬆創建自己的網站。 選擇博客主題和方向 在購買域名或註冊主機之前,最好先確定您計劃涵蓋的主題。個人網站可以圍繞旅行、烹飪、產品評論、音樂或任何激發您興趣的愛好展開。專注於您真正感興趣的領域可以鼓勵持續寫作

如何在 WordPress 中獲取登錄用戶信息以獲得個性化結果 如何在 WordPress 中獲取登錄用戶信息以獲得個性化結果 Apr 19, 2025 pm 11:57 PM

最近,我們向您展示瞭如何通過允許用戶將自己喜歡的帖子保存在個性化庫中來為用戶創建個性化體驗。您可以通過在某些地方(即歡迎屏幕)使用他們的名字,將個性化結果提升到另一個水平。幸運的是,WordPress使獲取登錄用戶的信息變得非常容易。在本文中,我們將向您展示如何檢索與當前登錄用戶相關的信息。我們將利用get_currentuserinfo(); 功能。這可以在主題中的任何地方使用(頁眉、頁腳、側邊欄、頁面模板等)。為了使其工作,用戶必須登錄。因此我們需要使用

如何在父分類的存檔頁面上顯示子分類 如何在父分類的存檔頁面上顯示子分類 Apr 19, 2025 pm 11:54 PM

您想了解如何在父分類存檔頁面上顯示子分類嗎?在自定義分類存檔頁面時,您可能需要執行此操作,以使其對訪問者更有用。在本文中,我們將向您展示如何在父分類存檔頁面上輕鬆顯示子分類。為什麼在父分類存檔頁面上顯示子分類?通過在父分類存檔頁面上顯示所有子分類,您可以使其不那麼通用,對訪問者更有用。例如,如果您運行一個關於書籍的WordPress博客,並且有一個名為“主題”的分類法,那麼您可以添加“小說”、“非小說”等子分類法,以便您的讀者可以

如何在 WordPress 中按帖子過期日期對帖子進行排序 如何在 WordPress 中按帖子過期日期對帖子進行排序 Apr 19, 2025 pm 11:48 PM

過去,我們分享過如何使用PostExpirator插件使WordPress中的帖子過期。好吧,在創建活動列表網站時,我們發現這個插件非常有用。我們可以輕鬆刪除過期的活動列表。其次,多虧了這個插件,按帖子過期日期對帖子進行排序也非常容易。在本文中,我們將向您展示如何在WordPress中按帖子過期日期對帖子進行排序。更新了代碼以反映插件中更改自定義字段名稱的更改。感謝Tajim在評論中讓我們知道。在我們的特定項目中,我們將事件作為自定義帖子類型。現在

wordpress主機怎麼建站 wordpress主機怎麼建站 Apr 20, 2025 am 11:12 AM

要使用 WordPress 主機建站,需要:選擇一個可靠的主機提供商。購買一個域名。設置 WordPress 主機帳戶。選擇一個主題。添加頁面和文章。安裝插件。自定義您的網站。發布您的網站。

如何使用 IFTTT 自動化 WordPress 和社交媒體(及更多) 如何使用 IFTTT 自動化 WordPress 和社交媒體(及更多) Apr 18, 2025 am 11:27 AM

您是否正在尋找自動化 WordPress 網站和社交媒體帳戶的方法? 通過自動化,您將能夠在 Facebook、Twitter、LinkedIn、Instagram 等平台上自動分享您的 WordPress 博客文章或更新。 在本文中,我們將向您展示如何使用 IFTTT、Zapier 和 Uncanny Automator 輕鬆實現 WordPress 和社交媒體的自動化。 為什麼要自動化 WordPress 和社交媒體? 自動化您的WordPre

如何在 WordPress 中顯示查詢數量和頁面加載時間 如何在 WordPress 中顯示查詢數量和頁面加載時間 Apr 19, 2025 pm 11:51 PM

我們的一位用戶詢問其他網站如何在頁腳中顯示查詢數量和頁面加載時間。您經常會在網站的頁腳中看到這一點,它可能會顯示類似以下內容:“1.248秒內64個查詢”。在本文中,我們將向您展示如何在WordPress中顯示查詢數量和頁面加載時間。只需將以下代碼粘貼到主題文件中您喜歡的任何位置(例如footer.php)。 queriesin

See all articles