Je développe un serveur de base de données similaire à Cassandra.
Le développement a commencé en C, mais les choses sont devenues très compliquées sans cours.
Actuellement, j'ai tout porté en C ++ 11, mais j'apprends toujours le C ++ "moderne" et j'ai des doutes sur beaucoup de choses.
La base de données fonctionnera avec des paires clé / valeur. Chaque paire a plus d'informations - quand est créé aussi quand il expirera (0 sinon expire). Chaque paire est immuable.
La clé est la chaîne C, la valeur est nulle *, mais au moins pour le moment, j'utilise la valeur comme chaîne C également.
Il y a une IList
classe abstraite . Il est hérité de trois classes
VectorList
- Tableau dynamique C - similaire à std :: vector, mais utiliserealloc
LinkList
- conçu pour les contrôles et la comparaison des performancesSkipList
- la classe qui sera finalement utilisée.
À l'avenir, je pourrais aussi faire de l' Red Black
arbre.
Chacun IList
contient zéro ou plusieurs pointeurs vers des paires, triés par clé.
S'il IList
est devenu trop long, il peut être enregistré sur le disque dans un fichier spécial. Ce fichier spécial est en quelque sorte read only list
.
Si vous devez rechercher une clé,
- la première en mémoire
IList
est recherchée (SkipList
,SkipList
ouLinkList
). - La recherche est ensuite envoyée aux fichiers triés par date
(le plus récent en premier, le plus ancien en dernier).
Tous ces fichiers sont mmap-ed en mémoire. - Si rien n'est trouvé, la clé n'est pas trouvée.
Je n'ai aucun doute sur la mise en œuvre des IList
choses.
Ce qui me laisse perplexe actuellement, c'est:
Les paires sont de tailles différentes , elles sont allouées par new()
et elles les ont std::shared_ptr
pointées du doigt.
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];
};
La variable membre "buffer" est celle dont la taille est différente. Il stocke la clé + la valeur.
Par exemple, si la clé est de 10 caractères et que la valeur est de 10 octets supplémentaires, l'objet entier sera sizeof(Pair::Blob) + 20
(le tampon a une taille initiale de 2, en raison de deux octets de fin nuls)
Cette même disposition est également utilisée sur le disque, donc je peux faire quelque chose comme ceci:
// 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);
Cependant, cette taille différente est un problème sur beaucoup d'endroits avec du code C ++.
Par exemple, je ne peux pas utiliser std::make_shared()
. C'est important pour moi, car si j'ai 1 million de paires, j'aurais 2 millions d'allocations.
De l'autre côté, si je fais du "buffer" dans un tableau dynamique (par exemple nouveau char [123]), je perdrai le "truc" mmap, j'aurai deux déréférences si je veux vérifier la clé et j'ajouterai un seul pointeur - 8 octets à la classe.
J'ai aussi essayé de « tirer » tous les membres de Pair::Blob
dans Pair
, de sorte Pair::Blob
à être juste le tampon, mais quand je l' ai testé, il était assez lent, probablement à cause de la copie des données d'objet autour.
Un autre changement Pair
auquel je pense également est de supprimer la classe et de la remplacer par std::shared_ptr
et de "repousser" toutes les méthodes Pair::Blob
, mais cela ne m'aidera pas avec la Pair::Blob
classe de taille variable .
Je me demande comment je peux améliorer la conception des objets afin d'être plus convivial en C ++.
Le code source complet est ici:
https://github.com/nmmmnu/HM3
IList::remove
ou quand IList est détruit. Cela prend beaucoup de temps, mais je vais le faire dans un fil séparé. Ce sera facile car IList le sera std::unique_ptr<IList>
quand même. donc je pourrai le "changer" avec une nouvelle liste et garder l'ancien objet quelque part où je pourrai appeler d-tor.
C string
et les données sont toujours un tampon void *
ou char *
, donc vous pouvez passer un tableau char. Vous pouvez trouver similaire dans redis
ou memcached
. À un moment donné, je pourrais décider d'utiliser std::string
ou d'un tableau de caractères fixe pour la clé, mais souligner que ce sera toujours la chaîne C.
std::map
oustd::unordered_map
? Pourquoi les valeurs (associées aux clés) en sont-ellesvoid*
? Vous auriez probablement besoin de les détruire à un moment donné; comment quand? Pourquoi n'utilisez-vous pas de modèles?