[关闭]
@liyuj 2017-03-26T05:34:48.000000Z 字数 33463 阅读 6057

Apache-Ignite-1.9.0-中文开发手册

4.SQL网格

4.1.SQL网格

内存SQL网格为Apache Ignite提供了分布式内存数据库的功能,它水平可扩展,容错并且兼容SQL的ANSI-99标准。
SQL网格支持完整的DML命令,包括SELECT, UPDATE, INSERT, MERGE以及DELETE。

内存SQL网格使得开发者与Ignite的交互不仅仅可以使用原生的,面向Java、C++和.NET的开发API,还可以通过JDBC或者ODBC API使用标准的SQL命令,这提供了真正的语言层面的跨平台连接性,比如PHP,Ruby以及其他的。

4.2.分布式查询

4.2.1.摘要

Ignite支持任意的SQL查询,没有任何限制。SQL语法是ANSI-99兼容的,也就意味着作为SQL查询的一部分,规范定义的任何SQL函数、聚合、分组以及关联,都是可以使用的。
此外,查询是完全分布式的。SQL引擎的功能不仅仅是将查询映射到特定的节点然后将结果汇总为最终的结果集,它还可以将存储在不同缓存甚至是不同节点上的数据进行关联。此外,引擎是以容错的方式保证,不会因为新节点加入集群或者旧节点离开而获得不完整或者错误的结果。

4.2.2.SQL查询如何工作

Ignite的SQL网格组件是与H2数据库紧紧绑定在一起的,简而言之,H2是一个Java写的,遵循一组开源许可证,基于内存和磁盘的数据库。
ignite-indexing模块加入节点的类路径之后,一个嵌入式的H2数据库实例就会作为Ignite节点进程的一部分被启动。Ignite借用了H2的SQL查询解析器以及优化器还有执行计划器。最后,届时H2会在一个特定的节点执行本地化的查询(一个分布式查询会被映射到节点或者查询是以LOCAL模式执行的),然后会将本地的结果集传递给分布式SQL引擎用于后续处理。
然而,数据和索引,通常是存储于Ignite数据网格端的,而Ignite以分布式以及容错的方式执行SQL查询,这个是H2不支持的。
Ignite SQL网格执行查询有两种方式:
首先,如果查询在一个部署有REPLICATED模式缓存的节点上执行,那么Ignite会假定所有的数据都是本地化的,然后将其直接传递给H2数据库引擎执行一个简单的本地化SQL查询,对于LOCAL模式的缓存,也是同样的执行流程。
第二,如果查询执行于PARTITIONED模式缓存,那么执行流程如下:

跨缓存查询的执行流程
跨缓存以及关联查询的执行流程与上面描述的分区缓存查询执行流程没什么不同,后面文档还会提到。

4.2.3.查询类型

在Java API层,通常有两种类型的SQL查询,分别为SqlQuerySqlFieldsQuery

替代APIs
Ignite内存SQL网格并不绑定到Java API,可以从.NET, C++通过 ODBC或者JDBC驱动连接到Ignite集群然后执行SQL查询。

SqlQuery
SqlQuery适用于查询执行完毕后需要获得存储于缓存(键和值)中的整个对象的场景,然后返回最终的结果集,下面的代码片段显示了在实践中如何实现:

  1. IgniteCache<Long, Person> cache = ignite.cache("personCache");
  2. SqlQuery sql = new SqlQuery(Person.class, "salary > ?");
  3. // Find all persons earning more than 1,000.
  4. try (QueryCursor<Entry<Long, Person>> cursor = cache.query(sql.setArgs(1000))) {
  5. for (Entry<Long, Person> e : cursor)
  6. System.out.println(e.getValue().toString());
  7. }

SqlFieldsQuery
不需要查询整个对象,只需要指定几个特定的字段即可,这样可以最小化网络和序列化的开销。为此,Ignite实现了一个字段查询的概念。基本上,SqlFieldsQuery接受一个常规的ANSI-99 SQL查询作为它的构造器参数,然后像下面的示例那样立即执行:

  1. IgniteCache<Long, Person> cache = ignite.cache("personCache");
  2. // Execute query to get names of all employees.
  3. SqlFieldsQuery sql = new SqlFieldsQuery(
  4. "select concat(firstName, ' ', lastName) from Person");
  5. // Iterate over the result set.
  6. try (QueryCursor<List<?>> cursor = cache.query(sql) {
  7. for (List<?> row : cursor)
  8. System.out.println("personName=" + row.get(0));
  9. }

可查询字段定义
SqlQuerySqlFieldsQuery中的指定字段可以被访问之前,他们需要在POJO层面加上注解,或者在QueryEntity中进行定义,以便SQL引擎可以感知到它们,后续章节还会详述。
访问条目的键和值
在SQL查询中使用_key_val关键字,可以指向条目的整个键和值,而不用写每个字段,如果在SQL查询执行的结果中返回键和值,也可以使用这两个关键字。

4.2.4.跨缓存查询

作为单个SqlQuerySqlFieldsQuery查询的一部分,查询的数据可以来自多个缓存。这时,缓存名会扮演类似传统RDBMS中SQL查询的模式名的角色。缓存的名字,用于创建IgniteCache的实例,如果用于查询的话,会作为默认的模式名并且不需要显式地指定。其余的存储于不同缓存中的对象,也会被查询,但是需要加上它的缓存名(额外的模式名)作为前缀。

  1. // In this example, suppose Person objects are stored in a
  2. // cache named 'personCache' and Organization objects
  3. // are stored in a cache named 'orgCache'.
  4. IgniteCache<Long, Person> personCache = ignite.cache("personCache");
  5. // Select with join between Person and Organization to
  6. // get the names of all the employees of a specific organization.
  7. SqlFieldsQuery sql = new SqlFieldsQuery(
  8. "select Person.name "
  9. + "from Person as p, \"orgCache\".Organization as org where "
  10. + "p.orgId = org.id "
  11. + "and org.name = ?");
  12. // Execute the query and obtain the query result cursor.
  13. try (QueryCursor<List<?>> cursor = personCache.query(sql.setArgs("Ignite"))) {
  14. for (List<?> row : cursor)
  15. System.out.println("Person name=" + row.get(0));
  16. }

上面的示例中,会从personCache创建一个SqlFieldsQuery的实例,之后personCache会作为默认的模式名,这就是Person对象没有通过显式指定的模式名(from Person as p)就能访问的原因。而Organization对象,因为它存储于一个单独的名为orgCache的缓存中,所以在该查询中这个缓存的名字作为模式名必须显式地指定("orgCache".Organization as org)。

修改缓存名
如果希望使用不同于缓存名的模式名,可以通过调用CacheConfiguration.setSqlSchema(...)方法解决。

4.2.5.分布式关联

Ignite支持并置和非并置的分布式SQL关联,此外,如果数据位于不同的缓存,Ignite可以进行跨缓存的关联。

  1. IgniteCache<Long, Person> cache = ignite.cache("personCache");
  2. // SQL join on Person and Organization.
  3. SqlQuery sql = new SqlQuery(Person.class,
  4. "from Person as p, \"orgCache\".Organization as org"
  5. + "where p.orgId = org.id "
  6. + "and lower(org.name) = lower(?)");
  7. // Find all persons working for Ignite organization.
  8. try (QueryCursor<Entry<Long, Person>> cursor = cache.query(sql.setArgs("Ignite"))) {
  9. for (Entry<Long, Person> e : cursor)
  10. System.out.println(e.getValue().toString());
  11. }

分区复制模式缓存之间的关联也可以无限制地进行。
然而,如果在至少两个分区模式的数据集之间进行关联,那么一定要确保要么关联的键是并置的,要么为查询开启了非并置关联参数,两种类型的分布式关联模式下面会详述。
分布式并置关联
默认情况下,如果一个SQL关联需要跨越多个Ignite缓存,那么所有的缓存都需要是并置的,否则,查询完成后会得到一个不完整的结果集,这是因为在关联阶段一个节点的可用数据只是本地的,如图1所示,首先,一个SQL查询会被发送到待关联数据所在的节点(Q),然后查询在每个节点的本地数据上立即执行(E(Q)),最后,所有的执行结果都会在客户端进行聚合(R)。

分布式非并置关联
虽然关系并置是一个强大的概念,即一旦配置了应用的业务实体(缓存),就可以以最优的方式执行跨缓存的关联,并且返回一个完整且一致的结果集。但还有一种可能就是,无法并置所有的数据,这时,就可能无法执行满足需求的所有SQL查询了。

在实践中不要过度使用基于非并置的分布式关联的方式,因为这种关联方式的性能差于基于关系并置的关联,因为要完成这个查询,要有更多的网络开销和节点间的数据移动。

当通过SqlQuery.setDistributedJoins(boolean)参数为一个SQL查询启用了非并置的分布式关联之后,查询映射的节点就会从远程节点通过发送广播或者单播请求的方式获取缺失的数据(本地不存在的数据),正如图2所示,有一个潜在的数据移动步骤(D(Q))。潜在的单播请求只会在关联在主键(缓存键)或者关系键上完成之后才会发送,因为执行关联的节点知道缺失数据的位置,其他所有的情况都会发送广播请求。

不管是广播还是单播请求,都是由一个节点发送到另一个节点来获取缺失的数据,是按照顺序执行的。SQL引擎会将所有的请求组成若干批量,这个批量的大小是由SqlQuery.setPageSize(int)参数管理的。

下面的代码片段是从Ignite的发行版的CacheQueryExample中提取的:

  1. IgniteCache<AffinityKey<Long>, Person> cache = ignite.cache("personCache");
  2. // SQL clause query with join over non-collocated data.
  3. String joinSql =
  4. "from Person, \"orgCache\".Organization as org " +
  5. "where Person.orgId = org.id " +
  6. "and lower(org.name) = lower(?)";
  7. SqlQuery qry = new SqlQuery<AffinityKey<Long>, Person>(Person.class, joinSql).setArgs("ApacheIgnite");
  8. // Enable distributed joins for the query.
  9. qry.setDistributedJoins(true);
  10. // Execute the query to find out employees for specified organization.
  11. System.out.println("Following people are 'ApacheIgnite' employees (distributed join): ", cache.query(qry).getAll());

要了解详细信息,可以参照非并置的分布式关联

4.2.6.已知的限制

事务性SQL
目前,SQL查询仅仅支持原子模式,意味着如果有一个事务已经提交了值A而值B正在提交过程中,然后如果有一个并行的SQL查询的话,会看到A而看不到B。

多版本并发控制(MVCC)
一旦Ignite SQL网格使用MVCC进行控制,SQL网格也会支持事务模式。

4.2.7.示例

关于本文描述的分布式关联如何使用的完整示例,会作为Ignite发行版的一部分进行分发,名为CacheQueryExampleGitHub上也有。

4.3.本地查询

有时,SQL网格中查询的执行会从分布式模式回落至本地模式,在本地模式中,查询会简单地传递至底层的H2引擎,他只会处理本地节点的数据集。
这些场景包括:

即使查询执行时网络拓扑发生变化(新节点加入集群或者老节点离开集群),前两个场景也会一直提供完整而一致的结果集。
然而,在应用显式开启本地模式的第三个场景中需要注意,原因是如果希望在部分节点的分区缓存上执行本地查询时网络还发生了变化,那么可能得到结果集的一部分,因为这时会触发一个并行的数据再平衡过程。SQL引擎无法处理这个特殊情况。如果仍然希望在分区缓存上执行本地查询,那么需要将查询作为affinityRun(...)或者affinityCall(...)方法的一部分。

4.4.分布式DML

4.4.1.摘要

Ignite SQL网格不仅仅可以使用ANSI-99语法的SQL在数据网格上查询数据,还可以使用众所周知的DML语句,比如INSERT、UPDATE或者DELETE修改数据。利用这个优势,依赖Ignite的SQL能力完全可以将其当做分布式内存数据库。

ANSI-99 SQL兼容
DML查询,和所有的SELECT查询一样,都是兼容ANSI-99 SQL标准的。

Ignite在内存中的数据都是以键-值对的形式存储的,因此所有和DML相关的操作都会被转换为相对应的基于键-值的缓存操作命令,比如cache.put(...)或者cache.invokeAll(...)。下面会深入地了解这些DML语句是如何实现的。

4.4.2.DML API

通常来说,所有的DML语句会被拆分为两组,一个是往缓存中添加条目(INSERTMERGE),还有就是修改已有的数据(UPDATEDELETE)。
要在Java中执行这些语句需要使用已有的用于SELECT查询的API - SqlFieldsQueryAPI,DML操作使用的API与只读查询是一致的,返回结果也是QueryCursor<List<?>>。唯一的不同是作为DML语句执行的结果,QueryCursor<List<?>>是只有一个long类型的单条目的List<?>,这个数值表示该DML语句影响的缓存条目的数量。而作为SELECT语句的结果,QueryCursor<List<?>>会包含一个从缓存获得的条目列表。

其他的API
DML API不受限于Java,也可以使用ODBC或者JDBC驱动接入Ignite集群,然后执行DML语句。

4.4.3.基本配置

在Ignite中要进行DML操作,需要使用基于QueryEntity的方式或者使用@QuerySqlField注解来配置所有可查询的字段,比如:
使用@QuerySqlField注解:

  1. public class Person {
  2. /** Field will be accessible from DML statements. */
  3. @QuerySqlField
  4. private final String firstName;
  5. /** Indexed field that will be accessible from DML statements. */
  6. @QuerySqlField (index = true)
  7. private final String lastName;
  8. /** Field will NOT be accessible from DML statements. */
  9. private int age;
  10. public Person(String firstName, String lastName) {
  11. this.firstName = firstName;
  12. this.lastName = lastName;
  13. }
  14. }

使用QueryEntity:

  1. <bean class="org.apache.ignite.configuration.CacheConfiguration">
  2. <property name="name" value="personCache"/>
  3. <!-- Configure query entities -->
  4. <property name="queryEntities">
  5. <list>
  6. <bean class="org.apache.ignite.cache.QueryEntity">
  7. <!-- Registering key's class. -->
  8. <property name="keyType" value="java.lang.Long"/>
  9. <!-- Registering value's class. -->
  10. <property name="valueType"
  11. value="org.apache.ignite.examples.Person"/>
  12. <!--
  13. Defining fields that will be accessible from DML side
  14. -->
  15. <property name="fields">
  16. <map>
  17. <entry key="firstName" value="java.lang.String"/>
  18. <entry key="lastName" value="java.lang.String"/>
  19. </map>
  20. </property>
  21. <!--
  22. Defining which fields, listed above, will be treated as
  23. indexed fields as well.
  24. -->
  25. <property name="indexes">
  26. <list>
  27. <!-- Single field (aka. column) index -->
  28. <bean class="org.apache.ignite.cache.QueryIndex">
  29. <constructor-arg value="lastName"/>
  30. </bean>
  31. </list>
  32. </property>
  33. </bean>
  34. </list>
  35. </property>
  36. </bean>

除了通过@QuerySqlField加注的或者通过QueryEntity定义的所有字段,还有两个为每个在SQL网格中注册的对象类型预定义的字段_key_val,这几个预定义字段指向缓存中存储的对象的整个键和值,他们可以像下面这样在DML中直接使用:

  1. //Preparing cache configuration.
  2. CacheConfiguration<Long, Person> cacheCfg = new CacheConfiguration<>
  3. ("personCache");
  4. //Registering indexed/queryable types.
  5. cacheCfg.setIndexedTypes(Long.class, Person.class);
  6. //Starting the cache.
  7. IgniteCache<Long, Person> cache = ignite.cache(cacheCfg);
  8. // Inserting a new key-value pair referring to prefedined `_key` and `_value`
  9. // fields for Person type.
  10. cache.query(new SqlFieldsQuery("INSERT INTO Person(_key, _val) VALUES(?, ?)")
  11. .setArgs(1L, new Person("John", "Smith")));

如果倾向于处理具体的字段,而不是通过执行查询处理整个对象的值,可以执行下面这样的查询:

  1. IgniteCache<Long, Person> cache = ignite.cache(cacheCfg);
  2. cache.query(new SqlFieldsQuery(
  3. "INSERT INTO Person(_key, firstName, lastName) VALUES(?, ?, ?)").
  4. setArgs(1L, "John", "Smith"));

注意DML引擎会根据firstNamelastName重新创建一个Person对象,然后将其注入缓存,但是这些字段是需要通过QueryEntity或者@QuerySqlField注解进行定义的,就像上面描述的那样。

4.4.4.高级配置

自定义键
如果只使用预定义的SQL数据类型作为缓存键,那么就没必要对和DML相关的配置做额外的操作,这些数据类型在GridQueryProcessor#SQL_TYPES常量中进行定义,列举如下:

预定义SQL数据类型
1.所有的基本类型及其包装器,除了charCharacter
2.String;
3.BigDecimal;
4.byte[];
5.java.util.Date, java.sql.Date, java.sql.Timestamp;
6.java.util.UUID

然而,如果决定引入复杂的自定义缓存键,那么在DML语句中要指向这些字段就需要:

下面的例子展示了如何实现:
Java:

  1. // Preparing cache configuration.
  2. CacheConfiguration cacheCfg = new CacheConfiguration<>("personCache");
  3. // Creating the query entity.
  4. QueryEntity entity = new QueryEntity("CustomKey", "Person");
  5. // Listing all the queryable fields.
  6. LinkedHashMap<String, String> flds = new LinkedHashMap<>();
  7. flds.put("intKeyField", Integer.class.getName());
  8. flds.put("strKeyField", String.class.getName());
  9. flds.put("firstName", String.class.getName());
  10. flds.put("lastName", String.class.getName());
  11. entity.setFields(flds);
  12. // Listing a subset of the fields that belong to the key.
  13. Set<String> keyFlds = new HashSet<>();
  14. keyFlds.add("intKeyField");
  15. keyFlds.add("strKeyField");
  16. entity.setKeyFields(keyFlds);
  17. // End of new settings, nothing else here is DML related
  18. entity.setIndexes(Collections.<QueryIndex>emptyList());
  19. cacheCfg.setQueryEntities(Collections.singletonList(entity));
  20. ignite.createCache(cacheCfg);

XML:

  1. <bean class="org.apache.ignite.configuration.CacheConfiguration">
  2. <property name="name" value="personCache"/>
  3. <!-- Configure query entities -->
  4. <property name="queryEntities">
  5. <list>
  6. <bean class="org.apache.ignite.cache.QueryEntity">
  7. <!-- Registering key's class. -->
  8. <property name="keyType" value="CustomKey"/>
  9. <!-- Registering value's class. -->
  10. <property name="valueType"
  11. value="org.apache.ignite.examples.Person"/>
  12. <!--
  13. Defining all the fields that will be accessible from DML.
  14. -->
  15. <property name="fields">
  16. <map>
  17. <entry key="firstName" value="java.lang.String"/>
  18. <entry key="lastName" value="java.lang.String"/>
  19. <entry key="intKeyField" value="java.lang.Integer"/>
  20. <entry key="strKeyField" value="java.lang.String"/>
  21. </map>
  22. </property>
  23. <!-- Defining the subset of key's fields -->
  24. <property name="keyFields">
  25. <set>
  26. <value>intKeyField<value/>
  27. <value>strKeyField<value/>
  28. </set>
  29. </property>
  30. </bean>
  31. </list>
  32. </property>
  33. </bean>

自定义缓存键的HashCode解析和一致性比较
创建好自定义缓存键,也用QueryEntity定义好了字段之后,就需要注意缓存键的哈希值计算方式以及与其他键的比较方式。
BinaryArrayIdentityResolver会作为Ignite中存储和传输以及序列化的所有对象默认的哈希值计算器以及一致性比较器,自定义的复杂缓存键,也会使用这个解析器,除非将其变更为BinaryFieldIdentityResolver,这个对于DML语句中的键是更合适的,或者,也可以切换为自定义解析器。

4.4.5.DML操作

MERGE
MERGE是一个非常简单的操作,因为它会被翻译成cache.put(...)或者cache.putAll(...),具体是哪一个,取决于MERGE语句涉及的要插入或者要更新的记录的数量。
下面的示例显示如何通过MERGE命令来更新数据集。一个是提供了条目列表,一个是通过执行子查询注入一个结果集。
MERGE(条目列表):

  1. cache.query(new SqlFieldsQuery("MERGE INTO Person(_key, firstName, lastName)" + "values (1, 'John', 'Smith'), (5, 'Mary', 'Jones')"));

MERGE(子查询):

  1. cache.query(new SqlFieldsQuery("MERGE INTO someCache.Person(_key, firstName, lastName) (SELECT _key + 1000, firstName, lastName " +
  2. "FROM anotherCache.Person WHERE _key > ? AND _key < ?)").setArgs(100, 200);

INSERT
MERGEINSERT命令的不同在于,后者添加的条目必须是缓存中不存在的。
如果要把一个键值对插入缓存,那么最后,INSERT语句会被转换为cache.putIfAbsent(...)操作,否则,如果插入的是多个键值对,那么DML引擎会为每个对创建一个EntryProcessor,然后使用cache.invokeAll(...)将数据注入缓存。
下面的示例显示如何通过INSERT命令插入一个数据集,一个是提供了条目列表,一个是通过执行子查询注入一个结果集。
INSERT(条目列表):

  1. cache.query(new SqlFieldsQuery("INSERT INTO Person(_key, firstName, " +
  2. "lastName) values (1, 'John', 'Smith'), (5, 'Mary', 'Jones')"));

INSERT(子查询):

  1. cache.query(new SqlFieldsQuery("INSERT INTO someCache.Person(_key, firstName, lastName) (SELECT _key + 1000, firstName, secondName " +
  2. "FROM anotherCache.Person WHERE _key > ? AND _key < ?)").setArgs(100, 200);

UPDATE
这个操作会更新缓存中的值的每个字段。
开始时,SQL引擎会根据UPDATE语句的WHERE条件生成并且执行一个SELECT查询,然后会修改满足条件的已有值。
修改的执行是利用cache.invokeAll(...)实现的。基本上来说,这意味着一旦SELECT查询的结果准备好,SQL引擎就会准备一定数量的EntryProcessors然后执行cache.invokeAll(...)操作,下一步,EntryProcessors修改完数据之后,会进行额外的检查来确保在SELECT和数据实际更新之间没有其他干扰。
下面这个简单示例显示了如何执行UPDATE语句。

  1. cache.query(new SqlFieldsQuery("UPDATE Person set lastName = ? " +
  2. "WHERE _key >= ?").setArgs("Jones", 2L));

UPDATE语句无法更新缓存键及其字段
原因是缓存键的状态决定了内部数据的布局及其一致性(键的哈希及其关系,索引完整性),所以目前除非先将其删除,否则无法更新缓存键。比如下面的查询:
UPDATE _key = 11 where _key = 10;
会导致下面的缓存操作:
val = get(10);
put(11, val);
remove(10);

DELETE
DELETE语句的执行也会被拆分为两个阶段,与UPDATE语句的执行类似。
首先,SQL引擎会使用SELECT语句来收集满足WHERE条件并且要被删除的缓存键,下一步,拿到这些键后,会准备一定数量的EntryProcessors然后执行cache.invokeAll(...)操作,当数据将被删除时,会进行额外的检查来确保在SELECT和数据实际删除之间没有其他干扰。
下面这个简单示例显示了如何执行DELETE语句。

  1. cache.query(new SqlFieldsQuery("DELETE FROM Person " +
  2. "WHERE _key >= ?").setArgs(2L));

流模式
使用Ignite的JDBC驱动,会通过流模式来获得更快的数据预加载。

4.4.6.修改顺序

如果一个DML语句插入/更新指向_val字段的整个值的同时,还试图修改属于_val的某一个字段时,那么,变更的顺序如下:

不管DML语句事实上如何定义,这个顺序是不会改变的。比如下面的语句执行完毕后,Person的最终值会是Mike Smith,尽管在查询中_val位于firstName后面。

  1. cache.query(new SqlFieldsQuery("INSERT INTO Person(_key, firstName, _val)" +
  2. " VALUES(?, ?, ?)").setArgs(1L, "Mike", new Person("John", "Smith")));

这与下面的查询的执行类似,这里_val在前面:

  1. cache.query(new SqlFieldsQuery("INSERT INTO Person(_key, _val, firstName)" +
  2. " VALUES(?, ?, ?)").setArgs(1L, new Person("John", "Smith"), "Mike"));

对于_val及其字段变更顺序的问题,INSERTUPDATEMERGE语句都是一样的。

4.4.7.并发修改

如上所述,UPDATEDELETE语句在内部会生成SELECT查询,目的是将查询执行的结果集作为要更新的缓存条目的集合。这个集合中的键是不会被锁定的,因此有一种可能就是在并发的情况下,属于某个键的值会被其他的查询修改。DML引擎已经实现了一种技术,即首先避免锁定键,然后保证在DML语句执行更新时值是最新的。
总体而言,引擎会并发地检测要更新的缓存条目的子集,然后重新执行SELECT语句来限制要修改的键的范围。
比如下面的要执行的UPDATE语句:

  1. // Adding the cache entry.
  2. cache.put(1, new Person("John", "Smith");
  3. // Updating the entry.
  4. cache.query(new SqlFieldsQuery("UPDATE Person set firstName = ? " +
  5. "WHERE lastName = ?").setArgs("Mike", "Smith"));

firstNamelastName更新之前,DML引擎会生成SELECT查询来获得符合UPDATE语句的WHERE条件的缓存条目,语句如下:

  1. SELECT _key, _value, "Mike" from Person WHERE lastName = "Smith"

之后通过SELECT获得的条目会被并发地更新:

  1. cache.put(1, new Person("Sarah", "Connor"))

DML引擎在UPDATE语句执行的更新阶段会检测到键为1的缓存条目要被修改,之后会暂停更新并且重新执行一个SELECT查询的修订版本来获得最新的条目值:

  1. SELECT _key, _value, "Mike" from Person WHERE secondName = "Smith"
  2. AND _key IN (SELECT * FROM TABLE(KEY long = [ 1 ]))

这个查询只会为过时的键执行,本例中只有一个键1
这个过程会一直重复,直到DML引擎确信在更新阶段所有的条目都已经更新到最新版。尝试次数的最大值是4,目前并没有配置参数来改变这个值。

DML引擎不会为并发删除的条目重复执行SELECT语句,重复执行的查询只针对还在缓存中的条目。

4.4.8.已知的限制

WHERE条件中的子查询
INSERTMERGE语句中的子查询和UPDATEDELETE操作自动生成的SELECT查询一样,如有必要都会被分布化然后执行,要么是并置,要么是非并置的模式。
然而,如果WHERE语句里面有一个子查询,那么他是不会以非并置的分布式模式执行的,子查询始终都会以并置的模式在本地节点上执行。
比如,有这样一个查询:

  1. DELETE FROM Person WHERE _key IN
  2. (SELECT personId FROM "salary".Salary s WHERE s.amount > 2000)

然后DML引擎会生成SELECT查询来获得要删除的条目列表,这个查询会在整个集群中分布化并且执行,如下所示:

  1. SELECT _key, _val FROM Person WHERE _key IN
  2. (SELECT personId FROM "salary".Salary s WHERE s.amount > 2000)

然而,IN子句中的子查询(SELECT personId FROM "salary".Salary ...)不会被进一步分布化,只会在一个集群节点的本地数据集上执行。
事务性支持
目前,DML仅仅支持原子模式,意味着如果有一个DML查询作为Ignite事务的一部分,那么它是不会加入事务的写队列,会被立刻执行。

多版本并发控制(MVCC)
一旦Ignite SQL网格使用MVCC进行控制,DML操作也会支持事务模式。

DML语句的执行计划支持
目前DML操作不支持EXPLAIN
一个方法就是执行UPDATEDELETE语句自动生成的SELECT语句或者DML语句使用的INSERTMERGE语句的执行计划,这样会提供一个要执行的DML操作所使用的索引情况。

4.4.9.示例

Ignite在源代码中包含了一个可以立即执行的CacheQueryDmlExample,这个示例演示了上面提到的所有DML操作的用法。

4.5.模式和索引

4.5.1.摘要

目前,Ignite可以通过基于注解或者基于QueryEntity的方式定义模式,每个模式都会绑定到一个Ignite缓存,缓存的名字默认会作为SQL查询的模式名。

数据定义语言支持
Ignite的下个版本计划提供对DDL语句的支持,有了这个特性之后,就可以通过标准的SQL命令,比如CREATE/ALTER/DROP TABLE或者CREATE/DROP INDEX来定义模式、缓存、索引,以及管理它们。

Ignite支持高级的索引功能,可以定义包括各种参数的单字段(也可以叫做列)或者分组索引,这些参数可以管理索引位于Java堆或者堆外空间等等。
Ignite中以分布式方式保持的索引和缓存数据集一样,每一个节点都保存数据的一个特定子集,还会保持和管理与这个数据对应的索引。
本章节会描述如何像查询字段那样,使用两种方法来定义和管理索引,以及如何在Ignite支持的特定索引实现之间进行切换。

4.5.2.基于注解的配置

索引,和可查询的字段一样,是可以通过编程的方式用@QuerySqlField进行配置的。
如下所示,期望的字段已经加注了该注解。
Java:

  1. public class Person implements Serializable {
  2. /** Indexed field. Will be visible for SQL engine. */
  3. @QuerySqlField (index = true)
  4. private long id;
  5. /** Queryable field. Will be visible for SQL engine. */
  6. @QuerySqlField
  7. private String name;
  8. /** Will NOT be visible for SQL engine. */
  9. private int age;
  10. /**
  11. * Indexed field sorted in descending order.
  12. * Will be visible for SQL engine.
  13. */
  14. @QuerySqlField(index = true, descending = true)
  15. private float salary;
  16. }

Scala:

  1. case class Person (
  2. /** Indexed field. Will be visible for SQL engine. */
  3. @(QuerySqlField @field)(index = true) id: Long,
  4. /** Queryable field. Will be visible for SQL engine. */
  5. @(QuerySqlField @field) name: String,
  6. /** Will NOT be visisble for SQL engine. */
  7. age: Int
  8. /**
  9. * Indexed field sorted in descending order.
  10. * Will be visible for SQL engine.
  11. */
  12. @(QuerySqlField @field)(index = true, descending = true) salary: Float
  13. ) extends Serializable {
  14. ...
  15. }

idsalary都是索引列,id字段升序排列(默认),而salary降序排列。
如果不希望索引一个字段,但是仍然想在SQL查询中使用它,那么在加注解时可以忽略index = true参数,这样的字段称为可查询字段,举例来说,上面的name就被定义为可查询字段。
最后,age既不是可查询字段也不是索引字段,在Ignite中,从SQL查询的角度看就是不可见的。

Scala注解
在Scala类中,@QuerySqlField注解必须和@Field注解一起使用,这样的话这个字段对于Ignite才是可见的,就像这样的:@(QuerySqlField @field)
作为替代,也可以使用ignite-scalar模块的@ScalarCacheQuerySqlField注解,他不过是@Field注解的别名。

注册索引类型
定义了索引字段和可查询字段之后,就需要和他们所属的对象类型一起,在SQL引擎中注册。
要告诉Ignite哪些类型应该被索引,需要通过CacheConfiguration.setIndexedTypes方法传入键-值对,如下所示:

  1. / Preparing configuration.
  2. CacheConfiguration<Long, Person> ccfg = new CacheConfiguration<>();
  3. // Registering indexed type.
  4. ccfg.setIndexedTypes(Long.class, Person.class);

注意,这个方法只接收成对的类型,一个键类一个值类,基本类型需要使用包装器类。

预定义字段
除了用@QuerySqlField注解标注的所有字段,每个表都有两个特别的预定义字段:_key_val,它表示到整个键对象和值对象的链接。这很有用,比如当他们中的一个是基本类型并且希望用它的值进行过滤时。要做到这一点,执行一个SELECT * FROM Person WHERE _key = 100查询即可。

多亏了二进制编组器,不需要将索引类型类加入集群节点的类路径中,SQL查询引擎不需要对象反序列化就可以钻取索引和可查询字段的值。

分组索引
当查询条件复杂时可以使用多字段索引来加快查询的速度,这时可以用@QuerySqlField.Group注解。如果希望一个字段参与多个分组索引时也可以将多个@QuerySqlField.Group注解加入orderedGroups中。
比如,下面的Person类中age字段加入了名为age_salary_idx的分组索引,他的分组序号是0并且降序排列,同一个分组索引中还有一个字段salary,他的分组序号是3并且升序排列。最重要的是salary字段还是一个单列索引(除了orderedGroups声明之外,还加上了index = true)。分组中的order不需要是什么特别的数值,他只是用于分组内的字段排序。
Java:

  1. public class Person implements Serializable {
  2. /** Indexed in a group index with "salary". */
  3. @QuerySqlField(orderedGroups={@QuerySqlField.Group(
  4. name = "age_salary_idx", order = 0, descending = true)})
  5. private int age;
  6. /** Indexed separately and in a group index with "age". */
  7. @QuerySqlField(index = true, orderedGroups={@QuerySqlField.Group(
  8. name = "age_salary_idx", order = 3)})
  9. private double salary;
  10. }

注意,将@QuerySqlField.Group放在@QuerySqlField(orderedGroups={...})外面是无效的。

4.5.3.基于QueryEntity的配置

索引和字段也可以通过org.apache.ignite.cache.QueryEntity进行配置,它便于利用Spring进行基于XML的配置。
在上面基于注解的配置涉及的所有概念,对于基于QueryEntity的方式也都有效,深入地说,通过@QuerySqlField配置的字段的类型然后通过CacheConfiguration.setIndexedTypes注册过的,在内部也会被转换为查询实体。
下面的示例显示的是如何像可查询字段那样定义一个单一字段和分组索引。

  1. <bean class="org.apache.ignite.configuration.CacheConfiguration">
  2. <property name="name" value="mycache"/>
  3. <!-- Configure query entities -->
  4. <property name="queryEntities">
  5. <list>
  6. <bean class="org.apache.ignite.cache.QueryEntity">
  7. <!-- Setting indexed type's key class -->
  8. <property name="keyType" value="java.lang.Long"/>
  9. <!-- Setting indexed type's value class -->
  10. <property name="valueType"
  11. value="org.apache.ignite.examples.Person"/>
  12. <!--
  13. Defining fields that will be either indexed or queryable.
  14. Indexed fields are added to 'indexes' list below.
  15. -->
  16. <property name="fields">
  17. <map>
  18. <entry key="id" value="java.lang.Long"/>
  19. <entry key="name" value="java.lang.String"/>
  20. <entry key="salary" value="java.lang.Long "/>
  21. </map>
  22. </property>
  23. <!--
  24. Defining which fields, listed above, will be treated as
  25. indexed fields.
  26. -->
  27. <property name="indexes">
  28. <list>
  29. <!-- Single field (aka. column) index -->
  30. <bean class="org.apache.ignite.cache.QueryIndex">
  31. <constructor-arg value="id"/>
  32. </bean>
  33. <!-- Group index. -->
  34. <bean class="org.apache.ignite.cache.QueryIndex">
  35. <constructor-arg>
  36. <list>
  37. <value>id</value>
  38. <value>salary</value>
  39. </list>
  40. </constructor-arg>
  41. <constructor-arg value="SORTED"/>
  42. </bean>
  43. </list>
  44. </property>
  45. </bean>
  46. </list>
  47. </property>
  48. </bean>

4.5.4.基于跳跃表以及快照的索引

当索引存储于Java堆上时,SQL网格提供了两种索引实现。
第一个是基于跳跃表数据结构的,它也是默认的实现。
第二个实现是基于一个快速复制的AVL树的修改版,这个实现在Ignite中被称为一个快照,可以通过CacheConfiguration.setSnapshotableIndex(...)方法开启。
对于下面讨论的堆外模式,Ignite只提供一种索引实现,是一个快速复制的AVL树的修改版。

4.5.5.堆外SQL索引

Ignite支持将索引数据放在堆外内存,这个设计对于避免在堆上保存特别大的数据集导致频繁的垃圾回收以及不可预知的响应时间是很有用的。
Ignite默认将SQL索引存储于堆内,如果将CacheConfiguration.setMemoryMode配置为堆外内存模式之一:OFFHEAP_TIEREDOFFHEAP_VALUES,或者将CacheConfiguration.setOffHeapMaxMemory属性配置为>=0,Ignite会将索引保存于堆外。
要通过开启堆外模式来提高SQL查询的性能,可以试着增加CacheConfiguration.setSqlOnheapRowCacheSize()属性的值,它的默认值是10000.
Java:

  1. CacheConfiguration<Object, Object> ccfg = new CacheConfiguration<>();
  2. // Set unlimited off-heap memory for cache and enable off-heap indexes.
  3. ccfg.setOffHeapMaxMemory(0);
  4. // Cache entries will be placed on heap and can be evited to off-heap.
  5. ccfg.setMemoryMode(ONHEAP_TIERED);
  6. ccfg.setEvictionPolicy(new RandomEvictionPolicy(100_000));
  7. // Increase size of SQL on-heap row cache for off-heap indexes.
  8. ccfg.setSqlOnheapRowCacheSize(100_000);

索引实现
对于堆外模式,Ignite只提供了一个索引实现,它是快速克隆的AVL树的修改版。

4.5.6.索引的权衡

为应用选择索引时,需要考虑很多事情。

索引每个字段是错误的!

有序索引示例
| A | B | C |
| 1 | 2 | 3 |
| 1 | 4 | 2 |
| 1 | 4 | 4 |
| 2 | 3 | 5 |
| 2 | 4 | 4 |
| 2 | 4 | 5 |
任意条件,比如a = 1 and b > 3,都会被视为有界范围,在log(N)时间内两个边界在索引中可以被快速检索到,然后结果就是两者之间的任何数据。
下面的条件会使用索引:
a = ?
a = ? and b = ?
a = ? and b = ? and c = ?
从索引的角度,条件a = ?c = ?不会好于a = ?
明显地,半界范围a > ?可以工作得很好。

4.6.其他特性

4.6.1.查询取消

Ignite中有两种方式停止长时间运行的SQL查询,SQL查询时间长的原因,比如使用了未经优化的索引等。
第一个方法是为特定的SqlQuerySqlFieldsQuery设置查询执行的超时时间。

  1. SqlQuery qry = new SqlQuery<AffinityKey<Long>, Person>(Person.class, joinSql);
  2. // Setting query execution timeout
  3. qry.setTimeout(10_000, TimeUnit.SECONDS);

第二个方法是使用QueryCursor.close()来终止查询。

  1. SqlQuery qry = new SqlQuery<AffinityKey<Long>, Person>(Person.class, joinSql);
  2. // Getting query cursor.
  3. QueryCursor<List> cursor = cache.query(qry);
  4. // Executing query.
  5. ....
  6. // Halting the query that might be still in progress.
  7. cursor.close();

Ignite的1.8及其以后版本开始支持查询取消的API。

4.6.2.自定义SQL函数

Ignite的SQL引擎支持通过额外用Java编写的自定义SQL函数,来扩展ANSI-99规范定义的SQL函数集。
一个自定义SQL函数仅仅是一个加注了@QuerySqlFunction注解的公共静态方法。

  1. // Defining a custom SQL function.
  2. public class MyFunctions {
  3. @QuerySqlFunction
  4. public static int sqr(int x) {
  5. return x * x;
  6. }
  7. }

持有自定义SQL函数的类需要使用setSqlFunctionClasses(...)方法在特定的CacheConfiguration中注册。

  1. // Preparing a cache configuration.
  2. CacheConfiguration cfg = new CacheConfiguration();
  3. // Registering the class that contains custom SQL functions.
  4. cfg.setSqlFunctionClasses(MyFunctions.class);

经过了上述配置的缓存部署之后,在SQL查询中就可以随意地调用自定义函数了,如下所示:

  1. // Preparing the query that uses customly defined 'sqr' function.
  2. SqlFieldsQuery query = new SqlFieldsQuery(
  3. "SELECT name FROM Blocks WHERE sqr(size) > 100");
  4. // Executing the query.
  5. cache.query(query).getAll();

在自定义SQL函数可能要执行的所有节点上,通过CacheConfiguration.setSqlFunctionClasses(...)注册的类都需要添加到类路径中,否则在自定义函数执行时会抛出ClassNotFoundException异常。

4.7.配置参数

可以通过调整一些与SQL查询有关的参数,来影响查询执行的行为。
这些参数分为全局参数和查询级参数,全局参数在CacheConfiguration层面配置,在该缓存上执行的所有查询都会受到影响。
缓存配置参数

属性名 描述 默认值
setSqlSchema(...) 配置当前缓存使用的SQL模式名,这个名字需要符合SQL的ANSI-99标准,加引号的区分大小写,不加引号的不区分大小写。 缓存名
setSqlEscapeAll(...) 如果配置为true,所有的SQL表和字段名都会加上双引号,比如"tableName"."fieldsName",这样会强制字段名区分大小写,同时也允许表名和字段名有特殊字符。 false
setSqlOnheapRowCacheSize(...) 定义缓存在堆内的SQL行数,来避免每次SQL索引访问的反序列化,这个参数只有在该缓存开启了堆外的时候才会起作用。 10,240
setSnapshotableIndex(...) 为存储在Java堆内的索引数据开启快照索引实现。 false

SqlFieldsSqlFieldsQuery配置参数

属性名 描述 默认值
setCollocated(...) 为了优化带有GROUP BY的查询的目的使用的并置标志,当Ignite执行分布式SQL查询时,它会向单个节点发送子查询,如果事先知道要查询的数据是在同一个节点上并置在一起的然后又对并置键(主键或者关系键)进行分组,Ignite会通过在远程节点分组数据而有一个显著的性能提升和网络优化。 false
setDistributedJoins(...) 为一个特定的查询开启非并置模式的分布式关联。 false
setEnforceJoinOrder(...) 配置一个标志来强制查询中的表关联顺序,如果配置为true,查询优化器就不会对join子句的表进行重新排序。 false
setLocal(...) 强制查询在纯本地模式下执行。 false
setPageSize(...) 定义单个响应中可以传输到发起节点的最大条目数, 1024
setTimeout(...) 配置查询执行的超时时间,如果正在执行的查询超过了该值,其会被自动取消。默认是禁用的,Ignite的1.8及其以后版本才可用。 0

4.8.JDBC驱动

4.8.1.JDBC连接

Ignite提供了JDBC驱动,使得在JDBC API端就可以通过标准SQL查询从缓存中获得分布式数据,以及使用DML语句比如INSERTUPDATE或者DELETE更新数据。
Ignite中,JDBC连接URL的规则如下:

  1. jdbc:ignite:cfg://[<params>@]<config_url>
  1. param1=value1:param2=value2:...:paramN=valueN

它支持如下的参数:

属性 描述 默认值
cache 缓存名,如果未定义会使用默认的缓存,区分大小写
nodeId 要执行的查询所在节点的Id,对于在本地查询是有用的
local 查询只在本地节点执行,这个参数和nodeId参数都是通过指定节点来限制数据集 false
collocated 优化标志,当Ignite执行一个分布式查询时,他会向单个的集群节点发送子查询,如果提前知道要查询的数据已经被并置到同一个节点,Ignite会有显著的性能提升和网络优化 false
distributedJoins 可以在非并置的数据上使用分布式关联。 false
streaming 通过INSERT语句为本链接开启批量数据加载模式,具体可以参照后面的流模式相关章节。 false
streamingAllowOverwrite 通知Ignite对于重复的已有键,覆写它的值而不是忽略他们,具体可以参照后面的流模式相关章节。 false
streamingFlushFrequency 超时时间,毫秒,数据流处理器用于刷新数据,数据默认会在连接关闭时刷新,具体可以参照后面的流模式相关章节。 0
streamingPerNodeBufferSize 数据流处理器的每节点缓冲区大小,具体可以参照后面的流模式相关章节。 1024
streamingPerNodeParallelOperations 数据流处理器的每节点并行操作数。具体可以参照后面的流模式相关章节。 16

客户端和服务端节点
所有的节点默认都是以服务端模式启动的,客户端模式需要显式地开启,然而无论怎么配置,JDBC驱动都会以客户端模式启动一个节点。
跨缓存查询
驱动连接到的缓存会被视为默认的模式,要跨越多个缓存进行查询,可以参照3.6.缓存查询章节。
关联和并置
就像3.6.缓存查询章节描述的那样,通过IgniteCacheAPI,如果关联对象是以并置模式存储的话,在分区缓存上的关联是可以正常执行的。细节可以参照3.11.关系并置章节。
复制和分区缓存
复制缓存上的查询会直接在一个节点上执行,而在分区缓存上的查询是分布在所有缓存节点上的。

4.8.2.流模式

使用JDBC驱动,可以以流模式(批处理模式)将数据注入Ignite集群。这时驱动会在内部实例化IgniteDataStreamer然后将数据传给它。要激活这个模式,可以在JDBC连接串中增加streaming参数并且设置为true

  1. // Register JDBC driver.
  2. Class.forName("org.apache.ignite.IgniteJdbcDriver");
  3. // Opening connection in the streaming mode.
  4. Connection conn = DriverManager.getConnection("jdbc:ignite:cfg://streaming=true@file:///etc/config/ignite-jdbc.xml");

目前,流模式只支持INSERT操作,对于想更快地将数据预加载进缓存的场景非常有用。JDBC驱动定义了多个连接参数来影响流模式的行为,这些参数已经在上述的参数表中列出。
这些参数几乎覆盖了IgniteDataStreamer的所有常规配置,这样就可以根据需要更好地调整流处理器。关于如何配置流处理器可以参考流处理器的相关文档来了解更多的信息。

基于时间的刷新
默认情况下,当要么连接关闭,要么达到了streamingPerNodeBufferSize,数据才会被刷新,如果希望按照时间的方式来刷新,那么可以调整streamingFlushFrequency参数。

  1. // Register JDBC driver.
  2. Class.forName("org.apache.ignite.IgniteJdbcDriver");
  3. // Opening a connection in the streaming mode and time based flushing set.
  4. Connection conn = DriverManager.getConnection("jdbc:ignite:cfg://streaming=true@streamingFlushFrequency=1000@file:///etc/config/ignite-jdbc.xml");
  5. PreparedStatement stmt = conn.prepareStatement(
  6. "INSERT INTO Person(_key, name, age) VALUES(CAST(? as BIGINT), ?, ?)");
  7. // Adding the data.
  8. for (int i = 1; i < 100000; i++) {
  9. // Inserting a Person object with a Long key.
  10. stmt.setInt(1, i);
  11. stmt.setString(2, "John Smith");
  12. stmt.setInt(3, 25);
  13. stmt.execute();
  14. }
  15. conn.close();
  16. // Beyond this point, all data is guaranteed to be flushed into the cache.

4.8.3.示例

Ignite JDBC驱动会自动地只获取缓存中存储的对象中实际需要的那些字段,比如,有一个像下面这样的Person对象。

  1. public class Person {
  2. @QuerySqlField
  3. private String name;
  4. @QuerySqlField
  5. private int age;
  6. // Getters and setters.
  7. ...
  8. }

如果在缓存中有这些类的实例,可以通过标准JDBC API
查询单独的字段(name,age或者两个),比如:

  1. // Register JDBC driver.
  2. Class.forName("org.apache.ignite.IgniteJdbcDriver");
  3. // Open JDBC connection (cache name is not specified, which means that we use default cache).
  4. Connection conn = DriverManager.getConnection("jdbc:ignite:cfg://file:///etc/config/ignite-jdbc.xml");
  5. // Query names of all people.
  6. ResultSet rs = conn.createStatement().executeQuery("select name from Person");
  7. while (rs.next()) {
  8. String name = rs.getString(1);
  9. ...
  10. }
  11. // Query people with specific age using prepared statement.
  12. PreparedStatement stmt = conn.prepareStatement("select name, age from Person where age = ?");
  13. stmt.setInt(1, 30);
  14. ResultSet rs = stmt.executeQuery();
  15. while (rs.next()) {
  16. String name = rs.getString("name");
  17. int age = rs.getInt("age");
  18. ...
  19. }

此外,可以使用DML语句对数据进行修改。
INSERT

  1. // Insert a Person with a Long key.
  2. PreparedStatement stmt = conn.prepareStatement("INSERT INTO Person(_key, name, age) VALUES(CAST(? as BIGINT), ?, ?)");
  3. stmt.setInt(1, 1);
  4. stmt.setString(2, "John Smith");
  5. stmt.setInt(3, 25);
  6. stmt.execute();

MERGE

  1. // Merge a Person with a Long key.
  2. PreparedStatement stmt = conn.prepareStatement("MERGE INTO Person(_key, name, age) VALUES(CAST(? as BIGINT), ?, ?)");
  3. stmt.setInt(1, 1);
  4. stmt.setString(2, "John Smith");
  5. stmt.setInt(3, 25);
  6. stmt.executeUpdate();

UPDATE

  1. // Update a Person.
  2. conn.createStatement().
  3. executeUpdate("UPDATE Person SET age = age + 1 WHERE age = 25");

DELETE

  1. conn.createStatement().execute("DELETE FROM Person WHERE age = 25");

ignite-jdbc.xml的最小版本大概像下面这样:

  1. <beans xmlns="http://www.springframework.org/schema/beans"
  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="
  4. http://www.springframework.org/schema/beans
  5. http://www.springframework.org/schema/beans/spring-beans.xsd">
  6. <bean id="ignite.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
  7. <property name="clientMode" value="true"/>
  8. <property name="peerClassLoadingEnabled" value="true"/>
  9. <!-- Configure TCP discovery SPI to provide list of initial nodes. -->
  10. <property name="discoverySpi">
  11. <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
  12. <property name="ipFinder">
  13. <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder"/>
  14. </property>
  15. </bean>
  16. </property>
  17. </bean>
  18. </beans>

4.8.4.后向兼容

对于之前版本的Ignite(1.4之前),JDBC连接的URL有如下的格式:

  1. jdbc:ignite://<hostname>:<port>/<cache_name>

细节可以参照文档

4.9.空间支持

4.9.1.摘要

Ignite除了支持标准ANSI-99标准的SQL查询,支持基本数据类型或者特定/自定义对象类型之外,还可以查询和索引几何数据类型,比如点、线以及包括这些几何形状空间关系的多边形。
空间信息的查询功能,以及对应的可用的函数和操作符,是在SQL的简单特性规范中定义的,Ignite使用的JTS Topology Suite完全实现了这个规范,它和H2一起,以分布式和容错的方式构建了一个独特的空间组件。

4.9.2.引入Ignite空间库

Ignite的空间库(ignite-geospatial)依赖于JTS,它是LGPL许可证,不同于Apache的许可证,因此ignite-geospatial并没有包含在Ignite的发布版中。
因为这个原因,ignite-geospatial的二进制库版本位于如下的Maven仓库中:

  1. <repositories>
  2. <repository>
  3. <id>GridGain External Repository</id>
  4. <url>http://www.gridgainsystems.com/nexus/content/repositories/external</url>
  5. </repository>
  6. </repositories>

在pom.xml中添加这个仓库以及如下的Maven依赖之后,就可以将该空间库引入应用中了。

  1. <dependency>
  2. <groupId>org.apache.ignite</groupId>
  3. <artifactId>ignite-geospatial</artifactId>
  4. <version>${ignite.version}</version>
  5. </dependency>

另外,也可以下载Ignite的源代码自己构建这个库。

4.9.3.执行空间查询

这个空间模块只对com.vividsolutions.jts类型的对象有用。
要配置索引以及/或者几何类型的可查询字段,可以使用和已有的非几何类型同样的方法,首先,可以使用org.apache.ignite.cache.QueryEntity定义索引,他对于基于Spring的XML配置文件非常方便,第二,通过@QuerySqlField注解来声明索引也可以达到同样的效果,他在内部会转化为QueryEntities
QuerySqlField:

  1. /**
  2. * Map point with indexed coordinates.
  3. */
  4. private static class MapPoint {
  5. /** Coordinates. */
  6. @QuerySqlField(index = true)
  7. private Geometry coords;
  8. /**
  9. * @param coords Coordinates.
  10. */
  11. private MapPoint(Geometry coords) {
  12. this.coords = coords;
  13. }
  14. }

QueryEntity:

  1. <bean class="org.apache.ignite.configuration.CacheConfiguration">
  2. <property name="name" value="mycache"/>
  3. <!-- Configure query entities -->
  4. <property name="queryEntities">
  5. <list>
  6. <bean class="org.apache.ignite.cache.QueryEntity">
  7. <property name="keyType" value="java.lang.Integer"/>
  8. <property name="valueType" value="org.apache.ignite.examples.MapPoint"/>
  9. <property name="fields">
  10. <map>
  11. <entry key="coords" value="com.vividsolutions.jts.geom.Geometry"/>
  12. </map>
  13. </property>
  14. <property name="indexes">
  15. <list>
  16. <bean class="org.apache.ignite.cache.QueryIndex">
  17. <constructor-arg value="coords"/>
  18. </bean>
  19. </list>
  20. </property>
  21. </bean>
  22. </list>
  23. </property>
  24. </bean>

使用上述方法定义了几何类型字段之后,就可以使用存储于这些字段中值进行查询了。

  1. // Query to find points that fit into a polygon.
  2. SqlQuery<Integer, MapPoint> query = new SqlQuery<>(MapPoint.class, "coords && ?");
  3. // Defining the polygon's boundaries.
  4. query.setArgs("POLYGON((0 0, 0 99, 400 500, 300 0, 0 0))");
  5. // Executing the query.
  6. Collection<Cache.Entry<Integer, MapPoint>> entries = cache.query(query).getAll();
  7. // Printing number of points that fit into the area defined by the polygon.
  8. System.out.println("Fetched points [" + entries.size() + ']');

完整示例
Ignite中用于演示空间查询的可以立即执行的完整示例,可以在这里找到。

4.10.性能和调试

4.10.1.使用EXPLAIN语句

为了读取执行计划以及提高查询性能的目的,Ignite支持EXPLAIN ...语法,注意一个计划游标会包含多行:最后一行是汇总节点的查询,其他是映射节点的。

  1. SqlFieldsQuery sql = new SqlFieldsQuery(
  2. "explain select name from Person where age = ?").setArgs(26);
  3. System.out.println(cache.query(sql).getAll());

执行计划本身是由H2生成的,这里有详细描述。

4.10.2.使用H2调试控制台

当用Ignite进行开发时,有时对于检查表和索引是否正确或者运行在嵌入节点内部的H2数据库中的本地查询是非常有用的,为此Ignite提供了启动H2控制台的功能。要启用该功能,在启动节点时要将IGNITE_H2_DEBUG_CONSOLE系统属性或者环境变量设置为true。然后就可以在浏览器中打开控制台,可能需要点击控制台中的刷新按钮,因为有可能控制台在数据库对象初始化之前打开。

4.10.3.SQL性能和可用性考量

当执行SQL查询时有一些常见的陷阱需要注意:

  1. 如果查询使用了操作符OR那么他可能不是以期望的方式使用索引。比如对于查询:select name from Person where sex='M' and (age = 20 or age = 30),会使用sex字段上的索引而不是age上的索引,虽然后者选择性更强。要解决这个问题需要用UNION ALL重写这个查询(注意没有ALL的UNION会返回去重的行,这会改变查询的语意而且引入了额外的性能开销),比如:select name from Person where sex='M' and age = 20 UNION ALL select name from Person where sex='M' and age = 30
  2. 如果查询使用了操作符IN,那么会有两个问题:首先无法提供可变参数列表,这意味着需要在查询中指定明确的列表,比如where id in (?, ?, ?),但是不能写where id in ?然后传入一个数组或者集合。第二,查询无法使用索引,要解决这两个问题需要像这样重写查询:select p.name from Person p join table(id bigint = ?) i on p.id = i.id,这里可以提供一个任意长度的对象数组(Object[])作为参数,然后会在字段id上使用索引。注意基本类型数组(比如int[],long[]等)无法使用这个语法,但是可以使用基本类型的包装器。

示例:

  1. new SqlFieldsQuery(
  2. "select * from Person p join table(id bigint = ?) i on p.id = i.id").setArgs(new Object[]{ new Integer[] {2, 3, 4} }))

他会被转换为下面的SQL:

  1. select * from "cache-name".Person p join table(id bigint = (2,3,4)) i on p.id = i.id

4.10.4.查询并行化

SQL查询在每个涉及的节点上,默认是以单线程模式执行的,这种方式对于使用索引返回一个小的结果集的查询是一种优化,比如:

  1. select * from Person where p.id = ?

某些查询以多线程模式执行会更好,这个和带有表扫描以及聚合的查询有关,这在OLAP的场景中比较常见,比如:

  1. select SUM(salary) from Person

通过CacheConfiguration.queryParallelism属性可以控制查询的并行化,这个参数定义了在单一节点中执行查询时使用的线程数。
如果查询包含JOIN,那么所有相关的缓存都应该有相同的并行化配置。

注意
当前,这个属性影响特定缓存上的所有查询,可以加速很重的OLAP查询,但是会减慢其他的简单查询,这个行为在未来的版本中会改进。

4.10.5.高级DML优化

使用UPDATEDELETE语句时,需要执行一个SELECT查询来获取之后要处理的缓存条目集。在某些情况下,与直接将DML语句转为特定的缓存操作相比,这样可以避免导致显著的性能问题。
总结一下4.4.分布式DML章节的内容,之所以UPDATEDELETE会自动执行一个SELECT查询,有如下的原因:

  1. UPDATE或者DELETE语句的WHERE子句会使用复杂的过滤。这在使用复杂而高级的条目过滤时就会发生,这时DML引擎需要做额外的工作来准备要被DML语句更新的条目列表;
  2. UPDATE语句包括表达式。即使WHERE子句比较简单并且通过使用_key或者_val直接指向要修改的缓存条目,这个表达式的执行结果仍然可能产生新的字段值,这也是为什么DML引擎需要执行一个SELECT来评估表达式的执行结果;
  3. UPDATE语句修改一个缓存条目的特定字段。DML引擎首先需要获取当前的缓存条目,再修改然后将其放回缓存。

更快地执行DML
要更快地执行DML操作,需要遵守如下的必要条件:

  1. DML操作不触发SELECT查询执行;
  2. 操作只调整单个缓存条目;

如果遵守如下的规则,就能满足上述的条件:

  1. 只使用_key_val关键字来过滤缓存条目;
  2. 在DML语句中只显式使用这些参数,不访问缓存条目的字段或表达式;
  3. 如果执行一个UPDATE语句,然后更新整个缓存条目(_val),而不是特定的字段。

可以看下面的示例:

  1. cache.query(new SqlFieldsQuery("UPDATE Person SET _val = ?3" +
  2. " WHERE _key = ?1 and _val = ?2").setArgs(7, 1, 2));

UPDATE语句会进行如下的操作:

作为结果,DML引擎大概会像下面这样执行缓存操作:

  1. cache.replace(7, 1, 2);
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注