@liyuj
2017-01-09T23:01:45.000000Z
字数 29314
阅读 6542
Apache-Ignite-1.8.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以及其他的。
只要是正确的SQL查询,Ignite都支持,没有任何限制。SQL语法是ANSI-99兼容的,也就意味着作为SQL查询的一部分,规范定义的任何SQL函数、聚合、分组以及关联,都是可以使用的。
此外,查询是完全分布式的。SQL引擎的功能不仅仅是将查询映射到特定的节点然后将结果汇总为最终的结果集,它还可以将存储在不同缓存甚至是不同节点上的数据进行关联。最后值得一提的是,引擎是以容错的形式支持这些功能,保证不会因为一个网络变更事件(新节点加入,旧节点离开时发生)而获得不完整或者错误的结果。
Ignite的SQL网格组件是与H2数据库紧紧绑定在一起的,简而言之,H2是一个Java写的,遵循一组开源许可证,基于内存和磁盘的数据库。
当ignite-indexing
模块加入节点的类路径之后,一个嵌入式的H2数据库实例就会作为Ignite节点进程的一部分被启动。Ignite借用了H2的SQL查询解析器以及优化器还有执行计划器。最后,届时H2会在一个特定的节点执行本地化的查询(一个分布式查询会被映射到节点或者查询是以LOCAL
模式执行的),然后会将本地的结果集传递给分布式SQL引擎用于后续处理。
然而,数据和索引,通常是存储于Ignite数据网格端的,另外,使查询工作于分布式以及容错的方式,也是Ignite的责任,H2本身并没有这样的功能。
原则上,Ignite SQL网格执行查询有两个主要的方式:
首先,如果查询在一个部署有REPLICATED
模式缓存的节点上执行,那么Ignite会假定所有的数据都是本地化的,然后将其直接传递给H2数据库引擎执行一个简单的本地化SQL查询,对于LOCAL
模式的缓存,也是同样的执行流程。
第二,如果查询执行于PARTITIONED
模式缓存,那么执行流程如下:
在Java API层,通常有两种类型的SQL查询,分别为SqlQuery
和SqlFieldsQuery
。
替代APIs
Ignite内存SQL网格并不绑定到Java API,可以从.NET, C++, ODBC或者JDBC端连接到Ignite集群,然后根本不需要感知到Java API就可以执行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引擎可以感知到它们,后续章节还会详述。
作为单个SqlQuery
和SqlFieldsQuery
的一部分,查询的数据可以来自多个缓存。这时,缓存名会扮演类似传统RDBMS中SQL查询的模式名的角色。缓存的名字,用于创建SqlQuery
或者SqlFieldsQuery
的实例,会作为默认的模式名并且不需要显式地指定。其余的存储于不同缓存中的对象,也会被查询,但是需要加上它的缓存名(额外的模式名)作为前缀。
// 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());
要了解详细信息,可以参照非并置的分布式关联。
关于本文描述的分布式关联如何使用的完整示例,会作为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标准的。
因为数据网格中的数据都是以键-值条目的形式存储的,所以在DML执行的某些阶段,所有和DML相关的操作都会被转换为相对应的基于键-值的缓存操作命令,比如cache.put(...)
或者cache.invokeAll(...)
。
下面会深入地了解这些DML语句是如何实现的,以及应用如何使用。
通常来说,所有的DML语句会被拆分为两组,一个是往缓存中添加条目(INSERT
和MERGE
),还有就是修改已有的数据(UPDATE
和DELETE
)。
要在Java中执行这些语句需要使用已有的SqlFieldsQuery
API,之前的文档中描述过它的SELECT
语句的使用方法,除了QueryCursor<List<?>>
之外,DML操作使用的API与只读查询是一致的。SqlFieldsQuery
的返回值作为DML语句执行的结果,是一个只有一个long
类型的单条目的List<?>
,这个数值表示该DML语句影响的缓存条目的数量。
其他的API
DML API不受限于Java,也可以使用ODBC或者JDBC驱动接入Ignite集群,然后执行DML语句。
在Ignite中要进行DML操作,需要使用基于QueryEntity的方式或者使用@QuerySqlField注解,这些字段要么是缓存的键,要么是值,因此可以在DML语句中直接使用。
除了通过@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")));
However, in a majority of use cases you prefer to work with concrete fields rather than with a whole object value by executing queries like the one below:
然而,大多数情况下,可能都倾向于处理具体的字段,而不是通过执行查询处理整个对象的值,就像下面这样:
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
注解进行定义的,就像下面这样:
@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>
自定义缓存键
如果只使用预定义的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);
自定义缓存键的HashCode解析和一致性比较
创建好自定义缓存键,然后也像上面描述的那样用QueryEntity
定义好了字段之后,就需要注意缓存键的HashCode计算方式以及与其他键的比较方式。
BinaryArrayIdentityResolver
会作为Ignite中存储和传输以及序列化的所有对象默认的HashCode计算器以及一致性比较器,自定义的复杂缓存键,也会使用这个解析器,除非将其变更为BinaryFieldIdentityResolver
,这个对于DML语句是更合适的,或者,也可以切换为自定义解析器。
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')"));
INSERT
MERGE
和INSERT
命令的不同在于,后者添加的条目必须是缓存中不存在的。
如果要把一个简单的键值对插入缓存,那么最后,INSERT
语句会被转换为cache.putIfAbsent(...)
操作,否则,如果插入的是多个键值对,那么DML引擎会为每个对创建一个EntryProcessor
,然后使用cache.invokeAll(...)
将数据注入缓存。
下面的示例显示如何通过INSERT
命令插入一个数据集:
cache.query(new SqlFieldsQuery("INSERT INTO Person(_key, firstName, " +
"lastName) values (1, 'John', 'Smith'), (5, 'Mary', 'Jones')"));
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语句无法更新缓存键及其字段
原因是缓存键的状态决定了内部数据的布局及其一致性(键的Hash及其关系,索引完整性),所以目前除非先将其删除,否则无法更新缓存键。
DELETE
DELETE
语句的执行也会被拆分为两个阶段,与UPDATE
语句的执行类似。
首先,SQL引擎会使用SELECT
语句来收集满足WHERE
条件并且要被删除的缓存键,下一步,拿到这些键后,会准备一定数量的EntryProcessors
然后执行cache.invokeAll(...)
操作,当数据将被删除时,会进行额外的检查来确保在SELECT
和数据实际删除之间没有其他干扰。
下面这个简单示例显示了如何执行DELETE
语句。
cache.query(new SqlFieldsQuery("DELETE FROM Person " +
"WHERE _key >= ?").setArgs(2L));
如果一个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操作不支持EXPLAIN
。
一个方法就是执行UPDATE
或DELETE
语句自动生成的SELECT
语句或者DML语句使用的INSERT
或MERGE
语句的执行计划,这样会提供一个要执行的DML操作所使用的索引情况。
Ignite在源代码中包含了一个可以立即执行的CacheQueryDmlExample
,这个示例演示了上面提到的所有DML操作的用法。
Ignite支持高级的索引功能,可以定义包括各种参数的单字段(也可以叫做列)或者分组索引,这些参数可以管理索引位于Java堆或者堆外空间等等。
Ignite中的索引和缓存数据集一样,也可以以分布式的方式存在,每一个节点都保存数据的一个特定子集,还会保持和管理与这个数据对应的索引。
本章节会描述如何像查询字段那样,使用两种方法来定义和管理索引,以及如何在Ignite支持的特定索引实现之间进行切换。
索引,和可查询的字段一样,是可以通过编程的方式用@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
降序排列。
如果不希望索引一个字段,但是仍然需要在查询的SELECT
和WHERE
子句中使用它,那么在加注解时可以忽略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的配置。
在上面基于注解的配置涉及的所有概念,对于基于查询实体的方式也都有效,深入地说,通过@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 which fields, listed above, will be treated as
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>
当索引存储于Java堆上时,SQL网格提供了两种索引实现。
第一个是基于跳跃表数据结构的,它也是默认的实现。
第二个实现是基于一个快速复制的AVL树的修改版,这个实现在Ignite中被称为一个快照,可以通过CacheConfiguration.setSnapshotableIndex(...)
方法开启。
对于下面讨论的堆外模式,Ignite只提供一种索引实现,是一个快速复制的AVL树的修改版。
Ignite支持将索引数据放在堆外内存,这个设计对于避免在堆上保存特别大的数据集导致频繁的垃圾回收以及不可预知的响应时间是很有用的。
Ignite默认将SQL索引存储于堆内,如果将CacheConfiguration.setMemoryMode
配置为堆外内存模式之一:OFFHEAP_TIERED
或OFFHEAP_VALUES
,或者将CacheConfiguration.setOffHeapMaxMemory
属性配置为>=0,Ignite会将索引保存于堆外。
要通过开启堆外模式来提高SQL查询的性能,可以试着增加CacheConfiguration.setSqlOnheapRowCacheSize()
属性的值,它的默认值是10000.
Java:
CacheConfiguration<Object, Object> ccfg = new CacheConfiguration<>();
// Set unlimited off-heap memory for cache and enable off-heap indexes.
ccfg.setOffHeapMaxMemory(0);
// Cache entries will be placed on heap and can be evited to off-heap.
ccfg.setMemoryMode(ONHEAP_TIERED);
ccfg.setEvictionPolicy(new RandomEvictionPolicy(100_000));
// Increase size of SQL on-heap row cache for off-heap indexes.
ccfg.setSqlOnheapRowCacheSize(100_000);
为应用选择索引时,需要考虑很多事情。
索引每个字段是错误的!
有序索引示例
| 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(...) |
为了优化目的使用的并置标志,当Ignite执行分布式SQL查询时,它会向单个节点发送子查询,通过在远程节点上对数据进行分组,如果要查询的数据元素在同一个节点上并置在一起,Ignite会有一个显著的性能提升和网络优化。 | false |
setDistributedJoins(...) |
为一个特定的查询开启非并置模式的分布式关联。 | false |
setEnforceJoinOrder(...) |
配置一个标志来强制查询中的表关联顺序,如果配置为true ,查询优化器就不会对join子句的表进行重新排序。 |
false |
setLocal(...) |
强制查询在纯本地模式下执行。 | false |
setPageSize(...) |
定义单个响应中可以传输到发起节点的最大条目数, | 1024 |
setTimeout(...) |
配置查询执行的超时时间,如果正在执行的查询超过了该值,其会被自动取消。默认是禁用的,Ignite的1.8及其以后版本才可用。 | 0 |
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,要了解更多细节可以参照1.6.客户端和服务器
章节。<params>
是可选的,格式如下:
param1=value1:param2=value2:...:paramN=valueN
它支持如下的参数:
属性 | 描述 | 默认值 |
---|---|---|
cache |
缓存名,如果未定义会使用默认的缓存,区分大小写 | |
nodeId |
要执行的查询所在节点的Id,对于在本地查询是有用的 | |
local |
查询只在本地节点执行,这个参数和nodeId 参数都是通过指定节点来限制数据集 |
false |
collocated |
优化标志,当Ignite执行一个分布式查询时,他会向单个的集群节点发送子查询,如果提前知道要查询的数据已经被并置到同一个节点,Ignite会有显著的性能提升和网络优化 | false |
distributedJoins |
可以在非并置的数据上使用分布式关联。 | false |
跨缓存查询
驱动连接到的缓存会被视为默认的模式,要跨越多个缓存进行查询,可以参照3.6.缓存查询
章节。
关联和并置
就像3.6.缓存查询
章节描述的那样,通过IgniteCache
API,如果关联对象是以并置模式存储的话,在分区缓存上的关联是可以正常执行的。细节可以参照3.11.关系并置
章节。
复制和分区缓存
在复制
缓存上的查询会直接在一个节点上执行,而在分区
缓存上的查询是分布在所有缓存节点上的。
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除了支持标准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)
,age
上的索引并不会生效,虽然它明显比sex
上的索引选择性更强。要解决这个问题需要用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[]等)无法使用这个语法,但是可以使用基本类型的包装器。使用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);