Hashicrop's Memory Datenbank
Das GO Module MemDB ermöglicht eine schnelle Datenbank im Arbeitsspeicher. HashiCorp nutzt diese u.a. in Consul oder Vault.
Die Datenbank bietet von ACID Atomicity, Consistency and Isolation. Den letzten Punkt Durability erfüllt sie nicht.
Mit den sehr ausgebauten Indexen kann auf die Daten fast wie in einer relationalen Datenbank zugegriffen werden und durch Transaktionen können Änderungen bei Fehlern auch zurückgesetzt werden. Weitere nützliche Aktionen sind das Erstellen eines “Snapshots” um diesen z.B. parallel zu speichern und “Watchers” auf die ich in diesem Artikel aber nicht weiter eingehen möchte.
Schema
Vor der Datenbankinitialisierung muss ein Schmea festgelegt werden. In diesem werden die Tabellen und ihre Indexe definiert. In folgenden Beispielen werden in der Datenbank Quadranten (X:Y) gespeichert. Diese haben eine UUID4 als ID und einen Typen (string). MemDB arbeitet direkt mit dem Struct und benötigt keine Tags wie z.B. die meisten SQL Treiber.
type Quadrant struct {
ID string
X int
Y int
Type string
}
Das Schema mit der Tabelle Quadrant wird wie folgt definiert.
schema := &memdb.DBSchema{
Tables: map[string]*memdb.TableSchema{
"quadrant": {
Name: "quadrant",
Indexes: map[string]*memdb.IndexSchema{
// indexes
"id": {
Name: "id",
Unique: true,
Indexer: &memdb.UUIDFieldIndex{Field: "ID"},
},
},
},
},
}
Initialisierung
Zur Initialisierung wird das Schmea übergeben. Falls das Schema nicht valide ist, wirft die Funktion einen Error.
// create db
db, err := memdb.NewMemDB(schema)
if err != nil {
panic(err)
}
Transaction
Um eine Aktion auszuführen muss eine Transaktion erstellt werden. Diese ist entweder im Modus Nur-Lesen (false
) oder Schreiben und Lesen (true
). Um die Datenbank nicht unnötig zu blockieren sollte eine Transaktion möglichst kurz vor den Vorgängen erstellt werden und sobald möglich mit Commit
oder Abort
wieder beendet werden.
isWriting := true
txn := db.Txn(isWriting)
defer txn.Abort()
// Datenbank Aktionen
// txn.First(...)
// Wird nur für Transaktionen die auch schreiben benötigt.
txn.Commit()
Elemente anlegen (Insert)
Beim Anlegen eines Elements muss angegeben werden in welche Tabelle aus dem Schema es hinzugefügt werden soll.
txn := db.Txn(true)
defer txn.Abort()
quadrant := &Quadrant{
ID: "e2319ab0-d3bc-4d58-8aa3-bdeea293deda",
X: 1,
Y: 1,
Type: "wasser"
}
txn.Insert("quadrant", quadrant)
txn.Commit()
Elemente löschen (Delete)
Zum Löschen eines Elements kann dieses mit Angabe der Tabelle an die Delete
Funktion übergeben werden.
txn.Delete("quadrant", quadrant)
Abfragen
Um Einträge aus der Datenbank abzufragen muss immer der Tabellennamen angegeben werden. MemDB gibt immer ein Interface zurück, das anschließend konvertiert werden muss.
Einzelner Eintrag
txn := db.Txn(false)
defer txn.Abort()
// tabelle, index, ...index felder (id)
raw, err := txn.First("quadrant", "id", "e2319ab0-d3bc-4d58-8aa3-bdeea293deda")
if err != nil {
panic(err)
}
quadrant := raw.(*Quadrant)
Für gruppierte Index Felder müssen dementsprechend mehr Parameter übergeben werden.
// tabelle, index, ...index felder (x,y)
raw, err := txn.First("quadrant", "position", "1", "1")
Mehrere Einträge
Mit Get
kann durch alle passenden Einträge im Index iteriert werden. Wie bei der einzelnen Abfrage muss die Tabelle und der Index angegeben werden. Optional können auch die Index Felder mit einem Wert versehen werden um z.B. nur alle Elemente mit dem Type: "wasser"
zu bekommen.
it, err := txn.Get("quadrant", "type", "wasser")
if err != nil {
panic(err)
}
for obj := it.Next(); obj != nil; obj = it.Next() {
quadrant := obj.(*Quadrant)
fmt.Printf("%d:%d - %s (%s)", quadrant.X, quadrant.Y, quadrant.Type, quadrant.ID)
}
// Output: 1:1 - wasser (e2319ab0-d3bc-4d58-8aa3-bdeea293deda)
Größere / Kleinere Einträge
Es kann im Index auch durch alle Einträge iteriert werden, die kleiner oder größer als der angegebene Eintrag sind.
// tabelle, index, ...index feld (x)
it, err = txn.LowerBound("quadrant", "x", 0)
if err != nil {
panic(err)
}
fmt.Println("Quadrant between 0 - 10:")
for obj := it.Next(); obj != nil; obj = it.Next() {
q := obj.(*Quadrant)
// stop if quadrant over 10
if q.X > 10 {
break
}
fmt.Printf("%d:%d - %s (%s)", quadrant.X, quadrant.Y, quadrant.Type, quadrant.ID)
}
Index
Ein Index hat einen Namen über den später auf Einträge im Index zugegriffen wird. Mit dem Flag Unique
kann angegeben werden, ob unter gleichen Indexparametern mehrere Einträge gefunden werden können (z.B. mehrere Einträge mit denselben Namen oder ID haben). Ein am Index definierter Indexer legt beim Speichern den Index an. Dieser benötigt den Feldnamen im Struct.
UUID Index
MemDB liefert direkt ein UUID Index mit.
// ...
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
Unique: true,
Indexer: &memdb.UUIDFieldIndex{Field: "ID"},
},
},
// ...
String Index
Im Beispiel hat das Struct ein Feld für den Typen. Um zu ermöglichen, dass mehrere Quadranten denselben Typen haben, wird Unique
auf false
gesetzt.
// ...
Indexes: map[string]*memdb.IndexSchema{
"type": {
Name: "type",
Unique: false,
Indexer: &memdb.StringFieldIndex{Field: "Type"},
},
},
// ...
Int Index
Für die Position können 2 Indexe angelegt werden. Mehrere Einträge können auf der X oder der Y Achse vorhanden sein.
// ...
Indexes: map[string]*memdb.IndexSchema{
"x": {
Name: "x",
Unique: false,
Indexer: &memdb.IntFieldIndex{Field: "X"},
},
"y": {
Name: "y",
Unique: false,
Indexer: &memdb.IntFieldIndex{Field: "Y"},
},
},
// ...
Gruppierter Index
Um ein exakten Quadranten zu erhalten kann neben der ID auch die Position verwendet werden. Da die Kombination aus X
und Y
Unique
ist können diese beiden Felder auch zusammen einen Index ergeben. Hierfür bietet MemDB die CompoundIndex
an.
// ...
Indexes: map[string]*memdb.IndexSchema{
"position": {
Name: "position",
Unique: true,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.IntFieldIndex{Field: "X"},
&memdb.IntFieldIndex{Field: "Y"},
},
},
},
},
// ...
Es ist auch möglich unterschiedliche FieldIndex Elemente zu gruppieren. Falls z.B. alle Einträge auf der X-Achse mit einem bestimmten Typ benötigt werden.
// ...
Indexes: map[string]*memdb.IndexSchema{
"xtype": {
Name: "xtype",
Unique: true,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.IntFieldIndex{Field: "X"},
&memdb.StringFieldIndex{Field: "Type"},
},
},
},
},
// ...
Snapshot
MemDB kann als Snapshot einen Klon der Datenbank erstellen. Änderungen auf der eigentlichen Datenbank lassen diesen unangetastet. So kann regelmäßig ein Klon erstellt werden und dieser in einem anderen Thread oder goroutine gespeichert werden ohne die den laufenden Prozess aufzuhalten. Dies eignet sich für Prozesse in denen die Geschwindigkeit wichtiger als die Persistents ist. Der Snapshot kann genau so durchsucht werden wie die normale Datenbank.
snapshot := db.Snapshot()