Heim  >  Artikel  >  CMS-Tutorial  >  Entdecken Sie die Leistungsfähigkeit von Bloom-Filtern mit Node.js und Redis

Entdecken Sie die Leistungsfähigkeit von Bloom-Filtern mit Node.js und Redis

PHPz
PHPzOriginal
2023-09-01 22:53:09987Durchsuche

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

Im richtigen Anwendungsfall sehen Bloom-Filter wie Magie aus. Das ist eine gewagte Aussage, aber in diesem Tutorial werden wir diese seltsame Datenstruktur untersuchen, wie man sie am besten nutzt und einige praktische Beispiele mit Redis und Node.js zeigen.

Ein Bloom-Filter ist eine probabilistische, einseitige Datenstruktur. Das Wort „Filter“ kann in diesem Zusammenhang verwirrend sein; „Filter“ bedeutet, dass es sich um eine aktive Sache, ein Verb, handelt, aber es könnte einfacher sein, es sich als Speicher, ein Substantiv, vorzustellen. Mit einem einfachen Blütenfilter können Sie zwei Dinge tun:

  1. Einen Artikel hinzufügen.
  2. Überprüfen Sie, ob ein Artikel noch nicht hinzugefügt wurde.
Dies sind wichtige Einschränkungen, die es zu verstehen gilt: Sie können weder Elemente löschen noch Elemente in Bloom-Filtern auflisten. Darüber hinaus können Sie nicht feststellen, ob ein Artikel in der Vergangenheit zum Filter hinzugefügt wurde. Hier kommt der probabilistische Charakter von Bloom-Filtern ins Spiel – Fehlalarme sind möglich, Fehlalarme jedoch nicht. Wenn der Filter richtig eingerichtet ist, ist die Wahrscheinlichkeit falsch positiver Ergebnisse sehr gering.

Es gibt Varianten von Bloom-Filtern, die zusätzliche Funktionen wie Löschen oder Skalieren hinzufügen, aber auch Komplexität und Einschränkungen mit sich bringen. Bevor wir zu den Variationen übergehen, ist es wichtig, zunächst einen einfachen Bloom-Filter zu verstehen. In diesem Artikel werden nur einfache Bloom-Filter vorgestellt.

Mit diesen Einschränkungen erhalten Sie viele Vorteile: feste Größe, Hash-basierte Verschlüsselung und schnelle Suchvorgänge.

Wenn Sie einen Bloom-Filter einrichten, müssen Sie eine Größe dafür angeben. Diese Größe ist fest. Wenn der Filter also ein oder eine Milliarde Elemente enthält, wird er niemals über die angegebene Größe hinaus wachsen. Wenn Sie dem Filter weitere Elemente hinzufügen, steigt die Wahrscheinlichkeit falsch positiver Ergebnisse. Wenn Sie einen kleineren Filter angeben, steigt die Falsch-Positiv-Rate schneller an, als wenn Sie einen größeren Filter verwenden.

Bloom-Filter basieren auf dem Konzept des Einweg-Hashings. Ähnlich wie das korrekte Speichern von Passwörtern verwenden Bloom-Filter einen Hashing-Algorithmus, um die eindeutige Kennung des übergebenen Elements zu ermitteln. Ein Hash ist grundsätzlich irreversibel und wird durch eine scheinbar zufällige Zeichenfolge dargestellt. Wenn also jemand Zugang zu einem Bloom-Filter erhält, verrät dieser nichts direkt.

Schließlich sind Bloom-Filter schnell. Dieser Vorgang erfordert weitaus weniger Vergleiche als andere Methoden und kann problemlos im Speicher gespeichert werden, wodurch leistungsbeeinträchtigende Datenbanktreffer verhindert werden.

Da Sie nun die Einschränkungen und Vorteile von Bloom-Filtern verstanden haben, schauen wir uns einige Situationen an, in denen sie verwendet werden können.

Einstellungen

Wir werden Bloom-Filter mit Redis und Node.js veranschaulichen. Redis ist das Speichermedium für Bloom-Filter; es ist schnell, speicherintern und verfügt über einige spezifische Befehle (

), die auf dem Standardport ausgeführt werden, damit unser Beispiel ordnungsgemäß funktioniert GETBITSETBIT),可以提高实施效率。我假设您的系统上安装了 Node.js、npm 和 Redis。您的 Redis 服务器应该在 localhost

In diesem Tutorial werden wir keinen Filter von Grund auf implementieren; Stattdessen konzentrieren wir uns auf die praktische Verwendung eines vorgefertigten Moduls in npm: Bloom-Redis. Bloom-Redis verfügt über eine Reihe sehr netter Methoden:

. addcontainsclear

Wie bereits erwähnt, erfordern Bloom-Filter einen Hashing-Algorithmus, um die eindeutige Kennung eines Artikels zu generieren. Bloom-Redis verwendet den bekannten MD5-Algorithmus, der gut funktioniert, obwohl er möglicherweise nicht für Bloom-Filter geeignet ist (etwas langsam, etwas übertrieben).

Einzigartiger Benutzername

Benutzernamen, insbesondere solche, die den Benutzer in der URL identifizieren, müssen eindeutig sein. Wenn Sie eine Anwendung erstellen, die es Benutzern ermöglicht, ihren Benutzernamen zu ändern, möchten Sie möglicherweise einen Benutzernamen, der niemals verwendet wird, um Verwirrung und Angriffe bei Benutzernamen zu vermeiden.

Ohne Bloom-Filter müssten Sie auf eine Tabelle verweisen, die jeden jemals verwendeten Benutzernamen enthält, was im großen Maßstab sehr teuer sein kann. Mit Bloom-Filtern können Sie jedes Mal ein Element hinzufügen, wenn ein Benutzer einen neuen Namen annimmt. Wenn ein Benutzer prüft, ob der Benutzername vergeben ist, müssen Sie lediglich den Bloom-Filter überprüfen. Dadurch können Sie mit absoluter Sicherheit feststellen, ob der angeforderte Benutzername bereits zuvor hinzugefügt wurde. Der Filter gibt möglicherweise fälschlicherweise zurück, dass der Benutzername vergeben wurde, obwohl der Benutzername tatsächlich nicht vergeben wurde. Dies ist jedoch nur eine Vorsichtsmaßnahme und verursacht keinen wirklichen Schaden (abgesehen davon, dass der Benutzer möglicherweise nicht „k3w1d00d47“ angeben kann). .

Um dies zu veranschaulichen, erstellen wir einen schnellen REST-Server mit Express. Erstellen Sie zunächst die

-Datei und führen Sie dann den folgenden Terminalbefehl aus. package.json

npm 安装bloom-redis --save

npm install express --save

npm install redis --saveDie Standardoptionsgröße von

bloom-redis ist auf 2 MB eingestellt. Das ist aus Vorsicht falsch, aber es ist ziemlich groß. Die Einstellung der Größe des Bloom-Filters ist von entscheidender Bedeutung: Ist sie zu groß, wird Speicher verschwendet, ist sie zu klein, ist die Falsch-Positiv-Rate zu hoch. Die Berechnung der Größe ist komplex und würde den Rahmen dieses Tutorials sprengen, aber zum Glück gibt es einen Bloom-Filter-Größenrechner, der diese Aufgabe erledigt, ohne dass man ein Lehrbuch knacken muss.

Jetzt erstellen Sie app.js wie folgt:

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);

So führen Sie diesen Server aus: node app.js。转到浏览器并将其指向:https://localhost:8010/check?username=kyle。响应应该是:{"username":"kyle","status":"free"}.

Lassen Sie uns dies nun tun, indem Sie Ihren Browser auf 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 -创建“} richten.

Vom Terminal aus können Sie die Größe des Filters sehen: redis-cli strlen 用户名-bloom-filter.

Jetzt sollte für einen Artikel 338622 angezeigt werden.

Jetzt können Sie versuchen, über die Route /save weitere Benutzernamen hinzuzufügen. Sie können so viele ausprobieren, wie Sie möchten.

Wenn Sie die Größe noch einmal überprüfen, stellen Sie möglicherweise fest, dass die Größe leicht zugenommen hat, jedoch nicht bei jeder Ergänzung. Neugierig, oder? Intern setzt der Bloom-Filter einzelne Bits (1/0) an verschiedenen Stellen in der in username-bloom gespeicherten Zeichenfolge. Diese sind jedoch nicht zusammenhängend. Wenn Sie also ein Bit auf Index 0 und dann ein Bit auf Index 10.000 setzen, ist alles dazwischen 0. Aus praktischen Gründen ist es zunächst nicht wichtig, die genaue Mechanik jedes Vorgangs zu verstehen. Sie müssen sich nur darüber im Klaren sein, dass dies normal ist und Sie niemals mehr in Redis speichern werden, als Sie angeben.

Frischer Inhalt

Neue Inhalte auf der Website können Benutzer dazu verleiten, wiederzukommen. Wie kann man den Benutzern also jedes Mal neue Inhalte zeigen? Bei einem herkömmlichen Datenbankansatz fügen Sie einer Tabelle eine neue Zeile hinzu, die die Benutzer-ID und die Story-ID enthält, und fragen dann die Tabelle ab, wenn Sie sich für die Anzeige eines Inhalts entscheiden. Wie Sie sich vorstellen können, wächst Ihre Datenbank sehr schnell, insbesondere wenn Ihre Benutzer und Inhalte wachsen.

In diesem Fall sind die Folgen falsch negativer Ergebnisse (z. B. das Nicht-Anzeigen unsichtbarer Inhalte) sehr gering, sodass Bloom-Filter eine praktikable Option sind. Auf den ersten Blick könnte man denken, dass jeder Benutzer einen Bloom-Filter benötigt, aber wir verwenden eine einfache Verkettung einer Benutzer-ID und einer Inhalts-ID und fügen diese Zeichenfolge dann in unseren Filter ein. Auf diese Weise können wir einen einzigen Filter für alle Benutzer verwenden.

In diesem Beispiel erstellen wir einen weiteren einfachen Express-Server, der Inhalte anzeigt. Jedes Mal, wenn Sie Route /show-content/any-username besuchen (wobei any-username ein beliebiger URL-sicherer Wert ist), wird ein neuer Inhalt angezeigt, bis die Website keinen Inhalt mehr enthält. Im Beispiel handelt es sich bei dem Inhalt um die erste Zeile der zehn besten Project Gutenberg-Bücher.

Wir müssen ein weiteres NPM-Modul installieren. Vom Terminal aus ausführen: npm install async --save

Ihre neue app.js-Datei:

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' }
      

Wenn Sie die Roundtrip-Zeit in den Entwicklungstools sorgfältig beachten, werden Sie feststellen, dass es umso länger dauert, je öfter Sie einen einzelnen Pfad mit dem Benutzernamen anfordern. Während die Überprüfung des Filters eine bestimmte Zeit in Anspruch nimmt, prüfen wir in diesem Fall, ob weitere Elemente vorhanden sind. Bloom-Filter können Ihnen nur begrenzte Informationen liefern, sodass Sie das Vorhandensein jedes Elements testen. In unserem Beispiel ist es natürlich ziemlich einfach, aber das Testen von Hunderten von Projekten ist ineffizient.

Veraltete Daten

In diesem Beispiel bauen wir einen kleinen Express-Server, der zwei Dinge tut: neue Daten per POST akzeptieren und die aktuellen Daten anzeigen (mittels einer GET-Anfrage). Wenn neue Daten an den Server gesendet werden, prüft die Anwendung, ob sie im Filter vorhanden sind. Wenn es nicht existiert, fügen wir es der Sammlung in Redis hinzu, andernfalls geben wir null zurück. Eine GET-Anfrage ruft es von Redis ab und sendet es an den Client.

Dies unterscheidet sich von den ersten beiden Fällen. Fehlalarme sind nicht akzeptabel. Als erste Verteidigungslinie werden wir Bloom-Filter einsetzen. Angesichts der Eigenschaften von Bloom-Filtern können wir nur sicher sein, dass sich etwas nicht im Filter befindet. In diesem Fall können wir die Daten also weiterhin hereinlassen. Wenn der Bloom-Filter Daten zurückgibt, die möglicherweise im Filter enthalten sind, prüfen wir dies anhand der tatsächlichen Datenquelle.

那么,我们得到了什么?我们获得了不必每次都检查实际来源的速度。在数据源速度较慢的情况下(外部 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 。此时,过滤器将始终返回正值。

结论

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

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

Das obige ist der detaillierte Inhalt vonEntdecken Sie die Leistungsfähigkeit von Bloom-Filtern mit Node.js und Redis. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn