Dans le bon cas d'utilisation, les filtres Bloom semblent magiques. C'est une déclaration audacieuse, mais dans ce didacticiel, nous explorerons cette étrange structure de données, comment l'utiliser au mieux, ainsi que quelques exemples pratiques utilisant Redis et Node.js.
Un filtre Bloom est une structure de données probabiliste à sens unique. Le mot « filtre » peut prêter à confusion dans ce contexte ; filtre signifie que c'est une chose active, un verbe, mais il pourrait être plus facile de le considérer comme un stockage, un nom. Avec un simple filtre bloom vous pouvez faire deux choses :
Il existe des variantes de filtres Bloom qui ajoutent des fonctionnalités supplémentaires telles que la suppression ou la mise à l'échelle, mais elles ajoutent également de la complexité et des limitations. Avant de passer aux variations, il est important de comprendre d’abord un simple filtre bloom. Cet article présente uniquement les filtres Bloom simples.
Avec ces limites, vous bénéficiez de nombreux avantages : taille fixe, cryptage basé sur le hachage et recherches rapides.
Lorsque vous configurez un filtre bloom, vous devez lui spécifier une taille. Cette taille est fixe, donc s'il y a un ou un milliard d'éléments dans le filtre, il ne dépassera jamais la taille spécifiée. À mesure que vous ajoutez des éléments au filtre, la probabilité de faux positifs augmente. Si vous spécifiez un filtre plus petit, le taux de faux positifs augmentera plus rapidement que si vous utilisez un filtre plus grand.
Les filtres Bloom reposent sur le concept de hachage unidirectionnel. Tout comme le stockage correct des mots de passe, les filtres Bloom utilisent un algorithme de hachage pour déterminer l'identifiant unique de l'élément qui y est transmis. Un hachage est essentiellement irréversible et est représenté par une chaîne de caractères apparemment aléatoire. Par conséquent, si quelqu’un accède à un filtre Bloom, celui-ci ne révélera rien directement.
Enfin, les filtres Bloom sont rapides. Cette opération implique beaucoup moins de comparaisons que les autres méthodes et peut être facilement stockée en mémoire, évitant ainsi les accès à la base de données ayant un impact sur les performances.
Maintenant que vous comprenez les limites et les avantages des filtres Bloom, examinons quelques situations dans lesquelles ils peuvent être utilisés.
Paramètres
) qui s'exécutent sur le port par défaut pour que notre exemple fonctionne correctement GETBIT
、SETBIT
),可以提高实施效率。我假设您的系统上安装了 Node.js、npm 和 Redis。您的 Redis 服务器应该在 localhost
.
. add
、contains
和 clear
Nom d'utilisateur unique
Sans filtres Bloom, vous auriez besoin de référencer un tableau contenant chaque nom d'utilisateur jamais utilisé, ce qui peut être très coûteux à grande échelle. Les filtres Bloom vous permettent d'ajouter un élément à chaque fois qu'un utilisateur adopte un nouveau nom. Lorsqu'un utilisateur vérifie si le nom d'utilisateur est pris, tout ce que vous avez à faire est de vérifier le filtre Bloom. Il pourra vous dire avec une certitude absolue si le nom d'utilisateur demandé a été ajouté précédemment. Le filtre peut renvoyer de manière incorrecte que le nom d'utilisateur a été pris alors qu'en fait le nom d'utilisateur n'a pas été pris, mais il s'agit simplement d'une précaution et ne cause aucun préjudice réel (à part le fait que l'utilisateur peut ne pas être en mesure de déclarer "k3w1d00d47") .
Pour illustrer cela, construisons un serveur REST rapide en utilisant Express. Commencez par créer le fichier
, puis exécutez la commande de terminal suivante. package.json
npm 安装bloom-redis --save
npm install express --save
npm install redis --save
La taille de l'option par défaut de
bloom-redis est définie sur 2 Mo. C'est une erreur par prudence, mais c'est assez important. La définition de la taille du filtre Bloom est essentielle : trop grande et vous gaspillez de la mémoire, trop petite et le taux de faux positifs sera trop élevé. Les calculs impliqués dans la détermination de la taille sont complexes et dépassent le cadre de ce didacticiel, mais heureusement, il existe un calculateur de taille de filtre Bloom qui fait le travail sans avoir à déchiffrer un manuel.
Maintenant, créez app.js
comme suit :
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);
Pour exécuter ce serveur : node app.js
。转到浏览器并将其指向:https://localhost:8010/check?username=kyle
。响应应该是:{"username":"kyle","status":"free"}
.
Maintenant, faisons cela en pointant votre navigateur vers 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 -创建“}
.
Depuis le terminal, vous pouvez voir la taille du filtre :
redis-cli strlen 用户名-bloom-filter
.
Maintenant, pour un élément, cela devrait apparaître 338622
.
Maintenant, allez-y et essayez d'ajouter plus de noms d'utilisateur en utilisant la route /save
. Vous pouvez en essayer autant que vous le souhaitez.
Si vous vérifiez à nouveau la taille, vous constaterez peut-être que la taille a légèrement augmenté, mais pas à chaque ajout. Curieux, non ? En interne, le filtre bloom définit des bits individuels (1/0) à différents emplacements de la chaîne stockée dans username-bloom. Cependant, ceux-ci ne sont pas contigus, donc si vous définissez un bit à l'index 0, puis un bit à l'index 10 000, tout le reste sera 0. Pour des raisons pratiques, il n'est pas important de comprendre la mécanique précise de chaque opération au début, sachez simplement que c'est normal et que vous ne stockerez jamais dans Redis plus que ce que vous spécifiez.
Le nouveau contenu sur le site Web peut inciter les utilisateurs à revenir, alors comment montrer du nouveau contenu aux utilisateurs à chaque fois ? En utilisant une approche de base de données traditionnelle, vous ajouteriez une nouvelle ligne à une table contenant l'identifiant de l'utilisateur et l'identifiant de l'histoire, puis interrogeriez la table lorsque vous décidez d'afficher un élément de contenu. Comme vous pouvez l’imaginer, votre base de données se développera très rapidement, notamment à mesure que vos utilisateurs et votre contenu se développeront.
Dans ce cas, les conséquences des faux négatifs (par exemple, ne pas afficher de contenu invisible) sont très faibles, ce qui fait des filtres Bloom une option viable. À première vue, vous pourriez penser que chaque utilisateur a besoin d'un filtre Bloom, mais nous allons utiliser une simple concaténation d'un identifiant d'utilisateur et d'un identifiant de contenu, puis insérer cette chaîne dans notre filtre. De cette façon, nous pouvons utiliser un seul filtre pour tous les utilisateurs.
Dans cet exemple, construisons un autre serveur Express de base qui affiche le contenu. Chaque fois que vous visitez route /show-content/any-username
(où any-username est une valeur sûre d'URL), un nouvel élément de contenu sera affiché jusqu'à ce que le site soit vide de contenu. Dans l'exemple, le contenu est la première ligne des dix meilleurs livres du Projet Gutenberg.
Nous devons installer un autre module npm. Exécuter depuis le terminal :
npm install async --save
Votre nouveau fichier 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' }
Si vous prêtez une attention particulière au temps d'aller-retour dans les outils de développement, vous constaterez que plus vous demandez un seul chemin avec le nom d'utilisateur, plus cela prend de temps. Bien que la vérification du filtre prenne un certain temps, dans ce cas, nous vérifions la présence de plusieurs éléments. Les filtres Bloom sont limités dans ce qu'ils peuvent vous dire, vous testez donc la présence de chaque élément. Bien sûr, dans notre exemple, c'est assez simple, mais tester des centaines de projets est inefficace.
Dans cet exemple, nous allons construire un petit serveur Express qui fera deux choses : accepter de nouvelles données via POST et afficher les données actuelles (à l'aide d'une requête GET). Lorsque de nouvelles données sont envoyées au serveur, l'application vérifie si elles existent dans le filtre. S'il n'existe pas, nous l'ajouterons à la collection dans Redis, sinon nous renverrons null. Une requête GET l'obtiendra de Redis et l'enverra au client.
C'est différent des deux premiers cas, les faux positifs ne sont pas acceptables. Nous utiliserons des filtres bloom comme première ligne de défense. Compte tenu des propriétés des filtres Bloom, nous pouvons seulement être sûrs qu'il n'y a pas quelque chose dans le filtre, donc dans ce cas, nous pouvons continuer à laisser entrer les données. Si le filtre Bloom renvoie des données qui pourraient se trouver dans le filtre, nous vérifions par rapport à la source de données réelle.
那么,我们得到了什么?我们获得了不必每次都检查实际来源的速度。在数据源速度较慢的情况下(外部 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
。此时,过滤器将始终返回正值。
布隆过滤器并不是万能的解决方案,但在适当的情况下,布隆过滤器可以为其他数据结构提供快速、有效的补充。
如果您仔细注意开发工具中的往返时间,您会发现使用用户名请求单个路径的次数越多,所需的时间就越长。虽然检查过滤器需要固定的时间,但在本例中,我们正在检查是否存在更多项目。布隆过滤器能够告诉您的信息有限,因此您正在测试每个项目是否存在。当然,在我们的示例中,它相当简单,但测试数百个项目效率很低。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!