Ich entwickle einen Datenbankserver ähnlich wie Cassandra.
Die Entwicklung wurde in C begonnen, aber ohne Unterricht wurde es sehr kompliziert.
Momentan habe ich alles in C ++ 11 portiert, aber ich lerne immer noch "modernes" C ++ und habe Zweifel an vielen Dingen.
Die Datenbank funktioniert mit Schlüssel / Wert-Paaren. Jedes Paar hat weitere Informationen - wann wird es erstellt, auch wenn es abläuft (0, wenn es nicht abläuft). Jedes Paar ist unveränderlich.
Schlüssel ist C-Zeichenfolge, Wert ist ungültig *, aber zumindest für den Moment arbeite ich auch mit dem Wert als C-Zeichenfolge.
Es gibt abstrakte IList
Klassen. Es wird von drei Klassen geerbt
VectorList
- Dynamisches Array C - ähnlich wie std :: vector, verwendet jedochrealloc
LinkList
- für Überprüfungen und Leistungsvergleiche gemachtSkipList
- die Klasse, die endlich verwendet wird.
In Zukunft könnte ich auch Red Black
Baum machen.
Jedes IList
enthält null oder mehr Zeiger auf Paare, sortiert nach Schlüsseln.
Wenn es IList
zu lang wird, kann es in einer speziellen Datei auf der Festplatte gespeichert werden. Diese spezielle Datei ist eine Art read only list
.
Wenn Sie nach einem Schlüssel suchen müssen,
- zuerst im Speicher
IList
wird gesucht (SkipList
,SkipList
oderLinkList
). - Anschließend wird die Suche an die nach Datum sortierten Dateien gesendet
(neueste Datei zuerst, älteste Datei - letzte).
Alle diese Dateien werden im Speicher mmaped. - Wenn nichts gefunden wird, wird der Schlüssel nicht gefunden.
Ich habe keine Zweifel an der Umsetzung der IList
Dinge.
Was mich derzeit verwirrt, ist Folgendes:
Die Paare sind unterschiedlich groß, sie werden von zugeordnet new()
und sie haben std::shared_ptr
auf sie gezeigt.
class Pair{
public:
// several methods...
private:
struct Blob;
std::shared_ptr<const Blob> _blob;
};
struct Pair::Blob{
uint64_t created;
uint32_t expires;
uint32_t vallen;
uint16_t keylen;
uint8_t checksum;
char buffer[2];
};
Die Mitgliedsvariable "buffer" ist die Variable mit unterschiedlicher Größe. Es speichert den Schlüssel + Wert.
Wenn der Schlüssel beispielsweise 10 Zeichen und der Wert weitere 10 Bytes beträgt, ist das gesamte Objekt sizeof(Pair::Blob) + 20
(der Puffer hat aufgrund von zwei nullterminierenden Bytes eine Anfangsgröße von 2).
Das gleiche Layout wird auch auf der Festplatte verwendet, sodass ich so etwas tun kann:
// get the blob
Pair::Blob *blob = (Pair::Blob *) & mmaped_array[pos];
// create the pair, true makes std::shared_ptr not to delete the memory,
// since it does not own it.
Pair p = Pair(blob, true);
// however if I want the Pair to own the memory,
// I can copy it, but this is slower operation.
Pair p2 = Pair(blob);
Diese unterschiedliche Größe ist jedoch an vielen Stellen mit C ++ - Code ein Problem.
Zum Beispiel kann ich nicht verwenden std::make_shared()
. Dies ist wichtig für mich, denn wenn ich 1 Million Paare hätte, hätte ich 2 Millionen Zuweisungen.
Von der anderen Seite, wenn ich "Puffer" für dynamisches Array mache (z. B. neues Zeichen [123]), verliere ich mmap "Trick", ich muss zwei Dereferenzen durchführen, wenn ich den Schlüssel überprüfen möchte, und ich werde einen einzelnen Zeiger hinzufügen - 8 Bytes an die Klasse.
Ich habe auch versucht zu „pull“ alle Mitglieder von Pair::Blob
in Pair
, so Pair::Blob
nur der Puffer zu sein, aber wenn ich es getestet, es war ziemlich langsam, wahrscheinlich wegen des Kopierens der Objektdaten um.
Eine andere Änderung, an die ich auch denke, besteht darin, die Pair
Klasse zu entfernen und durch std::shared_ptr
alle Methoden zu ersetzen und sie zurückzuschieben. Pair::Blob
Dies hilft mir jedoch nicht bei Pair::Blob
Klassen mit variabler Größe .
Ich frage mich, wie ich das Objektdesign verbessern kann, um C ++ freundlicher zu sein.
Der vollständige Quellcode ist hier:
https://github.com/nmmmnu/HM3
IList::remove
oder wenn IList zerstört wird. Es braucht viel Zeit, aber ich werde es in einem separaten Thread tun. Es wird einfach sein, weil IList es std::unique_ptr<IList>
sowieso sein wird. so kann ich es mit einer neuen Liste "wechseln" und das alte Objekt irgendwo aufbewahren, wo ich d-tor aufrufen kann.
C string
und die Daten immer ein Puffer sind void *
oder char *
Sie ein char-Array übergeben können. Sie finden ähnliche in redis
oder memcached
. Irgendwann könnte ich mich entscheiden, ein std::string
char-Array für den Schlüssel zu verwenden oder zu fixieren, aber unterstreiche, dass es immer noch eine C-Zeichenfolge ist.
std::map
oderstd::unordered_map
? Warum gibt es Werte (die Schlüsseln zugeordnet sind)void*
? Sie müssten sie wahrscheinlich irgendwann zerstören; wie wann? Warum verwenden Sie keine Vorlagen?