
Une brève note sur une base de données clé-valeur intégrée appelée Coffer
écrite en Golang. Si très brièvement: lorsque la base de données est dans un état arrêté, les données sont sur le disque, au démarrage, les données sont copiées en mémoire. La lecture vient de la mémoire. Pendant l'enregistrement, les données de la mémoire sont modifiées et les modifications sont enregistrées dans le journal du disque. La taille maximale des données stockées est limitée par la taille de la RAM. L'API vous permet de créer des en-têtes pour les enregistrements de base de données et de les utiliser dans les transactions, tout en maintenant la cohérence des données.
Mais d'abord, une petite introduction lyrique. Il était une fois, lorsque l'herbe était plus verte, il m'a fallu intégrer une base de données de valeurs-clés pour l'application go. En regardant autour et en tombant sur différents packages, je n'ai pas trouvé ce que j'aimerais (subjectivement) et j'ai simplement appliqué la solution avec une base de données relationnelle externe. Excellente solution de travail. Mais comme on dit, une cuillère a été trouvée, mais les sédiments sont restés. Tout d'abord, je voulais exactement la base de données native, écrite sur Go, directement native native. Et il y en a, regardez simplement génial. Cependant, il n'y en a pas un million. C'est même surprenant quand on considère qu'un programmeur est rare dans le monde qui n'a pas écrit de base de données, de framework ou de jeu casual dans sa vie.
Eh bien, vous pouvez essayer d'empiler votre vélo sur vos genoux, avec du blackjack et d'autres goodies. Dans le même temps, tout le monde sait, ou du moins devine, que l'écriture même d'une simple base de données de valeurs-clés ne semble simple qu'à première vue. Mais en fait, tout est beaucoup plus amusant (et c'est arrivé). Et j'étais curieux au sujet de l'ACID et inquiet au sujet des transactions. Les vraies transactions sont plus probables au sens financier, car J'étais alors occupé à la fintech.
Sécurité des données
Prenons le cas où, lors du fonctionnement d'une application avec enregistrement actif, l'alimentation électrique de l'ordinateur était recouverte d'un bassin en cuivre et le disque ne se cassait pas. Si à ce moment l'application a ok
reçue de la base de données, les données de cette opération ne seront pas perdues. Si la demande a reçu une réponse négative, alors bien sûr, l'opération n'a pas été terminée. Eh bien, le cas où l'application a envoyé une demande, mais n'a pas reçu de réponse: cette opération n'a probablement pas été terminée, mais il y a une petite chance que l'opération tombe dans le journal, mais exactement au moment où la réponse a été envoyée, une panne de courant s'est produite.
Comment au dernier cas savoir ce qui s'est passé avec les dernières opérations? C'est une question intéressante. Indirectement, vous pouvez le deviner (tirer des conclusions) en examinant la valeur de l'enregistrement qui vous intéresse après le lancement d'une nouvelle application à partir de la base de données. Cependant, si les opérations sont assez fréquentes, je crains que cela n'aidera pas. Vous pouvez voir le fichier du dernier journal (ce sera avec le plus grand nombre), mais manuellement, cela n'est pas pratique. Je pense qu'à l'avenir, vous pouvez ajouter la possibilité d'afficher les journaux dans l'API (naturellement, les journaux dans ce cas ne devraient pas être supprimés).
Franchement, je n'ai pas moi-même tiré le cordon de la prise, car Je ne veux pas risquer le fer pour vérifier la base de données. Dans les tests, je gâche simplement les fichiers journaux normaux, et dans ce cas, tout se passe comme prévu. Cependant, il n'y a aucune expérience dans l'utilisation pratique de la base de données, elle n'a pas fonctionné au niveau du prod et il y a des risques. Cependant, pour les projets pour animaux de compagnie, je pense que la base de données peut être utilisée sans crainte. En général, l'avertissement habituel, aucune garantie.
La base de données n'est actuellement pas protégée contre l'utilisation dans deux applications différentes (ou les mêmes, cela n'a pas d'importance ici) configurées pour fonctionner avec le même répertoire. Tenez compte de ce moment! Et pourtant, puisque la base de données est intégrée, puis en lui passant une sorte de type de référence dans les arguments, cela ne vaut vraiment pas la peine de la changer quelque part dans goroutine parallèle.
La configuration
La base de données a plusieurs paramètres qui peuvent être configurés, cependant, presque tous ont des valeurs par défaut, donc tout peut être ajusté dans une seule ligne courte cof, err, wrn := Db(dirPath).Create()
erreur est renvoyée (si une erreur se produit, continuez à travailler avec la base de données interdit) et d'avertissement, que vous pouvez connaître, mais cela n'interfère pas avec le fonctionnement de la base de données.
Je n'encombrerai pas le texte avec des descriptions encombrantes, si nécessaire, veuillez les regarder dans le fichier Lisezmoi du référentiel - github.com/claygod/coffer/blob/master/README_RU.md#config Notez la méthode du gestionnaire qui connecte le gestionnaire de la transaction, j'écrirai quelques lignes à ce sujet plus bas, je les énumère ici:
- Db (dirPath)
- BatchSize (batchSize)
- LimitRecordsPerLogfile (limitRecordsPerLogfile)
- FollowPause (100 * fois seconde)
- LogsByCheckpoint (1000)
- AllowStartupErrLoadLogs (true)
- MaxKeyLength (maxKeyLength)
- MaxValueLength (maxValueLength)
- MaxRecsPerOperation (1 000 000)
- RemoveUnlessLogs (true)
- LimitMemory (100 * 1000000)
- LimitDisk (1000 * 1000000)
- Handler ("handler1" et handler1)
- Handler ("handler2", & handler2)
- Handlers (map [string] * handler)
- Créer ()
API
Dans la mesure du possible, j'ai rendu l'API simple, et pour une base valeur-clé, ne soyez pas trop intelligent:
- Démarrer - démarrer la base de données
- Arrêter - arrêter la base de données
- StopHard - un arrêt quelles que soient les opérations en cours (je vais peut-être le supprimer)
- Enregistrer - enregistrer un instantané de l'état actuel de la base de données
- Écrire - ajouter un enregistrement à la base de données
- WriteList - ajoute plusieurs enregistrements à la base de données (modes stricts et facultatifs)
- WriteListUnsafe - ajoutez plusieurs enregistrements à la base de données sans égard à la sécurité des données
- Lire - obtenir un enregistrement par clé
- ReadList - obtenir une liste d'enregistrements
- ReadListUnsafe - obtenez une liste d'enregistrements sans égard à la sécurité des données
- Supprimer - supprimer un enregistrement
- DeleteList - supprime plusieurs enregistrements en mode strict / facultatif
- Transaction - exécuter une transaction
- Count - combien d'enregistrements dans la base de données
- CountUnsafe - combien d'enregistrements dans la base de données (un peu plus rapide, mais dangereux)
- RecordsList - une liste de toutes les clés de base de données
- RecordsListUnsafe - une liste de toutes les clés de base de données (un peu plus rapide, mais peu sûr)
- RecordsListWithPrefix - une liste de clés avec le préfixe spécifié
- RecordsListWithSuffix - une liste de clés avec la fin spécifiée
Brève explications sur l'API:
- Mode strict - faites tout ou rien.
- Mode optionnel - faites tout ce qui fonctionne.
- StopHard - cette méthode devrait peut-être être supprimée de l'API jusqu'à ce qu'elle soit décidée.
- Toutes les méthodes RecordsList ne sont pas rapides, car Il n'y a pas d'index dans le magasin en ce moment, alors qu'il s'agit d'un balayage complet.
- Toutes les méthodes dangereuses sont plus rapides, mais la cohérence n'est pas implicite lors de leur utilisation. Il est logique de les utiliser sur une base de données arrêtée pour son remplissage rapide ou autre chose dans la même veine.
- Le suiveur surveille la mise à jour régulière de l'instantané de la base de données, donc la méthode Save est très probablement pour certains cas spéciaux lorsque vous voulez vraiment créer un nouvel instantané (jusqu'à ce qu'un tel cas me vienne à l'esprit, mais c'est peut-être le cas).
Un cas d'utilisation simple:
package main import ( "fmt" "github.com/claygod/coffer" ) const curDir = "./" func main() {
Les transactions
Comme mentionné ci-dessus, ma définition des transactions peut ne pas coïncider avec celle généralement acceptée dans la construction DB, peut-être qu'elles ne sont unies que par une idée. Dans une implémentation spécifique, une transaction est un certain en-tête spécifié à l'étape de configuration de la base de données (méthode du Handler
). Lorsque nous appelons une transaction avec cet en-tête, la base de données bloque les enregistrements avec lesquels l'en-tête fonctionnera et transfère leurs valeurs actuelles à l'en-tête. L'en-tête manipule ces données selon ses besoins et renvoie les nouvelles valeurs de la base de données, ce qui les enregistre dans une centaine. Après cela, les enregistrements sont déverrouillés et deviennent disponibles pour d'autres opérations.
Il y a des exemples dans le référentiel qui révèlent très bien l'essence de l'utilisation des transactions. Par curiosité, j'ai fait un petit exemple financier, dans lequel il y a des opérations de débit et de crédit, de transfert, d'achat et de vente. Il était très facile d'écrire cet exemple, et en même temps, cette mise en œuvre à hauteur de genou est assez cohérente et adaptée à une utilisation dans diverses solutions financières, ou par exemple dans la logistique.
Un point important: le code du gestionnaire n'est pas stocké dans la base de données. J'ai eu une idée de le stocker dans un journal, mais cela m'a semblé trop inutile, donc je ne l'ai pas compliqué, et en conséquence la responsabilité de la cohérence des gestionnaires entre les différents démarrages de base de données incombe au développeur du code qui utilise la base de données. Les gestionnaires ne peuvent définitivement pas être modifiés si l'application et la base de données cessent de planter. Dans ce cas, vous devez d'abord démarrer la base de données, puis l'arrêter régulièrement - un nouvel instantané de données sera créé. Afin de ne pas vous tromper, je vous conseille d'utiliser le numéro de version au nom des gestionnaires.
Recevoir et traiter les réponses
La base de données renvoie des rapports indiquant l'état de la réponse et les données. Puisqu'il existe de nombreux codes et que l'écriture d'un commutateur avec le traitement de chacun d'eux est gênante, vous pouvez vérifier env. Cela ne devrait pas être fait. Le fait est que le code peut avoir le statut Ok, Erreur, Panique. Avec Ok, tout est clair, mais qu'en est-il des deux autres? Si l'état est Erreur, une opération spécifique est terminée ou n'est pas terminée. Cette erreur doit être gérée de manière appropriée dans l'application. Cependant, des travaux supplémentaires avec la base de données sont possibles (et nécessaires). Une autre chose Panic - le travail avec la base de données doit être interrompu.
La vérification de IsCodeError
simplifie le travail avec toutes les erreurs, donc si vous n'êtes pas intéressé par les détails, continuez à travailler.
La vérification IsCodePanic
couvre tous les cas dans lesquels le travail avec la base de données doit être arrêté.
Dans le cas simple, un triple interrupteur suffit pour traiter la réponse:
IsCodeOk
- continuer à fonctionner comme d' IsCodeOk
IsCodeError
- enregistrez l'erreur à partir du rapport et travaillez plus loinIsCodePanic
- enregistre l'erreur du rapport et arrête de travailler avec la base de données
Offtop
Pour le nom, une des options pour traduire la
mots en anglais a été choisie, je préférerais la box
, bien sûr, mais c'est un mot trop populaire, j'espère que le coffer
fera l'affaire.
Le sujet avec l'ACID me semble plutôt holistique, donc je dirais que Coffer est attaché à cela, mais pas un fait, et je ne prétends pas qu'il a réussi.
Performances
J'ai immédiatement écrit une base de données tenant compte de la concurrence et de la concurrence. C'est dans ce mode qu'il montre son efficacité (bien que cela soit probablement dit trop fort). Dans les résultats ci-dessous, le benchmark montre une bande passante de 200k rps. C'est bien sûr un banc artificiel, et la réalité sera complètement différente, car cela dépend beaucoup de la taille des données enregistrées, de la quantité de données déjà enregistrées, des performances du fer et de la phase de la lune. Mais la tendance est au moins compréhensible. Si la base de données est utilisée en mode monothread, chaque requête n'est exécutée qu'après avoir reçu la réponse à la précédente, la vitesse sera lente, et je vous conseille de regarder d'autres bases de données, mais pas Coffer.
- BenchmarkCofferTransactionSequence-4 2000 227928 ns / op
- BenchmarkCofferTransactionPar32HalfConcurent-4 100000 4199 ns / op
Soit dit en passant, si quelqu'un passe du temps et s'incline vers un référentiel avec Coffer, si possible, exécutez le banc allongé dedans. Je suis très intéressé par les machines dont les performances afficheront la base de données. Tout d'abord, bien sûr, tout dépend du disque. Cela est devenu particulièrement clair pour moi après avoir récemment acheté un nouveau Samsung EVO. Mais ne vous inquiétez pas, ce n'est pas un substitut pour un disque mort. L'ancien Toshiba continue de fonctionner correctement et stocke maintenant mes archives vidéo.
La montre intégrée en mémoire est toujours une carte simple, pas même divisée en sections. Bien sûr, il peut être utile de l'améliorer, par exemple, pour accélérer la sélection de clés par préfixes et suffixes. Bien que je ne l'ai pas fait, tk. la fonctionnalité principale, comme je le dis la puce DB, je vois dans les transactions, et le goulot d'étranglement dans les performances pour les transactions fonctionnera avec le disque, et alors seulement, avec la mémoire.
Licence
Maintenant, la licence vous permet de stocker jusqu'à dix millions d'enregistrements dans la base de données, il me semble que c'est un nombre suffisant. D'autres projets de développement de la base de données sont en cours de constitution.
En général, il est intéressant pour moi d'utiliser la base de données en tant que package et de me concentrer principalement sur son API.
Conclusions
Récemment, j'ai souvent rencontré la tâche d'écrire des services avec la caractéristique de la haute disponibilité. Malheureusement, étant donné que cela implique presque toujours la présence de plusieurs instances, cela ne vaut pas la peine d'utiliser la base de données intégrée avec un tel cas. Il reste l'option d'une application ou d'un service régulier qui existe dans une seule instance. Cela me semble un cas plus rare, mais néanmoins il l'est, et dans un tel cas, c'est bien d'avoir une base de données qui essaie, autant que possible, de sauvegarder les données qui y sont stockées. Le coffre que j'ai créé essaie de résoudre un tel problème. Voyons comment il le fait.
Remerciements
- À tous ceux qui ont lu l'article jusqu'au bout
- Des commentateurs souhaitant partager leurs opinions
- Envoyé dans une information personnelle sur les fautes de frappe et les erreurs dans le texte
- Voisin tournant de la musique la nuit
Les références
Référentiel DB
Description en russe