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 IListclasse abstraite . Il est hérité de trois classes
VectorList- Tableau dynamique C - similaire à std :: vector, mais utilisereallocLinkList- 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 Blackarbre.
Chacun IListcontient zéro ou plusieurs pointeurs vers des paires, triés par clé.
S'il IListest 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
IListest recherchée (SkipList,SkipListouLinkList). - 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 IListchoses.
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_ptrpointé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::Blobdans 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 Pairauquel je pense également est de supprimer la classe et de la remplacer par std::shared_ptret de "repousser" toutes les méthodes Pair::Blob, mais cela ne m'aidera pas avec la Pair::Blobclasse 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::removeou 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 stringet les données sont toujours un tampon void *ou char *, donc vous pouvez passer un tableau char. Vous pouvez trouver similaire dans redisou memcached. À un moment donné, je pourrais décider d'utiliser std::stringou d'un tableau de caractères fixe pour la clé, mais souligner que ce sera toujours la chaîne C.
std::mapoustd::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?