尽力而为的分布式C ++应用程序

我的文章的目的是讨论称为Ignite C ++的Apache Ignite分布式数据库的C ++ API及其功能。


关于Habr上的Apache Ignite已经写了不止一次了,因此,您中的某些人肯定已经大致知道它是什么以及为什么有必要。


向尚未熟悉Apache Ignite的人简要介绍


我不会详细介绍Apache Ignite是如何产生的以及它与经典数据库的不同之处。 所有这些问题已经在这里这里这里提出


因此,Apache Ignite本质上是为使用RAM而优化的快速分布式数据库。 Ignite本身是从内存中的数据网格发展而来的,直到最近,它仍被定位为基于分布式哈希表的非常快的,完全内存中的分布式缓存。 这就是为什么除了存储数据外,它还具有许多方便的功能来进行快速的分布式处理:Map-Reduce,原子数据操作,成熟的ACID事务,SQL数据查询,所谓的Continues Queries(继续查询),这使得可以监视某些数据和其他。


但是,最近该平台增加了对磁盘上数据持久存储的支持 。 之后,Apache Ignite获得了功能强大的面向对象数据库的所有优势,同时保留了便利性,丰富的工具,灵活的网格日期和速度。


一点理论


了解使用Apache Ignite的一个重要部分是它是用Java编写的。 您会问:“如果我还是通过SQL与数据库通信,它将对写入的数据库有什么影响?” 这有些道理。 如果只想将Ignite用作数据库,则可以使用Ignite随附的ODBC或JDBC驱动程序,使用专门创建的ignite.sh脚本来增加所需的服务器节点数,使用灵活的配置来配置它们,特别是甚至从PHP甚至从Go使用Ignite都对语言一路狂飙。


本地Ignite界面提供的功能不只是SQL。 最简单的方法是:当您不需要将数百兆字节的数据拉到客户端进行计算时,可以对数据库中的对象,集群中的分布式同步对象和分布式计算进行快速原子操作,而无需将数百兆字节的数据拉到客户端。 如您所知,API的这一部分无法通过SQL进行工作,而是以非常特殊的通用编程语言编写的。


自然,由于Ignite是用Java编写的,因此最全面的API是以这种编程语言实现的。 但是,除了Java,还有C#.NET和C ++的API版本。 这些是所谓的“厚”客户端-实际上,JVM中的Ignite节点是从C ++或C#启动的,通过JNI与之通信。 为了使群集能够以相应的语言(C ++和C#)运行分布式计算,这种节点是必不可少的。


此外,还有针对所谓的“瘦”客户端的开放协议。 这些已经是各种编程语言的轻量级库,可通过TCP / IP与集群通信。 它们占用的内存空间少得多,几乎可以立即启动,不需要计算机上的JVM,但是与“胖”客户端相比,它们的延迟时间稍差,并且API也不那么丰富。 如今,Java,C#和Node.js中有瘦客户端,C ++,PHP,Python3,Go中的客户端也在积极开发中。


在一篇博文中,我将介绍用于C ++ API的Ignite厚API,因为它目前提供了最全面的API。


开始使用


我不会详细介绍框架本身的安装和配置-该过程是例行程序,不是很有趣,并且在官方文档中对此进行了很好的描述。 让我们直接看一下代码。


由于Apache Ignite是分布式平台,因此要开始使用,首先要做的就是至少运行一个节点。 这可以使用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; } 

恭喜,您使用默认设置启动了第一个Cache Apache Ignite节点。 反过来,Ignite类是访问整个群集API的主要入口点。


处理数据


提供用于处理数据的API的Ignite C ++的主要组件是缓存ignite::cache::Cache<K,V> 。 缓存提供了一组处理数据的基本方法。 由于Cache本质上是分布式哈希表的接口,因此使用它的基本方法类似于使用普通容器(例如mapunordered_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; } 

看起来很简单,对吧? 实际上,如果我们更仔细地研究C ++的局限性,事情就会变得有些复杂。


C ++集成挑战


如前所述,Apache Ignite完全用Java(一种强大的OOP驱动的语言)编写。 逻辑上,该语言的许多功能(例如与反映程序执行时间有关)被积极地用于实现Apache Ignite组件。 例如,用于对象的序列化/反序列化,以存储在磁盘上并通过网络传输。


在C ++中,与Java不同,没有这种强大的反映。 总的来说,不幸的是,还没有。 特别是,没有办法找出对象的字段列表和类型,这可能允许自动生成对用户类型的对象进行序列化/反序列化所必需的代码。 因此,这里唯一的选择是要求用户明确提供有关用户类型以及如何使用它的必要元数据集。


在Ignite C ++中,这是通过ignite::binary::BinaryType<T>模板的特殊化实现的。 此方法在“瘦”和“瘦”客户端中都使用。 对于上面介绍的Person类,类似的专业化可能看起来像这样:


 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"); } }; } // namespace binary } // namespace ignite 

如您所见,除了序列化/反序列化BinaryType<Person>::WriteBinaryType<Person>::Read ,还有其他几种方法。 为了向平台解释如何使用其他语言(尤其是Java)中的自定义C ++类型,需要使用它们。 让我们仔细看看每种方法:


  • GetTypeName() -返回类型名称。 在使用该类型的所有平台上,类型名称必须相同。 如果仅在Ignite C ++中使用类型,则名称可以是任何名称。
  • GetTypeId() -此方法返回该类型的跨平台唯一标识符。 为了在不同平台上正确处理类型,必须在各处计算相同的类型。 默认情况下, GetBinaryStringHashCode(TypeName)方法返回与所有其他平台相同的Type ID,也就是说,此方法的此实现允许您从其他平台正确使用此类型。
  • GetFieldId() -返回类型名称的唯一标识符。 同样,对于正确的跨平台工作,值得使用GetBinaryStringHashCode()方法。
  • IsNull() -检查类的实例是否为NULL类型的对象。 用于正确序列化NULL值。 对于类本身的实例不是很有用,但是如果用户想使用智能指针并定义特殊化,例如对于BinaryType< std::unique_ptr<Person> > ,则可以非常方便。
  • GetNull() -尝试反序列化NULL值时调用。 关于IsNull所有信息对于GetNull()也是正确的。

的SQL


如果我们用经典数据库做一个类比,则缓存是一个数据库模式,其类名称包含一个表-类型名称。 除了高速缓存方案外,还有一种通用方案称为PUBLIC ,您可以在其中使用标准DDL命令(例如CREATE TABLEDROP TABLE等)来创建/删除无限数量的表。 如果他们只想将Ignite用作分布式数据库,则通常通过ODBC / JDBC连接到PUBLIC方案。


Ignite支持完整的SQL查询,包括DML和DDL。 目前尚不支持SQL事务,但社区现在正在积极地实施MVCC的实施,该事务将添加事务,据我所知,主要更改是最近在master中引入的。


要通过SQL处理缓存数据,必须在缓存配置中明确指定将在SQL查询中使用对象的哪些字段。 将配置写入XML文件中,然后在节点启动时指定配置文件的路径:


 <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> 

该配置由Java引擎解析,因此还必须为Java指定基本类型。 创建配置文件后,您需要启动节点,获取缓存实例,然后可以开始使用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; } 

同样,您可以使用insertupdatecreate table和其他查询。 当然,也支持跨缓存请求。 但是,在这种情况下,缓存名称应在请求中以引号指示为方案名称。 例如,代替


 select * from Person inner join Profession 

应该写


 select * from "PersonCache".Person inner join "ProfessionCache".Profession 

依此类推


Apache Ignite确实有很多可能性,当然,在一篇文章中,不可能涵盖所有这些可能性。 C ++ API现在正在积极开发中,因此很快会有更多有趣的事情。 我可能还会再写几篇文章,对一些功能进行更详细的分析。


PS我自2017年以来一直是Apache Ignite提交者,并且正在积极开发该产品的C ++ API。 如果您对C ++,Java或.NET相当熟悉,并希望通过活跃,友好的社区参与开放产品的开发,我们将始终为您找到其他一些有趣的任务。

Source: https://habr.com/ru/post/zh-CN420623/


All Articles