@liyuj
2017-05-31T22:18:24.000000Z
字数 34765
阅读 6281
Apache-Ignite-2.0.0-中文开发手册
内存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以及其他的。
Ignite支持任意的SQL查询,没有任何限制。SQL语法是ANSI-99兼容的,也就意味着作为SQL查询的一部分,规范定义的任何SQL函数、聚合、分组以及关联,都是可以使用的。
此外,查询是完全分布式的。SQL引擎的功能不仅仅是将查询映射到特定的节点然后将结果汇总为最终的结果集,它还可以将存储在不同缓存甚至是不同节点上的数据进行关联。此外,引擎是以容错的方式保证,不会因为新节点加入集群或者旧节点离开而获得不完整或者错误的结果。
Ignite的SQL网格组件是与H2数据库紧紧绑定在一起的,简而言之,H2是一个Java写的,遵循一组开源许可证,基于内存和磁盘的数据库。
当ignite-indexing
模块加入节点的类路径之后,一个嵌入式的H2数据库实例就会作为Ignite节点进程的一部分被启动。如果节点是在终端中通过ignite.sh{bat}
脚本启动的,那么需要将{apache_ignite}\libs\optional\ignite-indexing
目录拷贝到{apache_ignite}\libs\
,如果使用的是maven,那么需要将如下的依赖加入pom.xml
文件:
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-indexing</artifactId>
<version>${ignite.version}</version>
</dependency>
Ignite借用了H2的SQL查询解析器以及优化器还有执行计划器。最后,届时H2会在一个特定的节点执行本地化的查询(一个分布式查询会被映射到节点或者查询是以本地
模式执行的),然后会将本地的结果集传递给分布式SQL引擎用于后续处理。
然而,数据和索引,通常是存储于Ignite数据网格端的,而Ignite以分布式以及容错的方式执行SQL查询,这个是H2不支持的。
Ignite SQL网格执行查询有两种方式:
首先,如果查询在一个部署有REPLICATED
模式缓存的节点上执行,那么Ignite会假定所有的数据都是本地化的,然后将其直接传递给H2数据库引擎执行一个简单的本地化SQL查询,对于LOCAL
模式的缓存,也是同样的执行流程。
第二,如果查询执行于PARTITIONED
模式缓存,那么执行流程如下:
跨缓存查询的执行流程
跨缓存或者关联查询的执行流程与上面描述的分区
缓存查询执行流程没什么不同,后面文档还会提到。
处理带有ORDER BY以及GROUP BY的结果集
带有ORDER BY语句的SQL查询不需要将所有结果集都加载到查询发起(汇总)节点来完成排序。而是查询映射的每个节点都会对自己那部分数据进行排序然后汇总节点以流的方式进行合并。
对于有序的GROUP BY查询也是同样的优化方式,不需要在将其返回给应用之前将所有数据加载到汇总节点用于分组。在Ignite中,来自单独节点的部分结果集可以被逐步地流化、合并、聚合以及返回给应用。
在Java API层,通常有两种类型的SQL查询,分别为SqlQuery
和SqlFieldsQuery
。
替代APIs
Ignite内存SQL网格并不绑定到Java API,可以从.NET, C++通过 ODBC或者JDBC驱动连接到Ignite集群然后执行SQL查询。
SqlQuery
SqlQuery
适用于查询执行完毕后需要获得存储于缓存(键和值)中的整个对象的场景,然后返回最终的结果集,下面的代码片段显示了在实践中如何实现:
IgniteCache<Long, Person> cache = ignite.cache("personCache");
SqlQuery sql = new SqlQuery(Person.class, "salary > ?");
// Find all persons earning more than 1,000.
try (QueryCursor<Entry<Long, Person>> cursor = cache.query(sql.setArgs(1000))) {
for (Entry<Long, Person> e : cursor)
System.out.println(e.getValue().toString());
}
SqlFieldsQuery
不需要查询整个对象,只需要指定几个特定的字段即可,这样可以最小化网络和序列化的开销。为此,Ignite实现了一个字段查询
的概念。SqlFieldsQuery
接受一个常规的ANSI-99 SQL查询作为它的构造器参数,然后像下面的示例那样立即执行:
IgniteCache<Long, Person> cache = ignite.cache("personCache");
// Execute query to get names of all employees.
SqlFieldsQuery sql = new SqlFieldsQuery(
"select concat(firstName, ' ', lastName) from Person");
// Iterate over the result set.
try (QueryCursor<List<?>> cursor = cache.query(sql) {
for (List<?> row : cursor)
System.out.println("personName=" + row.get(0));
}
可查询字段定义
在SqlQuery
和SqlFieldsQuery
中的指定字段可以被访问之前,他们需要在POJO层面加上注解,或者在QueryEntity
中进行定义,以便SQL引擎可以感知到它们,后续章节还会详述。
访问条目的键和值
在SQL查询中使用_key
和_val
关键字,可以指向条目的整个键和值,而不用写每个字段,如果要在SQL查询执行的结果中返回键或值,也可以使用这两个关键字。
另外,如果键和值是基本类型(int, String, Date等),那么它会被自动地添加到查询的结果集中,比如:SELECT * FROM ...
。
作为单个SqlQuery
和SqlFieldsQuery
查询的一部分,查询的数据可以来自多个缓存。这时,缓存名会扮演类似传统RDBMS中SQL查询的模式名的角色。缓存的名字,用于创建IgniteCache
的实例,如果用于查询的话,会作为默认的模式名并且不需要显式地指定。其余的存储于不同缓存中的对象,也会被查询,但是需要加上它的缓存名(额外的模式名)作为前缀。
// In this example, suppose Person objects are stored in a
// cache named 'personCache' and Organization objects
// are stored in a cache named 'orgCache'.
IgniteCache<Long, Person> personCache = ignite.cache("personCache");
// Select with join between Person and Organization to
// get the names of all the employees of a specific organization.
SqlFieldsQuery sql = new SqlFieldsQuery(
"select Person.name "
+ "from Person as p, \"orgCache\".Organization as org where "
+ "p.orgId = org.id "
+ "and org.name = ?");
// Execute the query and obtain the query result cursor.
try (QueryCursor<List<?>> cursor = personCache.query(sql.setArgs("Ignite"))) {
for (List<?> row : cursor)
System.out.println("Person name=" + row.get(0));
}
上面的示例中,会从personCache
创建一个SqlFieldsQuery
的实例,之后personCache
会作为默认的模式名,这就是Person
对象没有通过显式指定的模式名(from Person as p)就能访问的原因。而Organization
对象,因为它存储于一个单独的名为orgCache
的缓存中,所以在该查询中这个缓存的名字作为模式名必须显式地指定("orgCache".Organization as org)。
修改缓存名
如果希望使用不同于缓存名的模式名,可以通过调用CacheConfiguration.setSqlSchema(...)
方法解决。
Ignite支持并置和非并置的分布式SQL关联,此外,如果数据位于不同的缓存,Ignite可以进行跨缓存的关联。
IgniteCache<Long, Person> cache = ignite.cache("personCache");
// SQL join on Person and Organization.
SqlQuery sql = new SqlQuery(Person.class,
"from Person as p, \"orgCache\".Organization as org"
+ "where p.orgId = org.id "
+ "and lower(org.name) = lower(?)");
// Find all persons working for Ignite organization.
try (QueryCursor<Entry<Long, Person>> cursor = cache.query(sql.setArgs("Ignite"))) {
for (Entry<Long, Person> e : cursor)
System.out.println(e.getValue().toString());
}
分区
和复制
模式缓存之间的关联也可以无限制地进行。
然而,如果在至少两个分区
模式的数据集之间进行关联,那么一定要确保要么关联的键是并置
的,要么为查询开启了非并置关联参数,两种类型的分布式关联模式下面会详述。
分布式并置关联
默认情况下,如果一个SQL关联需要跨越多个Ignite缓存,那么所有的缓存都需要是并置的,否则,查询完成后会得到一个不完整的结果集,这是因为在关联阶段一个节点的可用数据只是本地的,如图1所示,首先,一个SQL查询会被发送到待关联数据所在的节点(Q),然后查询在每个节点的本地数据上立即执行(E(Q)),最后,所有的执行结果都会在客户端进行聚合(R)。
分布式非并置关联
虽然关系并置是一个强大的概念,即一旦配置了应用的业务实体(缓存),就可以以最优的方式执行跨缓存的关联,并且返回一个完整且一致的结果集。但还有一种可能就是,无法并置所有的数据,这时,就可能无法执行满足需求的所有SQL查询了。
在实践中不要过度使用基于非并置的分布式关联的方式,因为这种关联方式的性能差于基于关系并置的关联,因为要完成这个查询,要有更多的网络开销和节点间的数据移动。
当通过SqlQuery.setDistributedJoins(boolean)参数为一个SQL查询启用了非并置的分布式关联之后,查询映射的节点就会从远程节点通过发送广播或者单播请求的方式获取缺失的数据(本地不存在的数据),正如图2所示,有一个潜在的数据移动步骤(D(Q))。潜在的单播请求只会在关联在主键(缓存键)或者关系键上完成之后才会发送,因为执行关联的节点知道缺失数据的位置,其他所有的情况都会发送广播请求。
不管是广播还是单播请求,都是由一个节点发送到另一个节点来获取缺失的数据,是按照顺序执行的。SQL引擎会将所有的请求组成若干批量,这个批量的大小是由
SqlQuery.setPageSize(int)
参数管理的。
下面的代码片段是从Ignite的发行版的CacheQueryExample中提取的:
IgniteCache<AffinityKey<Long>, Person> cache = ignite.cache("personCache");
// SQL clause query with join over non-collocated data.
String joinSql =
"from Person, \"orgCache\".Organization as org " +
"where Person.orgId = org.id " +
"and lower(org.name) = lower(?)";
SqlQuery qry = new SqlQuery<AffinityKey<Long>, Person>(Person.class, joinSql).setArgs("ApacheIgnite");
// Enable distributed joins for the query.
qry.setDistributedJoins(true);
// Execute the query to find out employees for specified organization.
System.out.println("Following people are 'ApacheIgnite' employees (distributed join): ", cache.query(qry).getAll());
要了解详细信息,可以参照非并置的分布式关联。
查询复制缓存
如果只在复制缓存所在的数据上执行SQL查询,那么可以设置SqlQuery.setReplicatedOnly(...)
为true
,这个给SQL引擎的特别提示会为查询产生更高效的执行计划。
事务性SQL
目前,SQL查询仅仅支持原子模式,意味着如果有一个事务已经提交了值A而值B正在提交过程中,然后如果有一个并行的SQL查询的话,会看到A而看不到B。
多版本并发控制(MVCC)
一旦Ignite SQL网格使用MVCC进行控制,SQL网格也会支持事务模式。
关于本文描述的分布式关联如何使用的完整示例,会作为Ignite发行版的一部分进行分发,名为CacheQueryExample
,GitHub上也有。
有时,SQL网格中查询的执行会从分布式模式回落至本地模式,在本地模式中,查询会简单地传递至底层的H2引擎,他只会处理本地节点的数据集。
这些场景包括:
复制
缓存的节点上执行,那么Ignite会假定所有的数据都在本地,然后就会隐式地在本地执行一个简单的查询;本地
缓存上执行;SqlQuery.setLocal(true)
或者SqlFieldsQuery.setLocal(true)
为查询显式地开启本地模式;即使查询执行时网络拓扑发生变化(新节点加入集群或者老节点离开集群),前两个场景也会一直提供完整而一致的结果集。
然而,在应用显式开启本地模式的第三个场景中需要注意,原因是如果希望在部分节点的分区
缓存上执行本地查询时网络还发生了变化,那么可能得到结果集的一部分,因为这时会触发一个并行的数据再平衡过程。SQL引擎无法处理这个特殊情况。如果仍然希望在分区
缓存上执行本地查询,那么需要将查询作为affinityRun(...)
或者affinityCall(...)
方法的一部分。
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语句是如何实现的。
通常来说,所有的DML语句会被拆分为两组,一个是往缓存中添加条目(INSERT
和MERGE
),还有就是修改已有的数据(UPDATE
和DELETE
)。
要在Java中执行这些语句需要使用已有的用于SELECT
查询的API - SqlFieldsQuery
API,DML操作使用的API与只读查询是一致的,返回结果也是QueryCursor<List<?>>
。唯一的不同是作为DML语句执行的结果,QueryCursor<List<?>>
是只有一个long
类型的单条目的List<?>
,这个数值表示该DML语句影响的缓存条目的数量。而作为SELECT
语句的结果,QueryCursor<List<?>>
会包含一个从缓存获得的条目列表。
其他的API
DML API不受限于Java,也可以使用ODBC或者JDBC驱动接入Ignite集群,然后执行DML语句。
在Ignite中要进行DML操作,需要使用基于QueryEntity的方式或者使用@QuerySqlField注解来配置所有可查询的字段,比如:
使用@QuerySqlField注解:
public class Person {
/** Field will be accessible from DML statements. */
@QuerySqlField
private final String firstName;
/** Indexed field that will be accessible from DML statements. */
@QuerySqlField (index = true)
private final String lastName;
/** Field will NOT be accessible from DML statements. */
private int age;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
使用QueryEntity:
<bean class="org.apache.ignite.configuration.CacheConfiguration">
<property name="name" value="personCache"/>
<!-- Configure query entities -->
<property name="queryEntities">
<list>
<bean class="org.apache.ignite.cache.QueryEntity">
<!-- Registering key's class. -->
<property name="keyType" value="java.lang.Long"/>
<!-- Registering value's class. -->
<property name="valueType"
value="org.apache.ignite.examples.Person"/>
<!--
Defining fields that will be accessible from DML side
-->
<property name="fields">
<map>
<entry key="firstName" value="java.lang.String"/>
<entry key="lastName" value="java.lang.String"/>
</map>
</property>
<!--
Defining which fields, listed above, will be treated as
indexed fields as well.
-->
<property name="indexes">
<list>
<!-- Single field (aka. column) index -->
<bean class="org.apache.ignite.cache.QueryIndex">
<constructor-arg value="lastName"/>
</bean>
</list>
</property>
</bean>
</list>
</property>
</bean>
除了通过@QuerySqlField加注的或者通过QueryEntity定义的所有字段,还有两个为每个在SQL网格中注册的对象类型预定义的字段_key
和_val
,这几个预定义字段指向缓存中存储的对象的整个键和值,他们可以像下面这样在DML中直接使用:
//Preparing cache configuration.
CacheConfiguration<Long, Person> cacheCfg = new CacheConfiguration<>
("personCache");
//Registering indexed/queryable types.
cacheCfg.setIndexedTypes(Long.class, Person.class);
//Starting the cache.
IgniteCache<Long, Person> cache = ignite.cache(cacheCfg);
// Inserting a new key-value pair referring to prefedined `_key` and `_value`
// fields for Person type.
cache.query(new SqlFieldsQuery("INSERT INTO Person(_key, _val) VALUES(?, ?)")
.setArgs(1L, new Person("John", "Smith")));
如果倾向于处理具体的字段,而不是通过执行查询处理整个对象的值,可以执行下面这样的查询:
IgniteCache<Long, Person> cache = ignite.cache(cacheCfg);
cache.query(new SqlFieldsQuery(
"INSERT INTO Person(_key, firstName, lastName) VALUES(?, ?, ?)").
setArgs(1L, "John", "Smith"));
注意DML引擎会根据firstName
和lastName
重新创建一个Person对象,然后将其注入缓存,但是这些字段是需要通过QueryEntity
或者@QuerySqlField
注解进行定义的,就像上面描述的那样。
自定义键
如果只使用预定义的SQL数据类型作为缓存键,那么就没必要对和DML相关的配置做额外的操作,这些数据类型在GridQueryProcessor#SQL_TYPES
常量中进行定义,列举如下:
预定义SQL数据类型
1.所有的基本类型及其包装器,除了char
和Character
;
2.String
;
3.BigDecimal
;
4.byte[]
;
5.java.util.Date
,java.sql.Date
,java.sql.Timestamp
;
6.java.util.UUID
。
然而,如果决定引入复杂的自定义缓存键,那么在DML语句中要指向这些字段就需要:
QueryEntity
中定义这些字段,与在值对象中配置字段一样;QueryEntitty.setKeyFields(..)
来对键和值进行区分;下面的例子展示了如何实现:
Java:
// Preparing cache configuration.
CacheConfiguration cacheCfg = new CacheConfiguration<>("personCache");
// Creating the query entity.
QueryEntity entity = new QueryEntity("CustomKey", "Person");
// Listing all the queryable fields.
LinkedHashMap<String, String> flds = new LinkedHashMap<>();
flds.put("intKeyField", Integer.class.getName());
flds.put("strKeyField", String.class.getName());
flds.put("firstName", String.class.getName());
flds.put("lastName", String.class.getName());
entity.setFields(flds);
// Listing a subset of the fields that belong to the key.
Set<String> keyFlds = new HashSet<>();
keyFlds.add("intKeyField");
keyFlds.add("strKeyField");
entity.setKeyFields(keyFlds);
// End of new settings, nothing else here is DML related
entity.setIndexes(Collections.<QueryIndex>emptyList());
cacheCfg.setQueryEntities(Collections.singletonList(entity));
ignite.createCache(cacheCfg);
XML:
<bean class="org.apache.ignite.configuration.CacheConfiguration">
<property name="name" value="personCache"/>
<!-- Configure query entities -->
<property name="queryEntities">
<list>
<bean class="org.apache.ignite.cache.QueryEntity">
<!-- Registering key's class. -->
<property name="keyType" value="CustomKey"/>
<!-- Registering value's class. -->
<property name="valueType"
value="org.apache.ignite.examples.Person"/>
<!--
Defining all the fields that will be accessible from DML.
-->
<property name="fields">
<map>
<entry key="firstName" value="java.lang.String"/>
<entry key="lastName" value="java.lang.String"/>
<entry key="intKeyField" value="java.lang.Integer"/>
<entry key="strKeyField" value="java.lang.String"/>
</map>
</property>
<!-- Defining the subset of key's fields -->
<property name="keyFields">
<set>
<value>intKeyField<value/>
<value>strKeyField<value/>
</set>
</property>
</bean>
</list>
</property>
</bean>
MERGE
MERGE
是一个非常简单的操作,因为它会被翻译成cache.put(...)
或者cache.putAll(...)
,具体是哪一个,取决于MERGE
语句涉及的要插入或者要更新的记录的数量。
下面的示例显示如何通过MERGE
命令来更新数据集。一个是提供了条目列表,一个是通过执行子查询注入一个结果集。
MERGE(条目列表):
cache.query(new SqlFieldsQuery("MERGE INTO Person(_key, firstName, lastName)" + "values (1, 'John', 'Smith'), (5, 'Mary', 'Jones')"));
MERGE(子查询):
cache.query(new SqlFieldsQuery("MERGE INTO someCache.Person(_key, firstName, lastName) (SELECT _key + 1000, firstName, lastName " +
"FROM anotherCache.Person WHERE _key > ? AND _key < ?)").setArgs(100, 200);
INSERT
MERGE
和INSERT
命令的不同在于,后者添加的条目必须是缓存中不存在的。
如果要把一个键值对插入缓存,那么最后,INSERT
语句会被转换为cache.putIfAbsent(...)
操作,否则,如果插入的是多个键值对,那么DML引擎会为每个对创建一个EntryProcessor
,然后使用cache.invokeAll(...)
将数据注入缓存。
下面的示例显示如何通过INSERT
命令插入一个数据集,一个是提供了条目列表,一个是通过执行子查询注入一个结果集。
INSERT(条目列表):
cache.query(new SqlFieldsQuery("INSERT INTO Person(_key, firstName, " +
"lastName) values (1, 'John', 'Smith'), (5, 'Mary', 'Jones')"));
INSERT(子查询):
cache.query(new SqlFieldsQuery("INSERT INTO someCache.Person(_key, firstName, lastName) (SELECT _key + 1000, firstName, secondName " +
"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
语句。
cache.query(new SqlFieldsQuery("UPDATE Person set lastName = ? " +
"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
语句。
cache.query(new SqlFieldsQuery("DELETE FROM Person " +
"WHERE _key >= ?").setArgs(2L));
流模式
使用Ignite的JDBC驱动,会通过流模式来获得更快的数据预加载。
如果一个DML语句插入/更新指向_val
字段的整个值的同时,还试图修改属于_val
的某一个字段时,那么,变更的顺序如下:
_val
被插入/更新;不管DML语句事实上如何定义,这个顺序是不会改变的。比如下面的语句执行完毕后,Person的最终值会是Mike Smith
,尽管在查询中_val
位于firstName
后面。
cache.query(new SqlFieldsQuery("INSERT INTO Person(_key, firstName, _val)" +
" VALUES(?, ?, ?)").setArgs(1L, "Mike", new Person("John", "Smith")));
这与下面的查询的执行类似,这里_val
在前面:
cache.query(new SqlFieldsQuery("INSERT INTO Person(_key, _val, firstName)" +
" VALUES(?, ?, ?)").setArgs(1L, new Person("John", "Smith"), "Mike"));
对于_val
及其字段变更顺序的问题,INSERT
、UPDATE
和MERGE
语句都是一样的。
如上所述,UPDATE
和DELETE
语句在内部会生成SELECT
查询,目的是将查询执行的结果集作为要更新的缓存条目的集合。这个集合中的键是不会被锁定的,因此有一种可能就是在并发的情况下,属于某个键的值会被其他的查询修改。DML引擎已经实现了一种技术,即首先避免锁定键,然后保证在DML语句执行更新时值是最新的。
总体而言,引擎会并发地检测要更新的缓存条目的子集,然后重新执行SELECT
语句来限制要修改的键的范围。
比如下面的要执行的UPDATE
语句:
// Adding the cache entry.
cache.put(1, new Person("John", "Smith");
// Updating the entry.
cache.query(new SqlFieldsQuery("UPDATE Person set firstName = ? " +
"WHERE lastName = ?").setArgs("Mike", "Smith"));
在firstName
和lastName
更新之前,DML引擎会生成SELECT
查询来获得符合UPDATE
语句的WHERE
条件的缓存条目,语句如下:
SELECT _key, _value, "Mike" from Person WHERE lastName = "Smith"
之后通过SELECT
获得的条目会被并发地更新:
cache.put(1, new Person("Sarah", "Connor"))
DML引擎在UPDATE
语句执行的更新阶段会检测到键为1
的缓存条目要被修改,之后会暂停更新并且重新执行一个SELECT
查询的修订版本来获得最新的条目值:
SELECT _key, _value, "Mike" from Person WHERE secondName = "Smith"
AND _key IN (SELECT * FROM TABLE(KEY long = [ 1 ]))
这个查询只会为过时的键执行,本例中只有一个键1
。
这个过程会一直重复,直到DML引擎确信在更新阶段所有的条目都已经更新到最新版。尝试次数的最大值是4
,目前并没有配置参数来改变这个值。
DML引擎不会为并发删除的条目重复执行
SELECT
语句,重复执行的查询只针对还在缓存中的条目。
WHERE条件中的子查询
INSERT
和MERGE
语句中的子查询和UPDATE
和DELETE
操作自动生成的SELECT
查询一样,如有必要都会被分布化然后执行,要么是并置,要么是非并置的模式。
然而,如果WHERE
语句里面有一个子查询,那么他是不会以非并置的分布式模式执行的,子查询始终都会以并置的模式在本地节点上执行。
比如,有这样一个查询:
DELETE FROM Person WHERE _key IN
(SELECT personId FROM "salary".Salary s WHERE s.amount > 2000)
然后DML引擎会生成SELECT
查询来获得要删除的条目列表,这个查询会在整个集群中分布化并且执行,如下所示:
SELECT _key, _val FROM Person WHERE _key IN
(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
。
一个方法就是执行UPDATE
或DELETE
语句自动生成的SELECT
语句或者DML语句使用的INSERT
或MERGE
语句的执行计划,这样会提供一个要执行的DML操作所使用的索引情况。
Ignite在源代码中包含了一个可以立即执行的CacheQueryDmlExample
,这个示例演示了上面提到的所有DML操作的用法。
Ignite支持在运行时使用数据定义语言(DDL)语句来创建和删除SQL索引,原生的Ignite SQL API以及JDBC和ODBC驱动都可以用于SQL模式的修改。
语法:
CREATE [SPATIAL] INDEX [IF NOT EXISTS] indexName ON tableName (indexColumn, ...)
,其中
indexColumn := columnName [ASC|DESC]tableName
是存储在分布式缓存中的类型名。
下面是创建一个简单有序索引的示例:
CREATE INDEX idx_person_name ON Person (name)
要创建一个组合索引,命令如下所示:
CREATE INDEX idx_person_name_birth_date ON Person (name ASC, birth_date DESC)
添加SPATIAL
关键字可以定义空间索引:
CREATE SPATIAL INDEX idx_person_address ON Person (address)
语法:
DROP INDEX [IF EXISTS] indexName
DROP INDEX idx_person_name
DDL命令可以通过SqlFieldsQuery
类执行,如下所示:
IgniteCache<PersonKey, Person> cache = ignite.cache("Person");
SqlFieldsQuery query = new SqlFieldsQuery(
"CREATE INDEX idx_person_name ON Person (name)");
cache.query(query).getAll();
下面的示例演示了如何通过JDBC驱动创建索引:
Class.forName("org.apache.ignite.IgniteJdbcDriver");
Connection conn = DriverManager.getConnection(
"jdbc:ignite:cfg://file:///etc/config/ignite-jdbc.xml");
try (Statement stmt = conn.createStatement()) {
stmt.execute("CREATE INDEX idx_person_name ON Person (name)");
}
目前,Ignite可以通过基于注解或者基于QueryEntity的方式定义模式,每个模式都会绑定到一个Ignite缓存,缓存的名字默认会作为SQL查询的模式名。
数据定义语言支持
Ignite的下个版本计划提供对DDL语句的支持,有了这个特性之后,就可以通过标准的SQL命令,比如CREATE/ALTER/DROP TABLE或者CREATE/DROP INDEX来定义模式、缓存、索引,以及管理它们。
Ignite支持高级的索引功能,可以定义包括各种参数的单字段(也可以叫做列)或者分组索引,这些参数可以管理索引位于Java堆或者堆外空间等等。
Ignite中以分布式方式保持的索引和缓存数据集一样,每一个节点都保存数据的一个特定子集,还会保持和管理与这个数据对应的索引。
本章节会描述如何像查询字段那样,使用两种方法来定义和管理索引,以及如何在Ignite支持的特定索引实现之间进行切换。
在运行时,通过广泛使用的DDL命令来创建和修改索引,相关内容可以参照分布式DDL
相关章节。
索引,和可查询的字段一样,是可以通过编程的方式用@QuerySqlField
进行配置的。
如下所示,期望的字段已经加注了该注解。
Java:
public class Person implements Serializable {
/** Indexed field. Will be visible for SQL engine. */
@QuerySqlField (index = true)
private long id;
/** Queryable field. Will be visible for SQL engine. */
@QuerySqlField
private String name;
/** Will NOT be visible for SQL engine. */
private int age;
/**
* Indexed field sorted in descending order.
* Will be visible for SQL engine.
*/
@QuerySqlField(index = true, descending = true)
private float salary;
}
Scala:
case class Person (
/** Indexed field. Will be visible for SQL engine. */
@(QuerySqlField @field)(index = true) id: Long,
/** Queryable field. Will be visible for SQL engine. */
@(QuerySqlField @field) name: String,
/** Will NOT be visisble for SQL engine. */
age: Int
/**
* Indexed field sorted in descending order.
* Will be visible for SQL engine.
*/
@(QuerySqlField @field)(index = true, descending = true) salary: Float
) extends Serializable {
...
}
id
和salary
都是索引列,id
字段升序排列(默认),而salary
降序排列。
如果不希望索引一个字段,但是仍然想在SQL查询中使用它,那么在加注解时可以忽略index = true
参数,这样的字段称为可查询字段,举例来说,上面的name
就被定义为可查询字段。
最后,age
既不是可查询字段也不是索引字段,在Ignite中,从SQL查询的角度看就是不可见的。
Scala注解
在Scala类中,@QuerySqlField
注解必须和@Field
注解一起使用,这样的话这个字段对于Ignite才是可见的,就像这样的:@(QuerySqlField @field)
。
作为替代,也可以使用ignite-scalar
模块的@ScalarCacheQuerySqlField
注解,他不过是@Field
注解的别名。
注册索引类型
定义了索引字段和可查询字段之后,就需要和他们所属的对象类型一起,在SQL引擎中注册。
要告诉Ignite哪些类型应该被索引,需要通过CacheConfiguration.setIndexedTypes
方法传入键-值对,如下所示:
/ Preparing configuration.
CacheConfiguration<Long, Person> ccfg = new CacheConfiguration<>();
// Registering indexed type.
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:
public class Person implements Serializable {
/** Indexed in a group index with "salary". */
@QuerySqlField(orderedGroups={@QuerySqlField.Group(
name = "age_salary_idx", order = 0, descending = true)})
private int age;
/** Indexed separately and in a group index with "age". */
@QuerySqlField(index = true, orderedGroups={@QuerySqlField.Group(
name = "age_salary_idx", order = 3)})
private double salary;
}
注意,将
@QuerySqlField.Group
放在@QuerySqlField(orderedGroups={...})
外面是无效的。
索引和字段也可以通过org.apache.ignite.cache.QueryEntity
进行配置,它便于利用Spring进行基于XML的配置。
在上面基于注解的配置涉及的所有概念,对于基于QueryEntity的方式也都有效,深入地说,通过@QuerySqlField
配置的字段的类型然后通过CacheConfiguration.setIndexedTypes
注册过的,在内部也会被转换为查询实体。
下面的示例显示的是如何像可查询字段那样定义一个单一字段和分组索引。
<bean class="org.apache.ignite.configuration.CacheConfiguration">
<property name="name" value="mycache"/>
<!-- Configure query entities -->
<property name="queryEntities">
<list>
<bean class="org.apache.ignite.cache.QueryEntity">
<!-- Setting indexed type's key class -->
<property name="keyType" value="java.lang.Long"/>
<!-- Setting indexed type's value class -->
<property name="valueType"
value="org.apache.ignite.examples.Person"/>
<!-- Defining fields that will be either indexed or queryable.
Indexed fields are added to 'indexes' list below.-->
<property name="fields">
<map>
<entry key="id" value="java.lang.Long"/>
<entry key="name" value="java.lang.String"/>
<entry key="salary" value="java.lang.Long "/>
</map>
</property>
<!-- Defining indexed fields.-->
<property name="indexes">
<list>
<!-- Single field (aka. column) index -->
<bean class="org.apache.ignite.cache.QueryIndex">
<constructor-arg value="id"/>
</bean>
<!-- Group index. -->
<bean class="org.apache.ignite.cache.QueryIndex">
<constructor-arg>
<list>
<value>id</value>
<value>salary</value>
</list>
</constructor-arg>
<constructor-arg value="SORTED"/>
</bean>
</list>
</property>
</bean>
</list>
</property>
</bean>
为应用选择索引时,需要考虑很多事情。
索引每个字段是错误的!
有序索引示例
| 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 > ?
可以工作得很好。
Ignite中有两种方式停止长时间运行的SQL查询,SQL查询时间长的原因,比如使用了未经优化的索引等。
第一个方法是为特定的SqlQuery
和SqlFieldsQuery
设置查询执行的超时时间。
SqlQuery qry = new SqlQuery<AffinityKey<Long>, Person>(Person.class, joinSql);
// Setting query execution timeout
qry.setTimeout(10_000, TimeUnit.SECONDS);
第二个方法是使用QueryCursor.close()
来终止查询。
SqlQuery qry = new SqlQuery<AffinityKey<Long>, Person>(Person.class, joinSql);
// Getting query cursor.
QueryCursor<List> cursor = cache.query(qry);
// Executing query.
....
// Halting the query that might be still in progress.
cursor.close();
Ignite的1.8及其以后版本开始支持查询取消的API。
Ignite的SQL引擎支持通过额外用Java编写的自定义SQL函数,来扩展ANSI-99规范定义的SQL函数集。
一个自定义SQL函数仅仅是一个加注了@QuerySqlFunction
注解的公共静态方法。
// Defining a custom SQL function.
public class MyFunctions {
@QuerySqlFunction
public static int sqr(int x) {
return x * x;
}
}
持有自定义SQL函数的类需要使用setSqlFunctionClasses(...)
方法在特定的CacheConfiguration
中注册。
// Preparing a cache configuration.
CacheConfiguration cfg = new CacheConfiguration();
// Registering the class that contains custom SQL functions.
cfg.setSqlFunctionClasses(MyFunctions.class);
经过了上述配置的缓存部署之后,在SQL查询中就可以随意地调用自定义函数了,如下所示:
// Preparing the query that uses customly defined 'sqr' function.
SqlFieldsQuery query = new SqlFieldsQuery(
"SELECT name FROM Blocks WHERE sqr(size) > 100");
// Executing the query.
cache.query(query).getAll();
在自定义SQL函数可能要执行的所有节点上,通过
CacheConfiguration.setSqlFunctionClasses(...)
注册的类都需要添加到类路径中,否则在自定义函数执行时会抛出ClassNotFoundException
异常。
可以通过调整一些与SQL查询有关的参数,来影响查询执行的行为。
这些参数分为全局参数和查询级参数,全局参数在CacheConfiguration
层面配置,在该缓存上执行的所有查询都会受到影响。
缓存配置参数
属性名 | 描述 | 默认值 |
---|---|---|
setSqlSchema(...) |
配置当前缓存使用的SQL模式名,这个名字需要符合SQL的ANSI-99标准,加引号的区分大小写,不加引号的不区分大小写。 | 缓存名 |
setSqlEscapeAll(...) |
如果配置为true ,所有的SQL表和字段名都会加上双引号,比如"tableName"."fieldsName" ,这样会强制字段名区分大小写,同时也允许表名和字段名有特殊字符。 |
false |
setSqlOnheapRowCacheSize(...) |
定义缓存在堆内的SQL行数,来避免每次SQL索引访问的反序列化,这个参数只有在该缓存开启了堆外的时候才会起作用。 | 10,240 |
setSnapshotableIndex(...) |
为存储在Java堆内的索引数据开启快照索引实现。 | false |
SqlFields
和SqlFieldsQuery
配置参数
属性名 | 描述 | 默认值 |
---|---|---|
setCollocated(...) |
为了优化带有GROUP BY的查询的目的使用的并置标志,当Ignite执行分布式SQL查询时,它会向单个节点发送子查询,如果事先知道要查询的数据是在同一个节点上并置在一起的然后又对并置键(主键或者关系键)进行分组,Ignite会通过在远程节点分组数据而有一个显著的性能提升和网络优化。 | false |
setDistributedJoins(...) |
为一个特定的查询开启非并置模式的分布式关联。 | false |
setEnforceJoinOrder(...) |
配置一个标志来强制查询中的表关联顺序,如果配置为true ,查询优化器就不会对join子句的表进行重新排序。 |
false |
setReplicatedOnly(...) |
如果SQL查询对应的数据都在复制缓存上,那么可以将该参数设置为true ,这是给SQL引擎的一个特别提示,它会为查询产生更高效的执行计划。 |
false |
setLocal(...) |
强制查询在纯本地模式下执行。 | false |
setPageSize(...) |
定义单个响应中可以传输到发起节点的最大条目数, | 1024 |
setPartitions(...) |
设置一个查询执行的分区,该查询只会在特定分区的主节点上执行。 | null |
setTimeout(...) |
配置查询执行的超时时间,如果正在执行的查询超过了该值,其会被自动取消。默认是禁用的,Ignite的1.8及其以后版本才可用。 | 0 |
setAlias(...) |
设置一个查询中用作表名的别名。 | null |
Ignite提供了JDBC驱动,使得在JDBC API端就可以通过标准SQL查询从缓存中获得分布式数据,以及使用DML语句比如INSERT
、UPDATE
或者DELETE
更新数据。
Ignite中,JDBC连接URL的规则如下:
jdbc:ignite:cfg://[<params>@]<config_url>
<config_url>
是必需的,表示对于通过JDBC Driver建立连接期间待启动的Ignite客户端节点来说指向Ignite配置文件的任意合法URL,JDBC驱动会转发应用通过客户端发给集群的查询。<params>
是可选的,格式如下:
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驱动需要将一组jar包添加到应用或者SQL工具的类路径中,打开{apache_ignite_release}\libs
文件夹,然后导入其中以及ignite-indexing
和ignite-spring
子文件夹的所有jar文件。
客户端和服务端节点
所有的节点默认都是以服务端模式启动的,客户端模式需要显式地开启,然而无论怎么配置,JDBC驱动都会以客户端模式启动一个节点。
跨缓存查询
驱动连接到的缓存会被视为默认的模式,要跨越多个缓存进行查询,可以参照3.6.缓存查询
章节。
关联和并置
就像3.6.缓存查询
章节描述的那样,通过IgniteCache
API,如果关联对象是以并置模式存储的话,在分区缓存上的关联是可以正常执行的。细节可以参照3.11.关系并置
章节。
复制和分区缓存
在复制
缓存上的查询会直接在一个节点上执行,而在分区
缓存上的查询是分布在所有缓存节点上的。
使用JDBC驱动,可以以流模式(批处理模式)将数据注入Ignite集群。这时驱动会在内部实例化IgniteDataStreamer
然后将数据传给它。要激活这个模式,可以在JDBC连接串中增加streaming
参数并且设置为true
:
// Register JDBC driver.
Class.forName("org.apache.ignite.IgniteJdbcDriver");
// Opening connection in the streaming mode.
Connection conn = DriverManager.getConnection("jdbc:ignite:cfg://streaming=true@file:///etc/config/ignite-jdbc.xml");
目前,流模式只支持INSERT操作,对于想更快地将数据预加载进缓存的场景非常有用。JDBC驱动定义了多个连接参数来影响流模式的行为,这些参数已经在上述的参数表中列出。
这些参数几乎覆盖了IgniteDataStreamer
的所有常规配置,这样就可以根据需要更好地调整流处理器。关于如何配置流处理器可以参考流处理器
的相关文档来了解更多的信息。
基于时间的刷新
默认情况下,当要么连接关闭,要么达到了streamingPerNodeBufferSize
,数据才会被刷新,如果希望按照时间的方式来刷新,那么可以调整streamingFlushFrequency
参数。
// Register JDBC driver.
Class.forName("org.apache.ignite.IgniteJdbcDriver");
// Opening a connection in the streaming mode and time based flushing set.
Connection conn = DriverManager.getConnection("jdbc:ignite:cfg://streaming=true@streamingFlushFrequency=1000@file:///etc/config/ignite-jdbc.xml");
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO Person(_key, name, age) VALUES(CAST(? as BIGINT), ?, ?)");
// Adding the data.
for (int i = 1; i < 100000; i++) {
// Inserting a Person object with a Long key.
stmt.setInt(1, i);
stmt.setString(2, "John Smith");
stmt.setInt(3, 25);
stmt.execute();
}
conn.close();
// Beyond this point, all data is guaranteed to be flushed into the cache.
Ignite JDBC驱动会自动地只获取缓存中存储的对象中实际需要的那些字段,比如,有一个像下面这样的Person
对象。
public class Person {
@QuerySqlField
private String name;
@QuerySqlField
private int age;
// Getters and setters.
...
}
如果在缓存中有这些类的实例,可以通过标准JDBC API
查询单独的字段(name,age或者两个),比如:
// Register JDBC driver.
Class.forName("org.apache.ignite.IgniteJdbcDriver");
// Open JDBC connection (cache name is not specified, which means that we use default cache).
Connection conn = DriverManager.getConnection("jdbc:ignite:cfg://file:///etc/config/ignite-jdbc.xml");
// Query names of all people.
ResultSet rs = conn.createStatement().executeQuery("select name from Person");
while (rs.next()) {
String name = rs.getString(1);
...
}
// Query people with specific age using prepared statement.
PreparedStatement stmt = conn.prepareStatement("select name, age from Person where age = ?");
stmt.setInt(1, 30);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
String name = rs.getString("name");
int age = rs.getInt("age");
...
}
此外,可以使用DML语句对数据进行修改。
INSERT
// Insert a Person with a Long key.
PreparedStatement stmt = conn.prepareStatement("INSERT INTO Person(_key, name, age) VALUES(CAST(? as BIGINT), ?, ?)");
stmt.setInt(1, 1);
stmt.setString(2, "John Smith");
stmt.setInt(3, 25);
stmt.execute();
MERGE
// Merge a Person with a Long key.
PreparedStatement stmt = conn.prepareStatement("MERGE INTO Person(_key, name, age) VALUES(CAST(? as BIGINT), ?, ?)");
stmt.setInt(1, 1);
stmt.setString(2, "John Smith");
stmt.setInt(3, 25);
stmt.executeUpdate();
UPDATE
// Update a Person.
conn.createStatement().
executeUpdate("UPDATE Person SET age = age + 1 WHERE age = 25");
DELETE
conn.createStatement().execute("DELETE FROM Person WHERE age = 25");
ignite-jdbc.xml
的最小版本大概像下面这样:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="ignite.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
<property name="clientMode" value="true"/>
<property name="peerClassLoadingEnabled" value="true"/>
<!-- Configure TCP discovery SPI to provide list of initial nodes. -->
<property name="discoverySpi">
<bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
<property name="ipFinder">
<bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder"/>
</property>
</bean>
</property>
</bean>
</beans>
对于之前版本的Ignite(1.4之前),JDBC连接的URL有如下的格式:
jdbc:ignite://<hostname>:<port>/<cache_name>
如果觉得方便在当前的Ignite版本中还可以使用这样的格式。
要在应用或者SQL工具中使用该驱动,需要将{apache_ignite_release}\libs\ignite-core-{version}.jar
添加到类路径中。
Ignite除了支持标准ANSI-99标准的SQL查询,支持基本数据类型或者特定/自定义对象类型之外,还可以查询和索引几何数据类型,比如点、线以及包括这些几何形状空间关系的多边形。
空间信息的查询功能,以及对应的可用的函数和操作符,是在SQL的简单特性规范中定义的,Ignite使用的JTS Topology Suite完全实现了这个规范,它和H2一起,以分布式和容错的方式构建了一个独特的空间组件。
Ignite的空间库(ignite-geospatial
)依赖于JTS,它是LGPL许可证,不同于Apache的许可证,因此ignite-geospatial
并没有包含在Ignite的发布版中。
因为这个原因,ignite-geospatial
的二进制库版本位于如下的Maven仓库中:
<repositories>
<repository>
<id>GridGain External Repository</id>
<url>http://www.gridgainsystems.com/nexus/content/repositories/external</url>
</repository>
</repositories>
在pom.xml中添加这个仓库以及如下的Maven依赖之后,就可以将该空间库引入应用中了。
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-geospatial</artifactId>
<version>${ignite.version}</version>
</dependency>
另外,也可以下载Ignite的源代码自己构建这个库。
这个空间模块只对com.vividsolutions.jts
类型的对象有用。
要配置索引以及/或者几何类型的可查询字段,可以使用和已有的非几何类型同样的方法,首先,可以使用org.apache.ignite.cache.QueryEntity
定义索引,他对于基于Spring的XML配置文件非常方便,第二,通过@QuerySqlField
注解来声明索引也可以达到同样的效果,他在内部会转化为QueryEntities
。
QuerySqlField:
/**
* Map point with indexed coordinates.
*/
private static class MapPoint {
/** Coordinates. */
@QuerySqlField(index = true)
private Geometry coords;
/**
* @param coords Coordinates.
*/
private MapPoint(Geometry coords) {
this.coords = coords;
}
}
QueryEntity:
<bean class="org.apache.ignite.configuration.CacheConfiguration">
<property name="name" value="mycache"/>
<!-- Configure query entities -->
<property name="queryEntities">
<list>
<bean class="org.apache.ignite.cache.QueryEntity">
<property name="keyType" value="java.lang.Integer"/>
<property name="valueType" value="org.apache.ignite.examples.MapPoint"/>
<property name="fields">
<map>
<entry key="coords" value="com.vividsolutions.jts.geom.Geometry"/>
</map>
</property>
<property name="indexes">
<list>
<bean class="org.apache.ignite.cache.QueryIndex">
<constructor-arg value="coords"/>
</bean>
</list>
</property>
</bean>
</list>
</property>
</bean>
使用上述方法定义了几何类型字段之后,就可以使用存储于这些字段中值进行查询了。
// Query to find points that fit into a polygon.
SqlQuery<Integer, MapPoint> query = new SqlQuery<>(MapPoint.class, "coords && ?");
// Defining the polygon's boundaries.
query.setArgs("POLYGON((0 0, 0 99, 400 500, 300 0, 0 0))");
// Executing the query.
Collection<Cache.Entry<Integer, MapPoint>> entries = cache.query(query).getAll();
// Printing number of points that fit into the area defined by the polygon.
System.out.println("Fetched points [" + entries.size() + ']');
完整示例
Ignite中用于演示空间查询的可以立即执行的完整示例,可以在这里找到。
为了读取执行计划以及提高查询性能的目的,Ignite支持EXPLAIN ...
语法,注意一个计划游标会包含多行:最后一行是汇总节点的查询,其他是映射节点的。
SqlFieldsQuery sql = new SqlFieldsQuery(
"explain select name from Person where age = ?").setArgs(26);
System.out.println(cache.query(sql).getAll());
执行计划本身是由H2生成的,这里有详细描述。
当用Ignite进行开发时,有时对于检查表和索引是否正确或者运行在嵌入节点内部的H2数据库中的本地查询是非常有用的,为此Ignite提供了启动H2控制台的功能。要启用该功能,在启动节点时要将IGNITE_H2_DEBUG_CONSOLE
系统属性或者环境变量设置为true
。然后就可以在浏览器中打开控制台,可能需要点击控制台中的刷新
按钮,因为有可能控制台在数据库对象初始化之前打开。
当执行SQL查询时有一些常见的陷阱需要注意:
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
。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[]等)无法使用这个语法,但是可以使用基本类型的包装器。示例:
new SqlFieldsQuery(
"select * from Person p join table(id bigint = ?) i on p.id = i.id").setArgs(new Object[]{ new Integer[] {2, 3, 4} }))
他会被转换为下面的SQL:
select * from "cache-name".Person p join table(id bigint = (2,3,4)) i on p.id = i.id
SQL查询在每个涉及的节点上,默认是以单线程模式执行的,这种方式对于使用索引返回一个小的结果集的查询是一种优化,比如:
select * from Person where p.id = ?
某些查询以多线程模式执行会更好,这个和带有表扫描以及聚合的查询有关,这在OLAP的场景中比较常见,比如:
select SUM(salary) from Person
通过CacheConfiguration.queryParallelism
属性可以控制查询的并行化,这个参数定义了在单一节点中执行查询时使用的线程数。
如果查询包含JOIN
,那么所有相关的缓存都应该有相同的并行化配置。
注意
当前,这个属性影响特定缓存上的所有查询,可以加速很重的OLAP查询,但是会减慢其他的简单查询,这个行为在未来的版本中会改进。
当明确知道对于查询来说一个索引比另一个更合适时,索引提示就会非常有用,他也有助于指导查询优化器来选择一个更高效的执行计划。在Ignite中要进行这个优化,可以使用USE INDEX(indexA,...,indexN)
语句,它会告诉Ignite对于查询的执行只会使用给定名字的索引之一。
下面是一个示例:
SELECT * FROM Person USE INDEX(index_age)
WHERE salary > 150000 AND age < 35;
如果只在复制缓存所在的数据上执行SQL查询,那么可以设置SqlQuery.setReplicatedOnly(...)
为true
,这个给SQL引擎的特别提示会为查询产生更高效的执行计划。
使用UPDATE
和DELETE
语句时,需要执行一个SELECT
查询来获取之后要处理的缓存条目集。在某些情况下,与直接将DML语句转为特定的缓存操作相比,这样可以避免导致显著的性能问题。
总结一下4.4.分布式DML
章节的内容,之所以UPDATE
和DELETE
会自动执行一个SELECT
查询,有如下的原因:
UPDATE
或者DELETE
语句的WHERE
子句会使用复杂的过滤。这在使用复杂而高级的条目过滤时就会发生,这时DML引擎需要做额外的工作来准备要被DML语句更新的条目列表;UPDATE
语句包括表达式。即使WHERE
子句比较简单并且通过使用_key
或者_val
直接指向要修改的缓存条目,这个表达式的执行结果仍然可能产生新的字段值,这也是为什么DML引擎需要执行一个SELECT
来评估表达式的执行结果;UPDATE
语句修改一个缓存条目的特定字段。DML引擎首先需要获取当前的缓存条目,再修改然后将其放回缓存。更快地执行DML
要更快地执行DML操作,需要遵守如下的必要条件:
SELECT
查询执行;如果遵守如下的规则,就能满足上述的条件:
_key
和_val
关键字来过滤缓存条目;UPDATE
语句,然后更新整个缓存条目(_val
),而不是特定的字段。可以看下面的示例:
cache.query(new SqlFieldsQuery("UPDATE Person SET _val = ?3" +
" WHERE _key = ?1 and _val = ?2").setArgs(7, 1, 2));
UPDATE
语句会进行如下的操作:
_key
以及条目的期望值_val
来显式地指定要修改的缓存条目;_val
关键字来更新缓存条目的整个值;作为结果,DML引擎大概会像下面这样执行缓存操作:
cache.replace(7, 1, 2);