O objetivo do meu post é falar sobre a API C ++ do banco de dados distribuído Apache Ignite chamado Ignite C ++, bem como seus recursos.
Sobre o Apache Ignite em um habr já escreveu mais de uma vez, então certamente alguns de vocês já sabem o que é e por que é necessário.
Brevemente sobre o Apache Ignite para aqueles que ainda não o conhecem
Não entrarei em detalhes sobre como o Apache Ignite surgiu e como ele difere dos bancos de dados clássicos. Todas essas questões já foram levantadas aqui , aqui ou aqui .
Portanto, o Apache Ignite é essencialmente um banco de dados distribuído rapidamente, otimizado para trabalhar com RAM. O Ignite cresceu a partir de uma Grade de dados na memória e, até recentemente, foi posicionado como um cache distribuído muito rápido e completamente na memória, com base em uma tabela de hash distribuída. É por isso que, além de armazenar dados, possui muitos recursos convenientes para seu processamento distribuído rápido: Map-Reduce, operações de dados atômicos, transações ACID completas, consultas de dados SQL, as chamadas Consultas Continuadas, que possibilitam monitorar alterações em determinados dados e outros.
Recentemente, no entanto, a plataforma adicionou suporte para armazenamento persistente de dados em disco . Depois disso, o Apache Ignite obteve todos os benefícios de um banco de dados orientado a objetos, mantendo a conveniência, a riqueza de ferramentas, a flexibilidade e a velocidade da data da grade.
Pouco de teoria
Uma parte importante para entender como trabalhar com o Apache Ignite é que ele é escrito em Java. Você perguntará: "Que diferença faz com o que o banco de dados está gravado se eu me comunicar com ele de qualquer maneira através do SQL?" Há alguma verdade nisso. Se você deseja usar o Ignite apenas como um banco de dados, pode pegar o driver ODBC ou JDBC que acompanha o Ignite, aumentar o número de nós do servidor necessários usando o script ignite.sh
especialmente criado, configurá-los usando configurações flexíveis e, especialmente, sobe sobre a linguagem, trabalhando com o Ignite, mesmo do PHP, até do Go.
A interface nativa do Ignite fornece muito mais recursos do que apenas o SQL. Das mais simples: operações atômicas rápidas com objetos no banco de dados, objetos de sincronização distribuídos e computação distribuída em um cluster com dados locais, quando você não precisa extrair centenas de megabytes de dados para o cliente para cálculos. Como você entende, essa parte da API não funciona através do SQL, mas é escrita em linguagens de programação de uso geral muito específicas.
Naturalmente, como o Ignite é escrito em Java, a API mais abrangente é implementada nessa linguagem de programação. No entanto, além do Java, também existem versões de API para C # .NET e C ++. Esses são os chamados clientes "grossos" - de fato, o nó Ignite na JVM, iniciado a partir de C ++ ou C #, cuja comunicação ocorre através da JNI. Esse tipo de nó é necessário, entre outras coisas, para que o cluster possa executar a computação distribuída nos idiomas correspondentes - C ++ e C #.
Além disso, existe um protocolo aberto para os chamados clientes "thin". Essas já são bibliotecas leves em várias linguagens de programação que se comunicam com o cluster via TCP / IP. Eles ocupam muito menos espaço na memória, são iniciados quase instantaneamente, não requerem uma JVM na máquina, mas possuem uma latência um pouco pior e APIs não tão ricas em comparação com clientes "grossos". Hoje existem thin clients em Java, C # e Node.js, clientes em C ++, PHP, Python3, Go são ativamente desenvolvidos.
Em um post, analisarei a API espessa Ignite para API C ++, pois é ela que atualmente fornece a API mais abrangente.
Introdução
Não vou me debruçar em detalhes sobre a instalação e a configuração do próprio framework - o processo é rotineiro, não é muito interessante e está bem descrito, por exemplo, na documentação oficial . Vamos direto ao código.
Como o Apache Ignite é uma plataforma distribuída, para começar, a primeira coisa que você precisa fazer é executar pelo menos um nó. Isso é feito de maneira muito simples, usando a classe 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; }
Parabéns, você lançou seu primeiro nó do Cache Apache Ignite com as configurações padrão. A classe Ignite, por sua vez, é o principal ponto de entrada para acessar toda a API do cluster.
Trabalhar com dados
O principal componente do Ignite C ++ que fornece uma API para trabalhar com dados é o cache, ignite::cache::Cache<K,V>
. O cache fornece um conjunto básico de métodos para trabalhar com dados. Como o Cache
é essencialmente uma interface para uma tabela de hash distribuída, os métodos básicos para trabalhar com ele se assemelham ao trabalho com contêineres comuns, como map
ou 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; }
Parece bem simples, certo? De fato, as coisas ficam um pouco complicadas se olharmos mais de perto as limitações do C ++.
Desafios de integração C ++
Como mencionei, o Apache Ignite é completamente escrito em Java - uma poderosa linguagem orientada a OOP. É lógico que muitos dos recursos dessa linguagem, associados, por exemplo, ao reflexo do tempo de execução do programa, foram usados ativamente para implementar os componentes do Apache Ignite. Por exemplo, para serialização / desserialização de objetos para armazenamento em disco e transmissão em rede.
No C ++, diferentemente do Java, não existe uma reflexão tão poderosa. Em geral, não, ainda não, infelizmente. Em particular, não há maneiras de descobrir a lista e o tipo de campos de um objeto, o que poderia permitir gerar automaticamente o código necessário para serializar / desserializar objetos de tipos de usuário. Portanto, a única opção aqui é solicitar ao usuário que forneça explicitamente o conjunto necessário de metadados sobre o tipo de usuário e como trabalhar com ele.
No Ignite C ++, isso é implementado através da especialização do modelo ignite::binary::BinaryType<T>
. Essa abordagem é usada tanto em clientes "grossos" quanto em "thin". Para a classe Person apresentada acima, uma especialização semelhante pode ser assim:
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"); } }; }
Como você pode ver, além dos BinaryType<Person>::Write
serialização / desserialização BinaryType<Person>::Write
, BinaryType<Person>::Read
, existem vários outros métodos. Eles são necessários para explicar à plataforma como trabalhar com tipos C ++ personalizados em outras linguagens, em particular Java. Vamos dar uma olhada em cada método:
GetTypeName()
- Retorna o nome do tipo. O nome do tipo deve ser o mesmo em todas as plataformas nas quais o tipo é usado. Se você usar o tipo apenas no Ignite C ++, o nome poderá ser qualquer coisa.GetTypeId()
- esse método retorna um identificador exclusivo de plataforma cruzada para o tipo. Para o trabalho correto com um tipo em plataformas diferentes, é necessário que seja calculado da mesma maneira em todos os lugares. O GetBinaryStringHashCode(TypeName)
retorna o mesmo tipo de identificação que em todas as outras plataformas por padrão, ou seja, essa implementação desse método permite que você trabalhe corretamente com esse tipo de outras plataformas.GetFieldId()
- Retorna um identificador exclusivo para o nome do tipo. Novamente, para o trabalho correto entre plataformas, vale a pena usar o método GetBinaryStringHashCode()
;IsNull()
- Verifica se uma instância de uma classe é um objeto do tipo NULL
. Usado para serializar corretamente valores NULL
. Não é muito útil com instâncias da própria classe, mas pode ser extremamente conveniente se o usuário quiser trabalhar com ponteiros inteligentes e definir especialização, por exemplo, para BinaryType< std::unique_ptr<Person> >
.GetNull()
- Chamado ao tentar desserializar um valor NULL
. Tudo o que foi dito sobre o IsNull
também é válido para GetNull()
.
SQL
Se traçarmos uma analogia com os bancos de dados clássicos, o cache será um esquema de banco de dados com um nome de classe contendo uma tabela - com um nome de tipo. Além dos esquemas de cache, existe um esquema geral chamado PUBLIC
no qual você pode criar / excluir um número ilimitado de tabelas usando comandos DDL padrão, como CREATE TABLE
, DROP TABLE
e assim por diante. É no esquema PUBLIC que eles geralmente se conectam via ODBC / JDBC se desejam usar o Ignite simplesmente como um banco de dados distribuído.
O Ignite suporta consultas SQL completas, incluindo DML e DDL. Ainda não há suporte para transações SQL, mas a comunidade está trabalhando ativamente na implementação do MVCC, que adicionará transações e, tanto quanto eu sei, as principais alterações foram introduzidas recentemente no mestre.
Para trabalhar com dados de cache via SQL, você deve especificar explicitamente na configuração de cache quais campos do objeto serão usados nas consultas SQL. A configuração é gravada no arquivo XML, após o qual o caminho para o arquivo de configuração é especificado quando o nó é iniciado:
<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>
A configuração é analisada pelo mecanismo Java, portanto, os tipos de base também devem ser especificados para Java. Após a criação do arquivo de configuração, é necessário iniciar o nó, obter a instância do cache e começar a usar o SQL:
//... 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; }
Da mesma maneira, você pode usar insert
, update
, create table
e outras consultas. Obviamente, solicitações de cache cruzado também são suportadas. No entanto, nesse caso, o nome do cache deve ser indicado na solicitação entre aspas como o nome do esquema. Por exemplo, em vez de
select * from Person inner join Profession
deveria escrever
select * from "PersonCache".Person inner join "ProfessionCache".Profession
E assim por diante
Existem muitas possibilidades no Apache Ignite e, é claro, em um post era impossível cobrir todas elas. A API C ++ está se desenvolvendo ativamente agora, logo, haverá mais interessante. É possível que eu escreva mais algumas postagens, onde analisarei alguns recursos com mais detalhes.
PS Eu sou um committer do Apache Ignite desde 2017 e estou desenvolvendo ativamente a API C ++ para este produto. Se você conhece bastante C ++, Java ou .NET e gostaria de participar do desenvolvimento de um produto aberto com uma comunidade ativa e amigável, sempre encontraremos algumas outras tarefas interessantes para você.