Prinzipien und praktische Anwendungen der Bloom-Filtertechnologie

Vor zwei Wochen habe ich mit Ihnen das Thema über den Geohash-Algorithmus geteilt – technische Prinzipien und praktische Anwendungen von GeoHash. Diese Woche werde ich Ihnen weiterhin ein weiteres Datenstruktur-Tool vorstellen, das in Designideen sehr clever ist – den Bloom-Filter. .

1 Ableitung der Bloom-Filterideen

1.1 Hommage an Zuo Shen

Beginnen wir mit einem praktischen Szenarioproblem.

Tatsächlich wurde ich diesem Szenarioproblem, einschließlich des Konzepts des Bloom-Filters, zum ersten Mal ausgesetzt, als ich mir während meiner Selbststudien-Programmierphase den Zuo Shen-Algorithmuskurs an der Xiaopo Station ansah. Hier zitiere ich speziell dieses Beispiel, um es zu veranschaulichen kann als Einführung in Zuo Shen betrachtet werden. Eine kleine Hommage an alle, die mir auf diesem Weg geholfen haben^_^

1.2 Szenarioprobleme

Das Problem bei diesem Szenario ist folgendes:

Jetzt sind wir dabei, ein Crawler-Programm zusammenzustellen. Wir haben bereits eine Liste, die eine große Anzahl von URLs als Eingabequelle speichert. Wir müssen den Crawler jede URL durchkriechen lassen und sie basierend auf dem Inhalt in einem Netzwerk erweitern der gecrawlten Webseite. Jeder angetroffene URL-Link.

In diesem Prozess kann aufgrund der Netzwerknatur des Netzwerks dieselbe URL wiederholt abgerufen werden. Daher müssen wir einen angemessenen Deduplizierungsmechanismus einrichten, um zu verhindern, dass das Crawler-Traversalprogramm in eine wiederholte Endlosschleife gerät.

Nehmen wir nun an, dass wir insgesamt 1 Milliarde URLs haben und den oben genannten Prozess über den Speicher einer einzelnen Maschine implementieren müssen. Wie kann dieser Prozess entworfen und implementiert werden?

1.3 Grobe Lösung

Der einfachste und intuitivste Weg, dieses Problem zu lösen, besteht darin, eine Sammlung zu verwalten, in der durchquerte URLs gespeichert werden. Alle durchquerten URLs werden zunächst beurteilt, ob sie sich in der Sammlung befinden. Wenn ja, werden sie direkt ignoriert. Wenn nicht, werden sie der Sammlung hinzugefügt . und verarbeiten.

Dadurch kann das Ziel erreicht werden, es muss jedoch das Problem der Kostenverluste berücksichtigt werden. Derzeit gibt es insgesamt 1 Milliarde URLs. Unter der Annahme, dass die Größe jeder URL 16 Byte beträgt, beträgt der erforderliche Gesamtspeicherplatz 1 Milliarde * 16 Byte = 16 GB. Dies gilt für eine einzelne Maschine. Es ist eine zu schwere Nummer.

1.4 Bitmap-Lösung

Wir haben festgestellt, dass wir in diesem Szenario nur die Informationen darüber identifizieren müssen, ob die entsprechende URL in der Sammlung vorhanden ist, ohne den tatsächlichen Inhalt der URL aufzuzeichnen. Basierend auf diesem Punkt stellen wir uns vor, ob wir ein Bit verwenden können, um die Existenz zu bestimmen Status der URL. Wenn es 0 ist, bedeutet dies, dass die URL nicht vorhanden ist. Wenn es 1 ist, bedeutet dies, dass die URL vorhanden ist. Dies kann auch unsere Nutzungsanforderungen erfüllen. Auf diese Weise reduzieren wir die  Arbeit Zum Speichern einer URL sind 16 Byte bis 1 Bit erforderlich. Der verbrauchte Speicherplatz beträgt also nur 1/128 des ursprünglichen Plans.

Die Idee, Bitmap zur Implementierung zu verwenden, ist sicherlich sehr gut, aber wie stellen wir eine Zuordnungsbeziehung zwischen einer URL und einem Bit her? Hier fällt mir natürlich Hashing als Lösung ein:

  • Klären Sie zunächst die Gesamtlänge der Bitmap, vorausgesetzt, sie beträgt m
  • Als nächstes erhalten Sie den Hash-Wert für jede URL.
  • Der Hashwert jeder URL ist Modulo m und der entsprechende Bitindex wird erhalten.

Durch den obigen Prozess haben wir die Zuordnungsbeziehung von URL -> Bit hergestellt und durch die diskrete Natur der Hash-Funktion sichergestellt, dass die jeder URL entsprechenden Bits so gleichmäßig wie möglich an verschiedenen Positionen der Bitmap verteilt sind.

Wenn es jedoch um Hash-Funktionen geht, können wir eines ihrer Probleme nicht vermeiden – Hash-Kollisionen. Da der Eingabebereich der Hash-Funktion unendlich und der entsprechende Ausgabebereich begrenzt ist, ist es unvermeidlich, dass mehrere verschiedene Eingaben erzeugt werden Die gleiche Ausgabe ist ein Phänomen, das als Hash-Kollision bezeichnet wird.

Weitere Informationen zum Hashing finden Sie in meinem vorherigen Artikel – Golang-Map-Implementierungsprinzip.

Darüber hinaus müssen wir, nachdem wir den Hash-Wert für die URL erhalten haben, auch das Modulo basierend auf der inhärenten Bitmap-Länge m verwenden, damit die Wahrscheinlichkeit, dass mehrere verschiedene URLs demselben Bit zugeordnet werden, höher ist.

Am Ende konnten wir immer noch keine strikte Eins-zu-Eins-Zuordnungsbeziehung zwischen einer URL und einem Bit herstellen, was dazu führte, dass das Bit seine erforderliche Genauigkeit bei der Identifizierung, ob die URL existiert, verlor.

1.5 Bloom-Filter

Auf der Grundlage der Bitmap-Lösung passen wir die Art und Weise an, wie wir dem Problem begegnen.

Aufgrund der Existenz von Hash-Kollisionen wissen wir, dass es bei Informationen, die auf Bitmap-Identifikations-URLs basieren, zu „Fehleinschätzungsproblemen“ kommen wird. In welchen Szenarien wird dieses „Fehleinschätzungsproblem“ auftreten und welche Konsequenzen wird es haben?

  • Wenn eine URL existiert, wird Bitmap sie dann fälschlicherweise als nicht vorhanden einschätzen?

Die Antwort ist nein.  Da die Hash-Funktion über die Eigenschaft einer stabilen Einwegzuordnung verfügt, generiert dieselbe URL denselben Hash-Wert, unabhängig davon, wie oft sie eingegeben wird, und wird schließlich demselben Bit zugeordnet. Wenn dies der Fall ist Wurde zuvor eingegeben, muss das entsprechende Bit auf 1 gesetzt werden. Anschließend wird festgestellt, dass es vorhanden ist.

  • Wenn eine URL nicht existiert, wird Bitmap sie dann fälschlicherweise als vorhanden einstufen?

Die Antwort ist vielleicht.  Aufgrund des Problems der Hash-Kollision können mehrere URLs demselben Bit zugeordnet werden. Angenommen, URL1 und URL2 sind beide Bit1 zugeordnet und Bit1 wird nach der Eingabe von URL1 auf 1 gesetzt, also auch wenn URL2 wurde nicht eingegeben. Bei der ersten Eingabe ist die entsprechende Bit1-Position jedoch ebenfalls 1, sodass sie fälschlicherweise als vorhanden eingeschätzt wird.

Bezüglich des Phänomens, dass URLs, die nicht existieren, fälschlicherweise als vorhanden eingeschätzt werden, müssen wir erstens klarstellen, dass es sich hierbei um ein Problem handelt, das nur bei einer kleinen Anzahl von URLs auftritt, die Hash-Kollisionen aufweisen. Zweitens, da unser Crawler-Programm dies ist Da es hauptsächlich in Big-Data-Szenarien verwendet wird, müssen wir mehr darauf achten. Es handelt sich um ein makroskopisches mathematisches Modell und eine Datengröße, daher ist es akzeptabel, das Problem des Fehlens einer kleinen Datenmenge zu akzeptieren.

Daher liegt die auf Bitmap basierende Implementierungslösung bereits innerhalb des Rahmens, den wir akzeptieren können. Als nächstes müssen wir überlegen, wie wir die Wahrscheinlichkeit dieses Fehleinschätzungsproblems durch vernünftiges Prozessdesign so weit wie möglich reduzieren können.

Hier verwenden wir die Methode, die Anzahl der Hash-Funktionen zu erhöhen. Wenn wir beispielsweise die Anzahl der Hash-Funktionen von 1 auf k erhöhen, beträgt die Anzahl der einer URL entsprechenden Bits k, sodass es zu Fehleinschätzungen kommt. Die Prämisse lautet Diese k Bits sind aufgrund der Hash-Kollision alle auf 1 gesetzt. Im Vergleich zu 1 Bit ist die Wahrscheinlichkeit einer Fehleinschätzung stark reduziert, und die genaue Wahrscheinlichkeit einer Fehleinschätzung kann auf der Grundlage mathematischer Ableitung ermittelt werden.

Zu diesem Zeitpunkt wurde allen die Implementierungsidee des Bloom-Filters gezeigt. Gehen wir zurück und definieren sie:

Die Bloom-Filterung besteht aus einer Bitmap und einer Reihe zufälliger Zuordnungsfunktionen. Sie speichert nicht den detaillierten Inhalt der Daten, sondern identifiziert nur Informationen darüber, ob die Daten vorhanden sind. Ihr größter Vorteil besteht darin, dass sie eine sehr gute Speicherplatzausnutzung und Abfrageeffizienz aufweist.

1.6 Vor- und Nachteile von Bloom-Filtern

Nachfolgend fassen wir die Vor- und Nachteile von Bloom-Filtern zusammen:

Vorteil:

  • Platzersparnis: Ein Bit identifiziert die Existenzinformationen eines Datenelements, und nachdem k Hash-Funktionen für die Zuordnung verwendet wurden, kann die Länge m der Bitmap weiter reduziert werden.
  • Effiziente Abfrage: Verwenden Sie k Hash-Funktionen für die Zuordnung. Da k eine Konstante ist, beträgt die tatsächliche Zeitkomplexität O (1).

Mangel:

  • Es besteht das Problem falsch positiver Ergebnisse und Fehleinschätzungen:

Daten, die nicht vorhanden sind, können fälschlicherweise als vorhanden eingeschätzt werden, Daten, die bereits vorhanden sind, können jedoch nicht falsch eingeschätzt werden.

  • Es gibt Probleme mit der Datenlöschung:

Aufgrund des Hash-Kollisionsproblems kann ein Bit von mehreren Eingabedaten verwendet werden und kann daher nicht gelöscht werden. Letztendlich gilt: Je länger die Bitmap verwendet wird, desto mehr Bits werden auf 1 gesetzt und desto höher ist die Wahrscheinlichkeit einer Fehleinschätzung.

Wenn im Extremszenario alle Bits auf 1 gesetzt sind, beträgt die Wahrscheinlichkeit einer Fehleinschätzung für alle nicht vorhandenen Daten 100 %.

Als Reaktion auf das Problem der Schwierigkeit beim Löschen von Bloom-Filterdaten werden im Folgenden zwei Lösungen vorgeschlagen:

Option 1: Datenarchivierung

Diese Lösung eignet sich für detaillierte Datensätze, bei denen wir noch über die gesamte Datenmenge in der Datenbank verfügen. Der Bloom-Filter wird nur als Caching-Schicht zum Schutz der relationalen Datenbank verwendet. Zu diesem Zeitpunkt können wir regelmäßig einige Vorgänge für einige davon ausführen Alte Daten in der Datenbank. Archivieren Sie, erstellen Sie dann regelmäßig eine neue Bitmap mit neuen Daten innerhalb des angegebenen Zeitraums und überschreiben Sie die alte Bitmap, um die Lebensdauer des Bloom-Filters zu verlängern.

Option 2: Kuckucksfilter

Der Kuckucksfilter ist eine andere Art alternativer Algorithmus-Tools, die Datenlöschvorgänge in Karten bis zu einem gewissen Grad unterstützen können. Dies ist ein sehr informatives Thema, und wir werden später einen separaten Artikel schreiben, um es zu beschreiben.

2 Ableitung der Fehleinschätzungsrate des Bloom-Filters

Dieser Teil des mathematischen Schlussfolgerungsprozesses ist die Idee, die mir entstand, nachdem ich während meiner aktuellen Arbeit an der Marketingtechnologie von Didi Chuxing aus dem technischen Austausch von Lehrer Shi in der Gruppe gelernt hatte. Ich muss Lehrer Shi hier besonders würdigen.

2.1 Ableitung der Falsch-Positiv-Rate

Zunächst legen wir die drei Grundparameter des Bloom-Filters fest:

  • Die Länge der Bitmap ist auf  m festgelegt ;
  • Die Anzahl der Hash-Funktionen ist auf k festgelegt  ;
  • Die Anzahl der Eingabeelemente in der Bitmap beträgt  n ; (beachten Sie, dass es sich um die Eingabeelemente und nicht um die auf 1 gesetzten Bits handelt)

Jetzt beginnen wir mit der Wahrscheinlichkeitsableitung:

  • Wenn ein Element einmal über die Hash-Funktion eingegeben und zugeordnet wird, beträgt die Wahrscheinlichkeit, dass ein Bit aufgrund dieser Operation auf 1 gesetzt wird, 1/m;
  • Im Gegenteil, die Wahrscheinlichkeit, dass dieses Bit aufgrund dieser Operation nicht auf 1 gesetzt wird, beträgt 1-1/m;
  • Es ergibt sich weiterhin, dass die Wahrscheinlichkeit, dass dieses Bit nach k Hash-Zuordnungen immer noch nicht auf 1 gesetzt ist, (1-1/m)^k beträgt;
  • Es ergibt sich außerdem, dass die Wahrscheinlichkeit, dass dieses Bit nach der Eingabe von n Elementen immer noch nicht auf 1 gesetzt ist, (1-1/m)^(k·n) beträgt;
  • Im Gegenteil, nach der Eingabe von n Elementen beträgt die Wahrscheinlichkeit, dass 1 Bit auf 1 gesetzt wird, 1-(1-1/m)^(k·n);

Mit der obigen Schlussfolgerung wissen wir, dass jedes Mal, wenn wir ein Element eingeben, die Voraussetzung für eine Fehleinschätzung darin besteht, dass nach der Hash-Zuordnung die entsprechenden k Bits unmittelbar zuvor auf 1 gesetzt wurden, sodass wir die Fehleinschätzung erhalten können. Die Wahrscheinlichkeit des Auftretens Ist -

[1-(1-1/m)^(k·n)]^k

Im Folgenden vereinfachen wir diesen Wahrscheinlichkeitsausdruck für eine Fehleinschätzung basierend auf der Infinitesimaläquivalentregel in der fortgeschrittenen Mathematik.

In der fortgeschrittenen Mathematik wissen wir, dass es bei x->0 (1+x)^(1/x)~e gibt, wobei e eine natürliche Konstante mit einem Wert von ungefähr 2,7182818 ist.

Also gilt, wenn m->∞, 1/m -> 0, also haben wir (1-1/m)^(-m)~e.

Es gibt also (1-1/m)^(k·n)=(1-1/m)^[(-m)·(-k·n/m)]~e^(-k·n/m )

Schließlich erhalten wir, dass bei m->∞ die Fehleinschätzungswahrscheinlichkeit vereinfacht werden kann als - [1-e^(-k·n/m)]^k.

2.2 Ideen zur Parameteroptimierung

Aus Abschnitt 2.1 wissen wir, dass die Wahrscheinlichkeit eines falsch positiven Ergebnisses in einem Bloom-Filter gleichzeitig mit der Länge m der Bimap, der Anzahl der Hash-Funktionen k und der Anzahl n der Eingabeelemente in der Bitmap zusammenhängt.

Unsere nächste Frage lautet: Wie können wir die Wahrscheinlichkeit falsch positiver Ergebnisse im Bloom-Filter durch eine vernünftige Parameterauswahl verringern?

Wenn wir uns diesem Problem stellen, ist die Perspektive, die wir einnehmen, unter der Voraussetzung, dass m und n bekannt sind, wie der Wert von k verwendet werden kann, um die Wahrscheinlichkeit einer Fehleinschätzung zu minimieren. Daher sind m und n für uns Konstanten und k ist die zu seinde Variable erhalten.

Um den Ausdruck der Fehleinschätzungswahrscheinlichkeit weiter zu vereinfachen, zeichnen wir den konstanten Ausdruck e^(n/m) als Konstante t auf, sodass der Ausdruck der Fehleinschätzungswahrscheinlichkeit - f(k)=[1-t^(-k) ] ^k

Wir differenzieren f(k) und indem wir den Minimalwert von f(k) finden (f'(k)=0, f''(k)>0), erhalten wir schließlich, wenn k·n/m=ln2 Wann , die Fehleinschätzungswahrscheinlichkeit f(k) den Minimalwert erreicht.

Daher sollten wir beim Entwerfen der Parameter der Bloom-Filterung die folgenden Ideen befolgen:

  • Stellen Sie zunächst die Bitmap-Länge m auf einen ausreichend großen Wert ein.
  • Zweitens schätzen wir die Anzahl der Elemente n, die in diesem Bloom-Filter gespeichert werden können
  • Als nächstes berechnen wir die entsprechende Anzahl von Hash-Funktionen basierend auf k·n/m=ln2
  • Schließlich berechnen wir die Wahrscheinlichkeit einer möglichen Fehleinschätzung mithilfe des Fehleinschätzungswahrscheinlichkeitsausdrucks [1-e^(-k·n/m)]^k, um zu sehen, ob er die Anforderungen erfüllen kann

Für die Parameterauswahl von Bloom-Filtern steht hier ein vorgefertigter Parameter-Tuning-Simulator zur Verfügung:

https://hur.st/bloomfilter/?n=9000000&p=&m=65000000&k=6

2.3 Auswahl des Hash-Algorithmus

Bei der Auswahl von Hash-Funktionen in Bloom-Filtern legen wir vor allem Wert auf die Rechenleistung. Im Vergleich dazu müssen Hash-Funktionen keine Verschlüsselungseigenschaften haben (es ist nicht erforderlich, dass zwei verschiedene Eingabequellen unterschiedliche Strukturen erzeugen müssen.).

Auf dieser Grundlage erwägen wir nicht die Verwendung verschlüsselter Hash-Algorithmen ähnlich wie sha1 und md5, sondern wählen zwischen unverschlüsselten Hash-Algorithmen. Unter diesen weist der Murmur3-Algorithmus eine relativ gute Leistung auf. Er wird in Kapitel 4 5 veröffentlicht . Im Long-Filter-Implementierungscode entscheiden wir uns für die Verwendung von murmur3 als Hash-Funktion.

murmur3 Github Open-Source-Adresse: https://github.com/spaolacci/murmur3

3 Implementierung des lokalen Bloom-Filters

Das Folgende zeigt den spezifischen Code zum Implementieren des Bloom-Filters basierend auf einer lokalen Bitmap:

3.1 Hash-Kodierung

Das Folgende ist das über murmur3 implementierte Hash-Codierungsmodul, das die Eingabezeichenfolge in einen Hash-Wert vom Typ int32 umwandelt:

import (
    "math"


    "github.com/spaolacci/murmur3"
)


type Encryptor struct {
}


func NewEncryptor() *Encryptor {
    return &Encryptor{}
}


func (e *Encryptor) Encrypt(origin string) int32 {
    hasher := murmur3.New32()
    _, _ = hasher.Write([]byte(origin))
    return int32(hasher.Sum32() % math.MaxInt32)
}

3.2 Bloom-Filterdienst

Das Folgende ist ein Bloom-Filterdienst, der auf einer lokalen Bitmap basiert:

  • m: Die Länge der Bimap, Eingabe durch den Benutzer
  • k: Die Anzahl der vom Benutzer eingegebenen Hash-Funktionen
  • n: Die Anzahl der Elemente im Bloom-Filter, gezählt vom Bloom-Filter
  • Bitmap: Bitmap vom Typ []int, das 32 Bit jedes int-Elements verwendet, sodass die Länge von []int m/32 beträgt. Um das Problem der endlosen Teilung während der Konstruktion zu vermeiden, wird die Slice-Länge zusätzlich um 1 erhöht
  • Encryptor: Hash-Funktionskodierungsmodul
import (
    "context"


    "github.com/demdxx/gocast"
)


type LocalBloomService struct {
    m, k, n   int32
    bitmap    []int
    encryptor *Encryptor
}


func NewLocalBloomService(m, k int32, encryptor *Encryptor) *LocalBloomService {
    return &LocalBloomService{
        m:         m,
        k:         k,
        bitmap:    make([]int, m/32+1),
        encryptor: encryptor,
    }
}

3.3 Abfrageprozess

Das Folgende ist der Abfrageprozess, um zu bestimmen, ob ein Elementwert im Bloom-Filter vorhanden ist:

  • Zunächst wird basierend auf der Methode LocalBloomService.getKEncrypted der Offset-Offset von k Bits ermittelt, der val entspricht.
  • Da jedes int-Element in []int 32 Bit verwendet, ist die entsprechende Indexposition in []int für jeden Offset Offset >> 5, also Offset/32
  • Die Position des Offsets in einem int-Element entspricht Offset & 31, also Offset % 32
  • Wenn ein Bit-Flag 0 ist, bedeutet dies, dass das Element val nicht im Bloom-Filter vorhanden sein darf.
  • Wenn alle Bit-Flags 1 sind, bedeutet dies, dass das Element val wahrscheinlich im Bloom-Filter vorhanden ist.
func (l *LocalBloomService) Exist(val string) bool {
    for _, offset := range l.getKEncrypted(val) {
        index := offset >> 5     // 等价于 / 32
        bitOffset := offset & 31 // 等价于 % 32


        if l.bitmap[index]&(1<<bitOffset) == 0 {
            return false
        }
    }


    return true
}

Die Implementierung zum Erhalten des k-Bit-Offset-Offsets, der einem Elementwert entspricht, ist wie folgt:

  • Bei der ersten Zuordnung wird das Element val als Eingabe verwendet, um den von murmur3 zugeordneten Hash-Wert zu erhalten.
  • Als nächstes wird jedes Mal, wenn die vorherige Runde von Hash-Werten als Eingabe verwendet wird, die Murmur3-Zuordnung abgerufen, um eine neue Runde von Hash-Werten zu erhalten.
  • Geben Sie das Ergebnis zurück, nachdem k Hashwerte erfasst wurden
func (l *LocalBloomService) getKEncrypted(val string) []int32 {
    encrypteds := make([]int32, 0, l.k)
    origin := val
    for i := 0; int32(i) < l.k; i++ {
        encrypted := l.encryptor.Encrypt(origin)
        encrypteds = append(encrypteds, encrypted%l.m)
        if int32(i) == l.k-1 {
            break
        }
        origin = gocast.ToString(encrypted)
    }
    return encrypteds
}

3.4 Prozess hinzufügen

Im Folgenden wird der Vorgang zum Anhängen von Elementen an den Bloom-Filter beschrieben:

  • Jedes Mal, wenn ein neues Element eintrifft, wird n im Bloom-Filter erhöht
  • Rufen Sie die Methode LocalBloomService.getKEncrypted auf, um den k-Bit-Offset-Offset zu erhalten, der dem Elementwert entspricht.
  • Erhalten Sie den Index des Bits in []int durch offset >> 5. Die Idee ist die gleiche wie in Abschnitt 3.3.
  • Erhalten Sie die Bitposition in int durch Offset & 31. Die Idee ist die gleiche wie in Abschnitt 3.3.
  • Setzen Sie durch die |-Operation die entsprechende Bitposition auf 1
  • Wiederholen Sie den obigen Vorgang und setzen Sie alle k Bits auf 1
func (l *LocalBloomService) Set(val string) {
    l.n++
    for _, offset := range l.getKEncrypted(val) {
        index := offset >> 5     // 等价于 / 32
        bitOffset := offset & 31 // 等价于 % 32


        l.bitmap[index] |= (1 << bitOffset)
    }
}

4 Implementieren Sie den Bloom-Filter basierend auf Redis

Das Folgende zeigt den spezifischen Code für die Implementierung des Bloom-Filters basierend auf der Redis-Bitmap:

4.1 Hash-Kodierung

Das Murmur3-Hash-Codierungsmodul ist dasselbe wie die Implementierung im lokalen Bloom-Filtermodul in Abschnitt 3.1 dieses Artikels und wird nicht noch einmal beschrieben:

import (
    "math"


    "github.com/spaolacci/murmur3"
)


type Encryptor struct {
}


func NewEncryptor() *Encryptor {
    return &Encryptor{}
}


func (e *Encryptor) Encrypt(origin string) int32 {
    hasher := murmur3.New32()
    _, _ = hasher.Write([]byte(origin))
    return int32(hasher.Sum32() % math.MaxInt32)
}

4.2 Redis-Client

Redigo Github Open-Source-Adresse: https://github.com/gomodule/redigo

Der auf Redigo basierende Redis-Client-Implementierungscode lautet wie folgt:

  • Basierend auf dem Redis-Verbindungspool werden Verbindungen wiederverwendet. Bei jedem Vorgang muss zuerst die Verbindung aus dem Verbindungspool abgerufen werden. Nach der Verwendung muss die Verbindung manuell wieder in den Pool eingefügt werden.
  • Der Redis-Client kapselt eine Eval-Schnittstelle, die zum Ausführen von Lua-Skripten und zum Abschließen der atomaren Zusammenstellung zusammengesetzter Anweisungen verwendet wird.
import (
    "context"
    "fmt"


    "github.com/demdxx/gocast"
    "github.com/gomodule/redigo/redis"
)


type RedisClient struct {
    pool *redis.Pool
}


func NewRedisClient(pool *redis.Pool) *RedisClient {
    return &RedisClient{
        pool: pool,
    }
}


// 执行 lua 脚本,保证复合操作的原子性
func (r *RedisClient) Eval(ctx context.Context, src string, keyCount int, keysAndArgs []interface{}) (interface{}, error) {
    args := make([]interface{}, 2+len(keysAndArgs))
    args[0] = src
    args[1] = keyCount
    copy(args[2:], keysAndArgs)


    // 获取连接
    conn, err := r.pool.GetContext(ctx)
    if err != nil {
        return -1, err
    }


    // 放回连接池
    defer conn.Close()


    // 执行 lua 脚本
    return conn.Do("EVAL", args...)
}

4.3 Bloom-Filterservice

Definieren Sie das Bloom-Filter-Servicemodul:

  • m: Bitmap-Länge, Eingabe durch den Benutzer
  • k: Anzahl der Hash-Funktionen, Eingabe durch den Benutzer
  • Client: Client, der eine Verbindung zu Redis herstellt
// 布隆过滤器服务
type BloomService struct {
    m, k      int32
    encryptor *Encryptor
    client    *RedisClient
}


// m -> bitmap 的长度; k -> hash 函数的个数;
// client -> redis 客户端;encryptor -> hash 映射器
func NewBloomService(m, k int32, client *RedisClient, encrytor *Encryptor) *BloomService {
    return &BloomService{
        m: m,
        k: k,
        client:    client,
        encryptor: encrytor,
    }
}

4.4 Abfrageprozess

Fragen Sie ab, ob der Eingabeinhalt im Bloom-Filter vorhanden ist:

  • Der Schlüssel entspricht dem Identifikationsschlüsselschlüssel der Bitmap im Bloom-Filter. Elemente, die unterschiedlichen Schlüsseln entsprechen, sind voneinander isoliert.
  • Val entspricht dem Eingabeelement und gehört zur Bitmap, die einem bestimmten Schlüssel entspricht.
  • Rufen Sie die Methode BloomService.getKEncrypted auf, um den Offset-Offset zu erhalten, der k Bits entspricht.
  • Rufen Sie die RedisClient.Eval-Methode auf, um das Lua-Skript auszuführen. Wenn eines der k Bits nicht 1 ist, gibt es false zurück, wenn es nicht existiert, andernfalls gibt es true zurück, wenn es existiert.
// key -> 布隆过滤器 bitmap 对应的 key   val -> 基于 hash 映射到 bitmap 中的值
func (b *BloomService) Exist(ctx context.Context, key, val string) (bool, error) {
    // 映射对应的 bit 位
    keyAndArgs := make([]interface{}, 0, b.k+2)
    keyAndArgs = append(keyAndArgs, key, b.k)
    for _, encrypted := range b.getKEncrypted(val) {
        keyAndArgs = append(keyAndArgs, encrypted)
    }


    rawResp, err := b.client.Eval(ctx, LuaBloomBatchGetBits, 1, keyAndArgs)
    if err != nil {
        return false, err
    }


    resp := gocast.ToInt(rawResp)
    if resp == 1{
        return true,nil
    }
    return false, nil
}

Die Ausführungsmethode zum Zuordnen des Eingabeelements zum K-Bit-Offset-Offset ist getKEncrypted. Die Logik ist dieselbe wie in Abschnitt 3.3 und wird nicht erneut beschrieben.

func (b *BloomService) getKEncrypted(val string) []int32 {
    encrypteds := make([]int32, 0, b.k)
    origin := val
    for i := 0; int32(i) < b.k; i++ {
        encrypted := b.encryptor.Encrypt(origin)
        encrypteds = append(encrypteds, encrypted)
        if int32(i) == b.k-1 {
            break
        }
        origin = gocast.ToString(encrypted)
    }
    return encrypteds
}

Das Folgende ist ein Lua-Skript, das Bitmap-Abfragevorgänge in Stapeln ausführt: k Bits werden abgefragt. Solange ein Bit als 0 markiert ist, wird 0 zurückgegeben; wenn alle Bits als 1 markiert sind, wird 1 zurückgegeben.

const LuaBloomBatchGetBits = `
  local bloomKey = KEYS[1]
  local bitsCnt = ARGV[1]
  for i=1,bitsCnt,1 do
    local offset = ARGV[1+i]
    local reply = redis.call('getbit',bloomKey,offset)
    if (not reply) then
        error('FAIL')
        return 0
    end
    if (reply == 0) then
        return 0
    end
  end
  return 1
`

4.5 Prozess hinzufügen

Der Vorgang zum Hinzufügen eines Eingabeelements zu einem Bloom-Filter ist wie folgt:

  • Der Schlüssel entspricht dem Identifikationsschlüsselschlüssel der Bitmap im Bloom-Filter. Elemente, die unterschiedlichen Schlüsseln entsprechen, sind voneinander isoliert.
  • Val entspricht dem Eingabeelement und gehört zur Bitmap, die einem bestimmten Schlüssel entspricht.
  • Rufen Sie die Methode BloomService.getKEncrypted auf, um den Offset-Offset zu erhalten, der k Bits entspricht.
  • Rufen Sie die Methode RedisClient.Eval auf, um das Lua-Skript auszuführen und alle k Bits auf 1 zu setzen
func (b *BloomService) Set(ctx context.Context, key, val string) error {
    // 映射对应的 bit 位
    keyAndArgs := make([]interface{}, 0, b.k+2)
    keyAndArgs = append(keyAndArgs, key, b.k)
    for _, encrypted := range b.getKEncrypted(val) {
        keyAndArgs = append(keyAndArgs, encrypted)
    }


    rawResp, err := b.client.Eval(ctx, LuaBloomBatchSetBits, 1, keyAndArgs)
    if err != nil {
        return err
    }
    
    resp := gocast.ToInt(rawResp)
    if resp != 1 {
        return fmt.Errorf("resp: %d", resp)
    }
    return nil
}

Basierend auf dem Lua-Skript wird auch die atomare Zusammenstellung zusammengesetzter Anweisungen implementiert und gleichzeitig k Bits auf 1 gesetzt.

const LuaBloomBatchSetBits = `
  local bloomKey = KEYS[1]
  local bitsCnt = ARGV[1]


  for i=1,bitsCnt,1 do
    local offset = ARGV[1+i]
    redis.call('setbit',bloomKey,offset,1)
  end
  return 1
`

5 Einführung in den Projektfall

In dem persönlichen Projekt, das ich zuvor implementiert habe, dem verteilten Timer xtimer, wurden Bloom-Filter als Hilfswerkzeug für die Überprüfung der Aufgaben-Idempotenz verwendet.

Eine ausführliche Einführung in dieses Projekt finden Sie im Artikel „Implementierung des verteilten Timers XTimer basierend auf der Coroutine-Pool-Architektur“.

Die Open-Source-Adresse von xtimer lautet wie folgt: https://github.com/xiaoxuxiansheng/xtimer

Das xtimer-Architekturdiagramm sieht wie folgt aus:

In xtimer konzentriert sich die tatsächliche Ausführung geplanter Aufgaben auf das Executor-Modul, das asynchron vom Upstream-Trigger-Modul gestartet wird. Es kann nur durch eine Slice-Ablaufzeitverlängerungsoperation ähnlich wie bei ack sichergestellt werden, dass die geplanten Aufgaben die Mindestanforderung erfüllen . Semantik, kann aber nicht die Semantik von genau einmal erreichen.

Bevor das Executor-Modul die Aufgabe tatsächlich ausführt, muss es daher den Ausführungsstatus der geplanten Aufgabe in der Datenbank abfragen und die Idempotenzprüfung abschließen. In diesem Prozess verwende ich BloomFilter, um eindeutig zu identifizieren, welcher Teil der Aufgabe nicht ausgeführt wurde Dieses Mal können Sie einen Datenbanküberprüfungsvorgang speichern und direkt in den nachfolgenden Ausführungsprozess einsteigen; für Aufgaben, die von BloomFilter als ausgeführt markiert wurden, müssen Sie die Datenbank ein zweites Mal überprüfen, um die Überprüfung abzuschließen.

Das gesamte Ausführungsflussdiagramm lautet wie folgt:

6 Zusammenfassung

In dieser Ausgabe teile ich Ihnen eine Datenstruktur mit einer sehr cleveren Designidee – dem Bloom-Filter.

Die Bloom-Filterung besteht aus einer Bitmap und einer Reihe zufälliger Zuordnungsfunktionen. Sie speichert nicht den detaillierten Inhalt der Daten, sondern identifiziert nur Informationen darüber, ob ein Datenelement vorhanden ist. Sein größter Vorteil besteht darin, dass er eine sehr gute Raumausnutzung und Abfrage aufweist Effizienz. Seine Existenz Die Nachteile bestehen darin, dass Daten schwer zu löschen sind und eine gewisse Wahrscheinlichkeit von Fehlalarmen und Fehleinschätzungen besteht.

Kleine Werbung am Ende des Artikels:

Chefs sind herzlich eingeladen, meinem persönlichen öffentlichen Account zu folgen: Mr. Xiao Xu’s Programming World

Acho que você gosta

Origin blog.csdn.net/iamonlyme/article/details/133278859
Recomendado
Clasificación