Der Zweck meines Beitrags ist es, über die C ++ - API der verteilten Apache Ignite-Datenbank namens Ignite C ++ sowie deren Funktionen zu sprechen.
Über den Apache Ignite auf einem Habr wurde bereits mehr als einmal geschrieben, so dass sicherlich einige von Ihnen bereits grob wissen, was es ist und warum es notwendig ist.
Kurz über Apache Ignite für diejenigen, die noch nicht damit vertraut sind
Ich werde nicht näher darauf eingehen, wie Apache Ignite entstanden ist und wie es sich von klassischen Datenbanken unterscheidet. All diese Fragen wurden hier , hier oder hier bereits aufgeworfen.
Apache Ignite ist also im Wesentlichen eine schnell verteilte Datenbank, die für die Arbeit mit RAM optimiert ist. Ignite selbst ist aus einem speicherinternen Datenraster hervorgegangen und wurde bis vor kurzem als sehr schneller, vollständig speicherinterner verteilter Cache basierend auf einer verteilten Hash-Tabelle positioniert. Aus diesem Grund bietet es neben dem Speichern von Daten viele praktische Funktionen für die schnelle verteilte Verarbeitung: Map-Reduce, atomare Datenoperationen, vollwertige ACID-Transaktionen, SQL-Datenabfragen, sogenannte Continues Queries, mit denen Änderungen an bestimmten Daten und Daten überwacht werden können andere.
In letzter Zeit hat die Plattform jedoch Unterstützung für die dauerhafte Speicherung von Daten auf der Festplatte hinzugefügt. Danach erhielt Apache Ignite alle Vorteile einer vollwertigen objektorientierten Datenbank, während der Komfort, der Werkzeugreichtum, die Flexibilität und die Geschwindigkeit des Rasterdatums erhalten blieben.
Ein bisschen Theorie
Ein wichtiger Teil zum Verständnis der Arbeit mit Apache Ignite ist, dass es in Java geschrieben ist. Sie werden fragen: "Welchen Unterschied macht es zu dem, was die Datenbank geschrieben hat, wenn ich trotzdem über SQL mit ihr kommuniziere?" Daran ist etwas Wahres. Wenn Sie Ignite nur als Datenbank verwenden möchten, können Sie den mit Ignite ignite.sh
oder JDBC-Treiber verwenden, die Anzahl der benötigten ignite.sh
mithilfe des speziell erstellten Skripts ignite.sh
erhöhen und diese mithilfe flexibler Konfigurationen und insbesondere konfigurieren schwebt über die Sprache und arbeitet mit Ignite sogar von PHP, sogar von Go.
Die native Ignite-Oberfläche bietet viel mehr Funktionen als nur SQL. Am einfachsten: schnelle atomare Operationen mit Objekten in der Datenbank, verteilten Synchronisationsobjekten und verteiltem Computing in einem Cluster für lokale Daten, wenn Sie nicht Hunderte von Megabyte Daten für Berechnungen auf den Client ziehen müssen. Wie Sie verstehen, funktioniert dieser Teil der API nicht über SQL, sondern ist in sehr spezifischen allgemeinen Programmiersprachen geschrieben.
Da Ignite in Java geschrieben ist, ist natürlich die umfassendste API in dieser Programmiersprache implementiert. Neben Java gibt es jedoch auch API-Versionen für C # .NET und C ++. Dies sind die sogenannten "Thick" -Clients - tatsächlich der Ignite-Knoten in der JVM, der über C ++ oder C # gestartet wird und mit dem über die JNI kommuniziert wird. Diese Art von Knoten ist unter anderem erforderlich, damit der Cluster verteiltes Computing in den entsprechenden Sprachen - C ++ und C # - ausführen kann.
Darüber hinaus gibt es ein offenes Protokoll für die sogenannten „Thin“ -Clients. Dies sind bereits kompakte Bibliotheken in verschiedenen Programmiersprachen, die über TCP / IP mit dem Cluster kommunizieren. Sie belegen viel weniger Speicherplatz, starten fast sofort, benötigen keine JVM auf dem Computer, haben jedoch eine etwas schlechtere Latenz und weniger umfangreiche APIs als "dicke" Clients. Heute gibt es Thin Clients in Java, C # und Node.js, Clients in C ++, PHP, Python3, Go werden aktiv entwickelt.
In einem Beitrag werde ich mich mit der Ignite Thick API für C ++ API befassen, da diese derzeit die umfassendste API bietet.
Erste Schritte
Ich werde nicht im Detail auf die Installation und Konfiguration des Frameworks selbst eingehen - der Prozess ist routinemäßig, nicht sehr interessant und wird beispielsweise in der offiziellen Dokumentation gut beschrieben. Gehen wir direkt zum Code.
Da Apache Ignite eine verteilte Plattform ist, müssen Sie zunächst mindestens einen Knoten ausführen, um loszulegen. Dies geschieht ganz einfach mit der Klasse ignite::Ignition
:
#include <iostream> #include <ignite/ignition.h> using namespace ignite; int main() { IgniteConfiguration cfg; Ignite node = Ignition::Start(cfg); std::cout << "Node started. Press 'Enter' to stop" << std::endl; std::cin.get(); Ignition::StopAll(false); std::cout << "Node stopped" << std::endl; return 0; }
Herzlichen Glückwunsch, Sie haben Ihren ersten Cache Apache Ignite-Knoten mit Standardeinstellungen gestartet. Die Ignite-Klasse ist wiederum der Haupteinstiegspunkt für den Zugriff auf die gesamte Cluster-API.
Mit Daten arbeiten
Die Hauptkomponente von Ignite C ++, die eine API für die Arbeit mit Daten bereitstellt, ist der Cache ignite::cache::Cache<K,V>
. Der Cache bietet eine Reihe grundlegender Methoden zum Arbeiten mit Daten. Da Cache
im Wesentlichen eine Schnittstelle zu einer verteilten Hash-Tabelle ist, ähneln die grundlegenden Methoden für die Arbeit damit der Arbeit mit normalen Containern wie map
oder unordered_map
.
#include <string> #include <cassert> #include <cstdint> #include <ignite/ignition.h> using namespace ignite; struct Person { int32_t age; std::string firstName; std::string lastName; } //... int main() { IgniteConfiguration cfg; Ignite node = Ignition::Start(cfg); cache::Cache<int32_t, Person> personCache = node.CreateCache<int32_t, Person>("PersonCache"); Person p1 = { 35, "John", "Smith" }; personCache.Put(42, p1); Person p2 = personCache.Get(42); std::cout << p2 << std::endl; assert(p1 == p2); return 0; }
Sieht ziemlich einfach aus, oder? Tatsächlich werden die Dinge etwas komplizierter, wenn wir uns die Einschränkungen von C ++ genauer ansehen.
Herausforderungen bei der C ++ - Integration
Wie bereits erwähnt, ist Apache Ignite vollständig in Java geschrieben - einer leistungsstarken OOP-gesteuerten Sprache. Es ist logisch, dass viele der Funktionen dieser Sprache, die beispielsweise mit der Reflektion der Programmausführungszeit verbunden sind, aktiv zur Implementierung von Apache Ignite-Komponenten verwendet wurden. Zum Beispiel zur Serialisierung / Deserialisierung von Objekten zur Speicherung auf einer Festplatte und zur Übertragung über ein Netzwerk.
In C ++ gibt es im Gegensatz zu Java keine so leistungsstarke Reflexion. Im Allgemeinen nein, leider noch nicht. Insbesondere gibt es keine Möglichkeit, die Liste und den Typ der Felder eines Objekts herauszufinden, wodurch automatisch der Code generiert werden kann, der zum Serialisieren / Deserialisieren von Objekten vom Benutzertyp erforderlich ist. Daher besteht die einzige Möglichkeit hier darin, den Benutzer aufzufordern, die erforderlichen Metadaten zum Benutzertyp und zur Arbeitsweise explizit anzugeben.
In Ignite C ++ wird dies durch die Spezialisierung der Vorlage ignite::binary::BinaryType<T>
. Dieser Ansatz wird sowohl bei "dicken" als auch bei "dünnen" Clients verwendet. Für die oben vorgestellte Personenklasse könnte eine ähnliche Spezialisierung folgendermaßen aussehen:
namespace ignite { namespace binary { template<> struct BinaryType<Person> { static int32_t GetTypeId() { return GetBinaryStringHashCode("Person"); } static void GetTypeName(std::string& name) { name = "Person"; } static int32_t GetFieldId(const char* name) { return GetBinaryStringHashCode(name); } static bool IsNull(const Person& obj) { return false; } static void GetNull(Person& dst) { dst = Person(); } static void Write(BinaryWriter& writer, const Person& obj) { writer.WriteInt32("age", obj.age; writer.WriteString("firstName", obj.firstName); writer.WriteString("lastName", obj.lastName); } static void Read(BinaryReader& reader, Person& dst) { dst.age = reader.ReadInt32("age"); dst.firstName = reader.ReadString("firstName"); dst.lastName = reader.ReadString("lastName"); } }; }
Wie Sie sehen können, gibt es neben den Serialisierungs- / Deserialisierungsmethoden BinaryType<Person>::Write
, BinaryType<Person>::Read
mehrere andere Methoden. Sie werden benötigt, um der Plattform zu erklären, wie mit benutzerdefinierten C ++ - Typen in anderen Sprachen, insbesondere Java, gearbeitet wird. Schauen wir uns jede Methode genauer an:
GetTypeName()
- Gibt den GetTypeName()
zurück. Der Typname muss auf allen Plattformen, auf denen der Typ verwendet wird, gleich sein. Wenn Sie den Typ nur in Ignite C ++ verwenden, kann der Name beliebig sein.GetTypeId()
- Diese Methode gibt eine plattformübergreifende eindeutige Kennung für den Typ zurück. Für die korrekte Arbeit mit einem Typ auf verschiedenen Plattformen ist es erforderlich, dass er überall gleich berechnet wird. Die GetBinaryStringHashCode(TypeName)
Methode GetBinaryStringHashCode(TypeName)
gibt standardmäßig dieselbe Typ-ID wie auf allen anderen Plattformen zurück. Mit dieser Implementierung dieser Methode können Sie von anderen Plattformen aus korrekt mit diesem Typ arbeiten.GetFieldId()
- Gibt eine eindeutige Kennung für den GetFieldId()
zurück. Auch hier lohnt es sich, für die korrekte plattformübergreifende Arbeit die GetBinaryStringHashCode()
-Methode zu verwenden.IsNull()
- Überprüft, ob eine Instanz einer Klasse ein Objekt vom Typ NULL
. Wird verwendet, um NULL
Werte korrekt zu serialisieren. Nicht sehr nützlich für Instanzen der Klasse selbst, aber es kann äußerst praktisch sein, wenn der Benutzer mit intelligenten Zeigern arbeiten und eine Spezialisierung definieren möchte, z. B. für BinaryType< std::unique_ptr<Person> >
.GetNull()
- GetNull()
aufgerufen, wenn versucht wird, einen NULL
Wert zu deserialisieren. Alles, was über IsNull
gesagt IsNull
gilt auch für GetNull()
.
SQL
Wenn wir eine Analogie zu klassischen Datenbanken ziehen, ist der Cache ein Datenbankschema mit einem Klassennamen, der eine Tabelle enthält - mit einem Typnamen. Zusätzlich zu den Cache-Schemata gibt es ein allgemeines Schema namens PUBLIC
in dem Sie eine unbegrenzte Anzahl von Tabellen mit Standard-DDL-Befehlen wie CREATE TABLE
, DROP TABLE
usw. erstellen / löschen können. Mit dem PUBLIC-Schema verbinden sie sich normalerweise über ODBC / JDBC, wenn sie Ignite einfach als verteilte Datenbank verwenden möchten.
Ignite unterstützt vollständige SQL-Abfragen, einschließlich DML und DDL. Es gibt noch keine Unterstützung für SQL-Transaktionen, aber die Community arbeitet derzeit aktiv an der Implementierung von MVCC, mit der Transaktionen hinzugefügt werden. Soweit ich weiß, wurden die wichtigsten Änderungen kürzlich in master eingeführt.
Um mit Cache-Daten über SQL zu arbeiten, müssen Sie in der Cache-Konfiguration explizit angeben, welche Felder des Objekts in SQL-Abfragen verwendet werden. Die Konfiguration wird in die XML-Datei geschrieben. Anschließend wird beim Starten des Knotens der Pfad zur Konfigurationsdatei angegeben:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <bean id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration"> <property name="cacheConfiguration"> <list> <bean class="org.apache.ignite.configuration.CacheConfiguration"> <property name="name" value="PersonCache"/> <property name="queryEntities"> <list> <bean class="org.apache.ignite.cache.QueryEntity"> <property name="keyType" value="java.lang.Integer"/> <property name="valueType" value="Person"/> <property name="fields"> <map> <entry key="age" value="java.lang.Integer"/> <entry key="firstName" value="java.lang.String"/> <entry key="lastName" value="java.lang.String"/> </map> </property> </bean> </list> </property> </bean> </list> </property> </bean> </beans>
Die Konfiguration wird von der Java-Engine analysiert, daher müssen die Basistypen auch für Java angegeben werden. Nachdem die Konfigurationsdatei erstellt wurde, müssen Sie den Knoten starten, die Cache-Instanz abrufen und SQL verwenden:
//... int main() { IgniteConfiguration cfg; cfg.springCfgPath = "config.xml"; Ignite node = Ignition::Start(cfg); cache::Cache<int32_t, Person> personCache = node.GetCache<int32_t, Person>("PersonCache"); personCache.Put(1, Person(35, "John", "Smith")); personCache.Put(2, Person(31, "Jane", "Doe")); personCache.Put(3, Person(12, "Harry", "Potter")); personCache.Put(4, Person(12, "Ronald", "Weasley")); cache::query::SqlFieldsQuery qry( "select firstName, lastName from Person where age = ?"); qry.AddArgument<int32_t>(12); cache::query::QueryFieldsCursor cursor = cache.Query(qry); while (cursor.HasNext()) { QueryFieldsRow row = cursor.GetNext(); std::cout << row.GetNext<std::string>() << ", "; std::cout << row.GetNext<std::string>() << std::endl; } return 0; }
Auf die gleiche Weise können Sie insert
, update
, create table
und andere Abfragen verwenden. Natürlich werden auch cacheübergreifende Anforderungen unterstützt. In diesem Fall sollte der Cache-Name in der Anforderung jedoch in Anführungszeichen als Name des Schemas angegeben werden. Zum Beispiel anstelle von
select * from Person inner join Profession
sollte schreiben
select * from "PersonCache".Person inner join "ProfessionCache".Profession
Usw
Es gibt wirklich viele Möglichkeiten in Apache Ignite und natürlich war es in einem Beitrag unmöglich, alle zu behandeln. Die C ++ - API wird derzeit aktiv entwickelt, daher wird es bald interessanter sein. Es ist möglich, dass ich noch ein paar Beiträge schreibe, in denen ich einige Funktionen genauer analysieren werde.
PS Ich bin seit 2017 ein Apache Ignite-Committer und entwickle aktiv die C ++ - API für dieses Produkt. Wenn Sie mit C ++, Java oder .NET ziemlich vertraut sind und an der Entwicklung eines offenen Produkts mit einer aktiven, freundlichen Community teilnehmen möchten, werden wir immer ein paar andere interessante Aufgaben für Sie finden.