分布式查询访问存储在相同或不同成员上的多个数据源的数据。
Hazelcast对您的数据进行分区,并将其分布到成员集群中。您可以迭代映射条目并查找您感兴趣的某些条目(由谓词指定)。但是,这不是非常有效,因为您必须携带整个条目集并在本地迭代。相反,Hazelcast允许您在分布式地图上运行分布式查询。
- 请求的谓词将发送到群集中的每个成员。
- 每个成员查看自己的本地条目并根据谓词过滤它们。在此阶段,条目的键/值对被反序列化,然后传递给谓词。
- 谓词请求者将来自每个成员的所有结果合并到一个集合中。
分布式查询具有高度可伸缩 如果向集群添加新成员,则会减少每个成员的分区计数,从而减少每个成员迭代其条目所花费的时间。此外,分区线程池会在每个成员中同时评估条目,并且网络流量也会减少,因为只有过滤后的数据才会发送给请求者。
Hazelcast为分布式查询提供以下API:
- Criteria API
- 分布式SQL查询
假设您有一个包含Employee对象值的“员工”地图,如下所示。
public class Employee implements Serializable {
private String name;
private int age;
private boolean active;
private double salary;
public Employee(String name, int age, boolean active, double salary) {
this.name = name;
this.age = age;
this.active = active;
this.salary = salary;
}
public Employee() {
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public double getSalary() {
return salary;
}
public boolean isActive() {
return active;
}
}
现在让我们使用上述API(Criteria API和Distributed SQL Query)查找活跃且年龄小于30的员工。以下小节描述了此示例的每个查询机制。
使用Portable对象时,如果一个对象的一个字段存在于一个成员上但在另一个成员上不存在,则Hazelcast不会抛出未知字段异常。相反,Hazelcast将该谓词视为永远错误的谓词,该谓词试图对未知字段执行查询。 |
Criteria API是Hazelcast提供的编程接口,类似于Java持久性查询语言(JPQL)。以下是上述示例查询的代码。
IMap<String, Employee> map = hazelcastInstance.getMap( "employee" );
EntryObject e = new PredicateBuilder().getEntryObject();
Predicate predicate = e.is( "active" ).and( e.get( "age" ).lessThan( 30 ) );
Collection<Employee> employees = map.values( predicate );
在上面的示例代码中,predicate验证条目是否处于活动状态且其age值是否小于30.这将使用该方法predicate应用于employee地图map.values(predicate)。此方法将谓词发送给所有集群成员,并合并来自它们的结果。由于谓词是在成员之间传递的,因此需要可序列化。
谓词也可以应用到keySet,entrySet并且localKeySet所述分布式Hazelcast地图。 |
在Predicates通过Hazelcast提供类包括您所查询的需求很多运营商。其中一些解释如下。
- equal:检查表达式的结果是否等于给定值。
- notEqual:检查表达式的结果是否不等于给定值。
- instanceOf:检查表达式的结果是否具有某种类型。
- like:检查表达式的结果是否与某些字符串模式匹配。%(百分号)是许多字符的占位符,(下划线)是仅占一个字符的占位符。
- greaterThan:检查表达式的结果是否大于某个值。
- greaterEqual:检查表达式的结果是否大于或等于某个值。
- lessThan:检查表达式的结果是否小于某个值。
- lessEqual:检查表达式的结果是否小于或等于某个值。
- between:检查表达式的结果是否在两个值之间(这是包含的)。
- in:检查表达式的结果是否是某个集合的元素。
- isNot:检查表达式的结果是否为false。
- regex:检查表达式的结果是否与某个正则表达式匹配。
有关所提供的所有谓词, 请参阅Predicates Javadoc。 |
您可以使用组合谓词and,or和not运营商,如在下面的例子。
public Collection<Employee> getWithNameAndAge( String name, int age ) {
Predicate namePredicate = Predicates.equal( "name", name );
Predicate agePredicate = Predicates.equal( "age", age );
Predicate predicate = Predicates.and( namePredicate, agePredicate );
return employeeMap.values( predicate );
}
public Collection<Employee> getWithNameOrAge( String name, int age ) {
Predicate namePredicate = Predicates.equal( "name", name );
Predicate agePredicate = Predicates.equal( "age", age );
Predicate predicate = Predicates.or( namePredicate, agePredicate );
return employeeMap.values( predicate );
}
public Collection<Employee> getNotWithName( String name ) {
Predicate namePredicate = Predicates.equal( "name", name );
Predicate predicate = Predicates.not( namePredicate );
return employeeMap.values( predicate );
}
您可以使用PredicateBuilder类简化谓词使用,从而提供更简单的谓词构建。请参阅下面的示例代码,该代码选择具有特定名称和年龄的所有人。
public Collection<Employee> getWithNameAndAgeSimplified( String name, int age ) {
EntryObject e = new PredicateBuilder().getEntryObject();
Predicate agePredicate = e.get( "age" ).equal( age );
Predicate predicate = e.get( "name" ).equal( name ).and( agePredicate );
return employeeMap.values( predicate );
}
com.hazelcast.query.SqlPredicate采用常规SQL where子句。这是一个例子:
IMap<Employee> map = hazelcastInstance.getMap( "employee" );
Set<Employee> employees = map.values( new SqlPredicate( "active AND age < 30" ) );
AND / OR: `<expression> AND <expression> AND <expression> ...`
- active AND age>30
- active=false OR age = 45 OR name = 'Joe'
- active AND ( age > 20 OR salary < 60000 )
平等: =, !=, <, ⇐, >, >=
- <expression> = value
- age ⇐ 30
- name = 'Joe'
- salary != 50000
之间: <attribute> [NOT] BETWEEN <value1> AND <value2>
- age BETWEEN 20 AND 33 ( same as age >= 20 AND age ⇐ 33 )
- age NOT BETWEEN 30 AND 40 ( same as age < 30 OR age > 40 )
在: <attribute> [NOT] IN (val1, val2,…)
- age IN ( 20, 30, 40 )
- age NOT IN ( 60, 70 )
- active AND ( salary >= 50000 OR ( age NOT BETWEEN 20 AND 30 ) )
- age IN ( 20, 30, 40 ) AND salary BETWEEN ( 50000, 80000 )
喜欢: <attribute> [NOT] LIKE "expression"
的%(百分比符号)是用于多个字符的占位符,一个_(下划线)为只有一个字符占位符。
- name LIKE 'Jo%' (对于'Joe','Josh','Joseph'等都是如此)
- name LIKE 'Jo_' (对于'Joe'是真的;对于'Josh'是假的)
- name NOT LIKE 'Jo_' (对于'Josh'是真的;对'Joe'是假的)
- name LIKE 'J_s%' (对于'乔希','约瑟夫';假'约翰','乔')
我喜欢: <attribute> [NOT] ILIKE 'expression'
与LIKE谓词类似,但不区分大小写。
- name ILIKE 'Jo%' (对于'Joe','jo','jOe','Josh','joSH'等都是如此)
- name ILIKE 'Jo_' (对于'Joe'或'jOE'是真的;对于'Josh'是假的)
REGEX:<attribute> [NOT] REGEX 'expression'
- name REGEX 'abc-.*' (对于'abc-123'为true;对于'abx-123'为false)
您可以使用__keyattribute来执行条目键的谓词搜索。请参阅以下示例:
IMap<String, Person> personMap = hazelcastInstance.getMap(persons);
personMap.put("Alice", new Person("Alice", 35, Gender.FEMALE));
personMap.put("Andy", new Person("Andy", 37, Gender.MALE));
personMap.put("Bob", new Person("Bob", 22, Gender.MALE));
[...]
Predicate predicate = new SqlPredicate("__key like A%");
Collection<Person> startingWithA = personMap.values(predicate);
在此示例中,代码创建一个集合,其条目的键以字母“A”开头。
Hazelcast为定义的谓词提供分页。通过它的PagingPredicate类,您可以通过使用谓词过滤它们并给出页面大小来逐页获取键,值或条目的集合。此外,您可以通过指定比较器对条目进行排序。
在下面的示例代码中:
- 该greaterEqual谓词从“学生”地图得到的值。此谓词具有过滤器,用于检索“年龄”大于或等于18的对象。
- 然后PagingPredicate构建一个页面大小为5的页面,因此每页中将有五个对象。第一次调用值时会创建第一页。
- 它使用nextPage() 方法获取后续页面,PagingPredicate并使用更新后再次查询地图PagingPredicate。
IMap<Integer, Student> map = hazelcastInstance.getMap( "students" );
Predicate greaterEqual = Predicates.greaterEqual( "age", 18 );
PagingPredicate pagingPredicate = new PagingPredicate( greaterEqual, 5 );
// Retrieve the first page
Collection<Student> values = map.values( pagingPredicate );
...
// Set up next page
pagingPredicate.nextPage();
// Retrieve next page
values = map.values( pagingPredicate );
...
如果未指定比较器PagingPredicate,但您希望逐页获取键或值的集合,则此集合必须是Comparable(即必须实现java.lang.Comparable)的实例。否则,java.lang.IllegalArgument抛出异常。
从Hazelcast 3.6开始,您还可以借助该方法更轻松地访问特定页面setPage()。这样,例如,如果您对第100页进行查询,它将一次获得所有100个页面,而不是使用该方法逐个到达第100页nextPage()。请注意,此功能会耗尽内存并引用PagingPredicate Javadoc。
事务上下文不支持分页谓词,也称为顺序和限制。
您可以使用partition predicate(PartitionPredicate)在集群中的单个分区上运行查询。
它将谓词和分区键作为参数,使用键获取分区ID,并仅在该键所属的分区上运行该谓词。
请参阅以下代码段:
...
Predicate predicate = new PartitionPredicate<String, Integer>(partitionKey, TruePredicate.INSTANCE);
Collection<Integer> values = map.values(predicate);
Collection<String> keys = map.keySet(predicate);
...
默认情况下,有271个分区,并且使用常规谓词,需要访问每个分区。但是,如果分区谓词只能访问单个分区,则可以大大提高性能。
要使分区谓词正常工作,您需要知道数据所属的分区,以便将请求发送到正确的分区。其中一种方法是在PartitionAware插入数据时使用接口,从而控制拥有的分区。有关更多信息和示例,请参阅PartitionAware部分。
一个具体的例子可能是销售电话和配件的网上商店。要查找电话的所有附件,可以执行选择该电话的所有附件的查询。此查询在群集中的所有成员上执行,因此可能会产生大量负载。但是,如果我们将配件存储在与手机相同的分区中,则分区谓词可以使用partitionKey手机的选项来选择正确的分区,然后查询该手机的配件; 这减少了系统的负担,并获得更快的查询结果。
Hazelcast分布式查询将并行运行在每个成员上,并仅将结果返回给调用者。然后,在调用者方面,结果将被合并。
当查询在成员上运行时,Hazelcast将遍历所有拥有的条目并找到匹配的条目。通过索引大多数查询字段可以更快地完成此操作,就像您对数据库所做的那样。索引将增加每个write 操作的开销,但查询速度会快很多。如果您经常查询地图,请确保为最常查询的字段添加索引。例如,如果进行active and age < 30查询,请确保为active和 age字段添加索引。以下示例代码通过以下方式执行:
- 从Hazelcast实例获取地图,和
- 使用IMap addIndex方法向地图添加索引。
IMap map = hazelcastInstance.getMap( "employees" );
// ordered, since we have ranged queries for this field
map.addIndex( "age", true );
// not ordered, because boolean field cannot have range
map.addIndex( "active", false );
IMap.addIndex(fieldName, ordered)用于添加索引。对于每个索引字段,如果您有远程查询(例如age>30,) age BETWEEN 40 AND 60,则应将ordered参数设置为true。否则,将其设置为false。
此外,您还可以IMap在配置中定义索引。一个例子如下所示。
<map name="default">
...
<indexes>
<index ordered="false">name</index>
<index ordered="true">age</index>
</indexes>
</map>
您还可以IMap使用编程配置定义索引,如下例所示。
mapConfig.addMapIndexConfig( new MapIndexConfig( "name", false ) );
mapConfig.addMapIndexConfig( new MapIndexConfig( "age", true ) );
以下是同一样本的Spring声明性配置。
<hz:map name="default">
<hz:indexes>
<hz:index attribute="name"/>
<hz:index attribute="age" ordered="true"/>
</hz:indexes>
</hz:map>
要编制索引的非基本类型应该实现Comparable。 | |
从Hazelcast 3.9开始,如果将数据结构配置为使用高密度存储器 和索引,索引也会自动存储在高密度存储器中。这可以防止在对索引进行大量更新时遇到完整的GC。 |
索引使用的基础数据结构需要复制查询结果以确保结果正确。当从数据结构读取索引(读取时)或写入(写入时)时,执行此复制过程。
读取复制意味着,对于每个索引读取操作,在将查询结果发送给调用者之前复制查询结果。根据查询结果的大小,这种类型的索引复制可能较慢,因为结果存储在映射中,即,所有条目需要在存储之前计算哈希值。与索引读取操作不同,每个索引写入操作都很快,因为不会发生复制。因此,在索引写入密集型情况下,此选项可能是首选。
写入复制意味着每个索引写入操作都完全复制底层映射以提供写时复制语义,这可能是一个缓慢的操作,具体取决于索引大小。与索引写入操作不同,每个索引读取操作都很快,因为操作仅包括访问存储结果并将其返回给调用者的映射。
另一种选择是永远不要将查询结果复制到单独的地图。这意味着底层索引映射支持的结果可以在执行查询后更改(例如,可能已从索引添加或删除条目,或者可能已重新映射该条目)。如果您期望“大多数正确”结果,即在查询结果集中返回的某些条目与初始查询条件不匹配时不成问题,则可以首选此选项。这是最快的选择,因为没有复制。
您可以使用系统属性设置一个这些选项hazelcast.index.copy.behavior。可以设置以下值中解释的以下值:
- COPY_ON_READ (默认值)
- COPY_ON_WRITE
- NEVER
BINARY和OBJECT内存格式支持使用此系统属性。仅在Hazelcast 3.8.7中,它也支持NATIVE内存格式。 |
您还可以定义可在谓词,查询和索引中引用的自定义属性。可以通过实现a来定义自定义属性ValueExtractor。有关详细信息,请参阅索引自定义属性部分。
您可以使用该pool-size属性更改专用于查询操作的线程池的大小。每个查询都使用来自每个Hazelcast成员的通用操作ThreadPool的单个线程 - 让我们将其称为查询编排线程。在该成员的查询的整个执行范围内,该线程被阻塞。
在两种情况下,查询编排线程将使用查询线程池中的线程:
- 如果您运行PagingPredicate- 因为每个页面作为单独的任务运行,
- 如果将系统属性设置hazelcast.query.predicate.parallel.evaluation为true - 因为谓词是并行计算的。
有关分页谓词和上述系统属性描述的信息,请参阅使用分页谓词和系统属性部分过滤。
下面是声明性配置的示例。
<executor-service name="hz:query">
<pool-size>100</pool-size>
</executor-service>
以下是等效的编程配置。
Config cfg = new Config();
cfg.getExecutorConfig("hz:query").setPoolSize(100);
处理来自客户端的成员的查询请求时,Hazelcast提供以下系统属性来调整线程池:
- hazelcast.clientengine.thread.count这是处理非分区感知客户端请求的线程数,例如map.size()执行程序任务。其默认值是核心数乘以20。
- hazelcast.clientengine.query.thread.count这是处理来自客户端的查询请求的线程数。其默认值是核心数。
如果客户端有很多查询请求,您可能希望增加其值hazelcast.clientengine.query.thread.count。除了此调整之外,您还可以考虑增加hazelcast.clientengine.thread.count系统中CPU负载不高并且有足够可用内存的值。
Hazelcast允许查询集合和数组。查询集合和数组与所有Hazelcast序列化方法兼容,包括便携式序列化。
让我们看一下以伪代码表示的以下数据结构:
class Motorbike {
Wheel wheels[2];
}
class Wheel {
String name;
}
为了查询集合/数组的单个元素,您可以执行以下查询:
// it matches all motorbikes where the zero wheel's name is 'front-wheel'
Predicate p = Predicates.equal("wheels[0].name", "front-wheel");
Collection<Motorbike> result = map.values(p);
也可以使用any语义查询集合/数组,如下所示:
// it matches all motorbikes where any wheel's name is 'front-wheel'
Predicate p = Predicates.equal("wheels[any].name", "front-wheel");
Collection<Motorbike> result = map.values(p);
可以使用SQLPredicate如下所示执行完全相同的查询:
Predicate p = new SqlPredicate("wheels[any].name = 'front-wheel'");
Collection<Motorbike> result = map.values(p);
[] 符号适用于集合和数组。
您还可以使用集合和数组中的查询创建索引。
请注意,为了利用索引,查询中使用的属性名称必须与索引定义中使用的属性名称相同。
假设您有以下索引定义:
<indexes>
<index ordered="false">wheels[any].name</index>
</indexes>
以下查询将使用索引:
Predicate p = Predicates.equal("wheels[any].name", "front-wheel");
但是,以下查询不会利用索引,因为它不使用与索引中使用的完全相同的属性名称:
Predicates.equal("wheels[0].name", "front-wheel")
为了在上面提到的情况下使用索引,你必须创建另一个索引,如下所示:
<indexes>
<index ordered="false">wheels[0].name</index>
</indexes>
角落情况的处理可能与编程语言有点不同Java。
让我们看看以下示例,以了解其中的差异。为了简化分析,我们假设MotorbikeHazelcast Map中只存储了一个对象。
ID | 询问 | 数据状态 | 提取结果 | 比赛 |
1 | Predicates.equal("wheels[7].name", "front-wheel") | wheels.size() == 1 | null | 没有 |
2 | Predicates.equal("wheels[7].name", null) | wheels.size() == 1 | null | 是 |
3 | Predicates.equal("wheels[0].name", "front-wheel") | wheels[0].name == null | null | 没有 |
4 | Predicates.equal("wheels[0].name", null) | wheels[0].name == null | null | 是 |
五 | Predicates.equal("wheels[0].name", "front-wheel") | wheels[0] == null | null | 没有 |
6 | Predicates.equal("wheels[0].name", null) | wheels[0] == null | null | 是 |
7 | Predicates.equal("wheels[0].name", "front-wheel") | wheels == null | null | 没有 |
8 | Predicates.equal("wheels[0].name", null) | wheels == null | null | 是 |
如你所见,没有 NullPointerException`s or `IndexOutOfBoundException`s are thrown in the extraction process, even though parts of the expression are `null。
查看示例4,6和8,我们也可以很容易地注意到无法区分表达式的哪个部分为空。如果我们执行以下查询wheels[1].name = null,则可能会将其评估为true,因为:
- wheels collection / array为null。
- index == 1 是出界的。
- name轮子的属性[1]对象是null。
为了使查询明确,必须添加额外的条件,例如, wheels != null AND wheels[1].name = null。
可以定义可在谓词,查询和索引中引用的自定义属性。
自定义属性是一种“合成”属性,它不是作为一个field或一个getter在从中提取的对象中存在。因此,有必要定义关于如何提取属性的策略。目前,提取自定义属性的唯一方法是实现com.hazelcast.query.extractor.ValueExtractor 包含提取逻辑的方法。
自定义属性与所有Hazelcast序列化方法兼容,包括便携式序列化。
为了实现a ValueExtractor,扩展抽象com.hazelcast.query.extractor.ValueExtractor类并实现该extract()方法。此方法不返回任何值,因为提取的值由ValueCollector。为了从单个提取返回多个结果,请ValueCollector.collect()多次调用该方法,以便收集器收集所有结果。
请参阅ValueExtractor和ValueCollectorJavadocs。
可移植序列化是一种特殊的序列化,其中不需要在类路径上具有序列化对象的类以便读取其属性。这就是为什么传递给ValueExtractor.extract() 方法的目标对象不是已存储的确切类型的原因。相反,com.hazelcast.query.extractor.ValueReader将传递一个实例。 ValueReader能够以通用和类型无关的方式读取Portable对象的属性。它包含两种方法:
- read(String path, ValueCollector<T> collector)- 允许将所有结果直接传递给ValueCollector。
- read(String path, ValueCallback<T> callback)- 启用对读取操作的结果进行过滤,转换和分组,并手动将其传递给ValueCollector。
请参考ValueReaderJavadoc。
这听起来有点违反直觉,但是当涉及数组或集合时,单个提取可能会返回多个值。让我们看一下伪代码中的以下数据结构:
class Motorbike {
Wheel wheel[2];
}
class Wheel {
String name;
}
让我们假设我们想要从单个摩托车对象中提取所有车轮的名称。每辆摩托车都有两个轮子,因此每辆自行车有两个名字。要从提取操作返回两个值,请使用分别收集它们ValueCollector。以这种方式收集多个值允许您对这些多个值进行操作,就像在评估谓词期间它们是单个值一样。
假设我们注册了一个带有名称的自定义提取器wheelName并执行了以下查询: wheelName = front-wheel。
提取可以为每个提取最多两个轮名,Motorbike因为每个Motorbike轮最多有两个轮。在这种情况下,如果单个值将谓词的条件评估为true以返回匹配就足够了,因此Motorbike如果“任意”轮子与表达式匹配,它将返回。
一个ValueExtractor如果在查询中指定可以使用自定义的参数。自定义参数可以在位于自定义属性名称之后的方括号内传递,例如customAttribute[argument]。
让我们看看以下查询:currency[incoming] == EUR 这currency是一个使用com.test.CurrencyExtractor提取的自定义属性。
字符串incoming是将ArgumentParser在提取期间传递给的参数。解析器将根据解析器的自定义逻辑解析字符串,它将返回一个已解析的对象。解析的对象可以是单个对象,数组,集合或任何任意对象。由ValueExtractor的实现者来理解解析的参数对象的语义。
现在是不是可以注册一个自定义的ArgumentParser,因此使用默认解析器。它遵循pass-through语义,这意味着位于方括号中的字符串“按原样”传递给ValueExtractor.extract()方法。
请注意,不允许在参数字符串中使用方括号。
以下代码段演示了如何使用a定义自定义属性ValueExtractor。
MapAttributeConfig attributeConfig = new MapAttributeConfig();
attributeConfig.setName("currency");
attributeConfig.setExtractor("com.bank.CurrencyExtractor");
MapConfig mapConfig = new MapConfig();
mapConfig.addMapAttributeConfig(attributeConfig);
currency是将使用CurrencyExtractor类提取的自定义属性的名称。
请记住,在实例化地图后可能无法添加提取器。必须在地图的初始配置中预先定义所有提取器。
以下代码段演示了如何在Hazelcast XML配置中定义自定义属性。
<map name="trades">
<attributes>
<attribute extractor="com.bank.CurrencyExtractor">currency</attribute>
</attributes>
</map>
类似于上面的示例,currency是将使用CurrencyExtractor类提取的自定义属性的名称 。
请注意,属性名称可以以ASCII字母[A-Za-z]或数字[0-9]开头,稍后可能包含ASCII字母[A-Za-z],数字[0-9]或下划线。
您可以使用自定义属性创建索引。
索引定义中使用的属性名称必须与属性配置中使用的属性名称匹配。
允许使用提取参数定义索引,如下例所示:
<indexes>
<!-- custom attribute without an extraction argument -->
<index ordered="true">currency</index>
<!-- custom attribute using an extraction argument -->
<index ordered="true">currency[EUR]</index>
</indexes>
自Hazelcast 3.8以来,不推荐使用MapReduce。您可以分别使用Fast-Aggregations和Hazelcast Jet进行地图聚合和常规数据处理。有关更多详细信息,请参阅MapReduce Deprecation部分。 |
自谷歌发布 有关此概念的研究白皮书以来,您可能已经听说过MapReduce 。借助Hadoop作为最常见和众所周知的实现,MapReduce获得了广泛的受众,并使其成为由数据仓库主导的各种业务应用程序。
MapReduce是一种用于以分布式方式处理大量数据的软件框架。因此,处理通常分布在几台机器上。MapReduce背后的基本思想是将源数据映射到键值对的集合中,并按键分组,在第二步中减少这些对,以实现最终结果。
主要思想可以通过以下步骤进行总结。
- 阅读源数据。
- 将数据映射到一个或多个键值对。
- 使用相同的密钥减少所有对。
用例
MapReduce算法最着名的例子是文本处理工具,例如计算大型文本或网站中的单词频率。除此之外,还有更多有趣的用例示例如下所示。
- 日志分析
- 数据查询
- 汇总和总结
- 分布式排序
- ETL(提取变换加载)
- 信用和风险管理
- 欺诈识别
- 和更多。
本节将深入介绍MapReduce模式,并帮助您了解不同MapReduce阶段背后的语义以及它们在Hazelcast中的实现方式。
除此之外,以下部分还比较了Hadoop和Hazelcast MapReduce实现,以帮助具有Hadoop背景的采用者快速熟悉Hazelcast MapReduce。
下面的流程图演示了MapReduce部分介绍中提到的字数统计示例(分布式事件分析)的基本工作流程。从左到右,它迭代数据结构的所有条目(在本例中为IMap)。在映射阶段,它将句子分成单个单词并为每个单词发出一个键值对:单词是键,1是值。在下一阶段,收集(分组)值并将其传输到相应的减速器,最终将它们缩减为单个键值对,该值是单词的出现次数。在最后一步,将不同的reducer结果分组到最终结果并返回给请求者。
在伪代码中,相应的map和reduce函数如下所示。Hazelcast代码示例将在下一节中显示。
map( key:String, document:String ):Void ->
for each w:word in document:
emit( w, 1 )
reduce( word:String, counts:List[Int] ):Int ->
return sum( counts )
如工作流示例中所示,MapReduce过程由多个阶段组成。原始MapReduce模式描述了两个阶段(map,reduce)和一个可选阶段(combine)。在Hazelcast中,这些阶段要么仅存在以解释数据流,要么在实际操作期间并行执行,而总体思路仍然存在。
(K x V)\ *→(长x宽)*
[(k * 1 *,v * 1 *),...,(k * n *,v * n *)]→[(l * 1 *,w * 1 *),...,(l * m * ,w * m *)]
映射阶段
映射阶段迭代任何类型的合法输入源的所有键值对。然后,映射器分析输入对并发出零个或多个新的键值对。
K x V→(长x宽)*
(k,v)→[(l * 1 *,w * 1 *),...,(l * n *,w * n *)]
结合阶段
在组合阶段,收集具有相同密钥的多个键值对,并在发送到减速器之前将其组合成中间结果。在Hazelcast中,组合阶段也是可选的,但强烈建议降低流量。
就字数例子而言,这可以用句子“土星是一颗行星,但地球也是一颗行星”来解释。如上所示,我们将发送两个键值对(行星,1)。注册的组合器现在收集这两对,并将它们组合成(行星,2)的中间结果。而不是通过电线发送的两个键值对,现在只有一个用于关键的“行星”。
组合器的伪代码类似于reducer。
combine( word:String, counts:List[Int] ):Void ->
emit( word, sum( counts ) )
分组/改组阶段
分组或改组阶段实际上只存在于Hazelcast中,因为它不是真正的阶段; 具有相同键的发出键值对始终传送到同一作业中的同一减速器。它们组合在一起,相当于改组阶段。
减少阶段
在缩减阶段,收集的中间键值对通过其键减少以构建最终的按键结果。根据用例,该值可以是相同键的所有发射值的总和,平均值或完全不同的值。
这是该阶段的简化表示。
L x W \ *→X *
(l,[w * 1 *,...,w * n *])→[x * 1 *,...,x * n *]
产生最终结果
这不是一个真正的MapReduce阶段,但它是Hazelcast的最后一步,因为所有减速器都被通知减少已完成。然后,原始作业启动器会请求所有缩减结果并构建最终结果。
互联网上充满了有用的资源,可以在MapReduce上查找更深入的信息。以下是更多介绍材料的简短集合。此外,还有关于各种MapReduce模式的书籍以及如何为您的用例编写MapReduce函数。要命名它们都不在本文档的范围内,但这里有一些资源:
- http://research.google.com/archive/mapreduce.html
- http://en.wikipedia.org/wiki/MapReduce
- http://hci.stanford.edu/courses/cs448g/a2/files/map_reduce_tutorial.pdf
- http://ksat.me/map-reduce-a-really-simple-introduction-kloudo/
- http://www.slideshare.net/franebandov/an-introduction-to-mapreduce-6789635
本节介绍Hazelcast MapReduce框架的基础知识。在浏览不同的API类时,我们将构建前面讨论过的单词计数示例,并逐步创建它。
用于MapReduce操作的Hazelcast API包含用于构建和提交作业的流畅的类似DSL的配置语法。JobTracker是所有MapReduce操作的基本入口点,可com.hazelcast.core.HazelcastInstance通过调用getJobTracker并提供所需JobTracker配置的名称来检索。`JobTracker的配置将在后面讨论; 现在我们专注于API本身。此外,API的完整提交部分旨在支持完全被动的编程方式。
为了简单介绍一下习惯Hadoop的人,我们创建了类名,尽可能熟悉Hadoop上的同类。这意味着虽然大多数用户会识别很多类似的声音类,但由于类似DSL的样式API,配置作业的方式更加流畅。
在构建示例时,我们将尽可能多地选择,例如,我们将创建一个专门的JobTracker配置(最后)。JobTracker不需要特殊配置,因为对于所有其他Hazelcast功能,您可以使用“default”作为配置名称。但是,特殊配置提供了更好的选项来预测框架执行的行为。
完整的示例在此处可用作准备运行的Maven项目。
JobTracker创建Job实例,而每个实例com.hazelcast.mapreduce.Job定义一个MapReduce配置。无论是并行执行还是在上一次执行完成后,都可以多次提交相同的Job。
检索之后JobTracker,请注意它只应与从同一HazelcastInstance派生的数据结构一起使用。否则,您可能会遇到意外行为。 |
要从JobTrackerHazelcast中检索一个,我们将首先使用“默认”配置,以便于显示基本方法。
JobTracker jobTracker = hazelcastInstance.getJobTracker( "default" );
JobTracker使用与大多数其他Hazelcast功能相同的入口点检索。构建群集连接后,使用创建的HazelcastInstance JobTracker从Hazelcast 请求配置(或默认)。
下一步是创建一个新的Job并配置它以执行针对集群数据的第一个MapReduce请求。
如上一节所述,您可以使用检索到的JobTracker实例创建Job 。作业定义了MapReduce任务的一个配置。每个作业将定义映射器,组合器和减速器。但是,由于Job实例只是一个配置,因此可以多次提交,无论执行是并行还是一个接一个地执行。
始终使用`JobTracker的名称和在提交时生成的jobId的唯一组合来标识提交的作业。检索jobId的方法将在后面的一个部分中显示。
要创建一个Job,需要第二个类com.hazelcast.mapreduce.KeyValueSource。我们将KeyValueSource在下一节深入研究该课程。KeyValueSource用于将任何类型的数据或数据结构包装到一组定义良好的键值对中。
下面的示例代码是检索JobTracker实例中的示例的直接后续内容,它重用了已创建的HazelcastInstance和JobTracker实例。
该示例首先检索数据映射的实例,然后创建Job实例。在进一步讨论API文档时,将讨论用于配置作业的实现。
由于Job类高度依赖于泛型来支持类型安全性,因此泛型随着时间的推移而变化,并且可能与旧变量类型的赋值不兼容。为了充分利用流畅的API,我们建议您使用本示例中所示的流畅方法链接,以防止需要太多变量。 |
IMap<String, String> map = hazelcastInstance.getMap( "articles" );
KeyValueSource<String, String> source = KeyValueSource.fromMap( map );
Job<String, String> job = jobTracker.newJob( source );
ICompletableFuture<Map<String, Long>> future = job
.mapper( new TokenizerMapper() )
.combiner( new WordCountCombinerFactory() )
.reducer( new WordCountReducerFactory() )
.submit();
// Attach a callback listener
future.andThen( buildCallback() );
// Wait and retrieve the result
Map<String, Long> result = future.get();
如上所示,我们创建Job实例并定义mapper,combiner和reducer。然后我们将请求提交给集群。该submit方法返回一个ICompletableFuture,可用于附加我们的回调或等待以阻塞方式处理结果。
有更多选项可用于作业配置,例如定义一般块大小或操作将运行的键。有关更多信息,请参阅Job.java的Hazelcast源代码。
KeyValueSource可以将Hazelcast数据结构(如IMap,MultiMap,IList,ISet)包装到键值对输入源中,也可以构建自己的自定义键值输入源。后一种选择可以为Hazelcast MapReduce提供各种数据,例如即时下载的网页内容或数据文件。熟悉Hadoop的人会认识到与Input类的相似之处。
你可以想象KeyValueSource一个更大的java.util.Iterator实现。尽管必须实现大多数方法,但实现该getAllKeys方法是可选的。如果实现能够预先收集所有密钥,则应该实现并且isAllKeysSupported必须返回true。这样,Job配置的KeyPredicates能够在将密钥发送到集群之前预先评估密钥。否则,它们将被序列化并传输,以便在执行时进行评估。
如上例所示,抽象KeyValueSource类提供了许多静态方法,可以轻松地将Hazelcast数据结构包装到Hazelcast KeyValueSource已经提供的实现中。数据结构的泛型由结果KeyValueSource实例继承。对于IList或ISet等数据结构,密钥类型始终为String。映射时,键是数据结构的名称,而值类型和值本身是从IList或ISet本身继承的。
// KeyValueSource from com.hazelcast.core.IMap
IMap<String, String> map = hazelcastInstance.getMap( "my-map" );
KeyValueSource<String, String> source = KeyValueSource.fromMap( map );
// KeyValueSource from com.hazelcast.core.MultiMap
MultiMap<String, String> multiMap = hazelcastInstance.getMultiMap( "my-multimap" );
KeyValueSource<String, String> source = KeyValueSource.fromMultiMap( multiMap );
// KeyValueSource from com.hazelcast.core.IList
IList<String> list = hazelcastInstance.getList( "my-list" );
KeyValueSource<String, String> source = KeyValueSource.fromList( list );
// KeyValueSource from com.hazelcast.core.ISet
ISet<String> set = hazelcastInstance.getSet( "my-set" );
KeyValueSource<String, String> source = KeyValueSource.fromSet( set );
PartitionIdAware
所述com.hazelcast.mapreduce.PartitionIdAware接口可以通过实现KeyValueSource实施如果基础数据集是意识到Hazelcast划分模式的(因为它是所有内部数据结构)。如果实现此接口,则会KeyValueSource对集群成员上的所有分区多次重复使用同一实例。因此,close和/ open方法也被执行多次,但每个partitionId执行一次。
使用该Mapper接口,您将实现映射逻辑。Mappers可以转换,拆分,计算和汇总数据源中的数据。在Hazelcast中,您还可以通过实现com.hazelcast.core.HazelcastInstanceAware和请求其他地图,多图,列表和/或集合来集成来自多个KeyValueSource数据源的数据。
map在数据结构中每个可用条目调用一次映射器函数。如果您处理以基于分区的方式运行的分布式数据结构,则多个映射器将在成员分配的分区上的不同集群成员上并行工作。然后,映射器准备并可能转换输入键值对,并为还原阶段发出零个或多个键值对。
对于我们的单词计数示例,我们检索输入文档(文本文档),然后通过将文本拆分为可用单词来对其进行转换。之后,如伪代码中所讨论的,我们使用键值对发出每个单词,其中单词为键,1为值。
它的常见实现Mapper可能类似于以下示例:
public class TokenizerMapper implements Mapper<String, String, String, Long> {
private static final Long ONE = Long.valueOf( 1L );
@Override
public void map(String key, String document, Context<String, Long> context) {
StringTokenizer tokenizer = new StringTokenizer( document.toLowerCase() );
while ( tokenizer.hasMoreTokens() ) {
context.emit( tokenizer.nextToken(), ONE );
}
}
}
此代码将映射的文本拆分为其标记,只要有更多标记就会遍历标记生成器,并且每个单词会发出一对。请注意,我们还没有收集同一个单词的多次出现,我们只是单独触发每个单词。
LifecycleMapper / LifecycleMapperAdapter
LifecycleMapper接口或其适配器类LifecycleMapperAdapter可用于使Mapper实现生命周期识别。这意味着当分区或数据集的映射开始以及映射最后一个条目时,将通知它。
只有特殊算法可能需要这些额外的生命周期事件来准备,清理或发出其他值。
如引言中所述,在将映射值从映射器传输到reducer时,使用Combiner来最小化不同集群成员之间的流量。它通过聚合同一个发出的键的多个值来实现。这是一个完全可选的操作,但强烈建议使用它。
可以将组合器视为中间减速器。计算值始终返回到最初为其创建组合器的键。由于组合器是根据发出的密钥创建的,因此Combiner实现本身未在作业配置中定义; 相反,创建了一个能够创建预期的Combiner实例的CombinerFactory。
由于Hazelcast MapReduce并行执行映射和减少阶段,因此Combiner实现必须能够处理分块数据。因此,您必须在呼叫时重置其内部状态finalizeChunk。调用该finalizeChunk方法会创建一大块中间数据进行分组(混洗)并发送给reducer。
组合器可以覆盖beginCombine并finalizeCombine执行准备或清理工作。
对于我们的字数计算示例,我们将有一个类似于以下示例的简单CombinerFactory和Combiner实现。
public class WordCountCombinerFactory
implements CombinerFactory<String, Long, Long> {
@Override
public Combiner<Long, Long> newCombiner( String key ) {
return new WordCountCombiner();
}
private class WordCountCombiner extends Combiner<Long, Long> {
private long sum = 0;
@Override
public void combine( Long value ) {
sum++;
}
@Override
public Long finalizeChunk() {
return sum;
}
@Override
public void reset() {
sum = 0;
}
}
}
组合器必须能够将其当前值作为块返回,并通过设置sum回0来重置内部状态。由于组合器始终从单个线程调用,因此不需要变量的同步或波动。
Reducers做最后一点算法工作。这可以是聚合值,计算平均值或算法预期的任何其他工作。
由于值以块的形式到达,因此reduce对于创建密钥的每个发出值,该方法被多次调用。如果没有为作业配置配置Combiner实现,则每个块也可能发生多次。
与组合器不同,reducer的finalizeReduce方法每个reducer只调用一次(每个键一次)。因此,减速器不需要随时重置其内部状态。
减速器可以覆盖beginReduce以执行准备工作。
对于我们的字数计算示例,实现将类似于以下代码示例。
public class WordCountReducerFactory implements ReducerFactory<String, Long, Long> {
@Override
public Reducer<Long, Long> newReducer( String key ) {
return new WordCountReducer();
}
private class WordCountReducer extends Reducer<Long, Long> {
private volatile long sum = 0;
@Override
public void reduce( Long value ) {
sum += value.longValue();
}
@Override
public Long finalizeReduce() {
return sum;
}
}
}
与组合器不同,如果数据耗尽,reducers会倾向于切换线程,以防止阻塞线程进入JobTracker配置。当要处理的新数据到达时,它们将在稍后重新安排,但不太可能在与之前相同的线程上执行。从Hazelcast版本3.3.3开始,框架确保了新线程内存可见性的保证。这意味着删除了使字段变为volatile的先前要求。
Collator是一个可选操作,在作业发送成员上执行,并且能够在返回到用户的代码库之前修改最终减少的结果。只有特殊用例才有可能使用整理器。
对于一个假想的用例,我们可能想知道在我们分析的文档中有多少单词。对于这种情况,可以将Collator实现赋予submitJob实例的方法。
整理器看起来像以下代码段:
public class WordCountCollator implements Collator<Map.Entry<String, Long>, Long> {
@Override
public Long collate( Iterable<Map.Entry<String, Long>> values ) {
long sum = 0;
for ( Map.Entry<String, Long> entry : values ) {
sum += entry.getValue().longValue();
}
return sum;
}
}
输入类型的定义有点奇怪,但由于Combiner和Reducer实现是可选的,因此输入类型在很大程度上取决于数据的状态。如上所述,collators是非典型的用例,框架的泛型总是有助于找到正确的签名。
您可以使用KeyPredicate预先选择是否应在映射阶段选择密钥进行映射。如果KeyValueSource实现能够在执行之前知道所有密钥,则在不同集群成员之间划分操作之前过滤密钥。
A KeyPredicate还可以用于仅选择特定范围的数据,例如,时间帧,或类似的用例。
KeyPredicate仅映射包含单词“hazelcast”的键的基本实现可能类似于以下代码示例:
public class WordCountKeyPredicate implements KeyPredicate<String> {
@Override
public boolean evaluate( String s ) {
return s != null && s.toLowerCase().contains( "hazelcast" );
}
}
您可以TrackableJob在提交作业后检索实例。JobTracker使用唯一的jobId(per JobTracker)请求它。您可以使用它获取作业的运行时统计信息。可用信息仅限于已处理(映射)的记录数以及不同分区或成员的处理状态(如果KeyValueSource不是PartitionIdAware)。
要在提交作业后检索jobId,请使用com.hazelcast.mapreduce.JobCompletableFuture而不是com.hazelcast.core.ICompletableFuture作为返回的将来的变量类型。
下面的示例代码简要介绍了如何检索实例和运行时数据。有关更多信息,请查看与您运行的Hazelcast版本对应的Javadoc。
该示例执行以下步骤以获取作业实例。
- 它使用hazelcastInstance getMap方法获取地图。
- 从地图中,它使用KeyValueSource fromMap方法获取源。
- 从源头开始,它可以使用JobTracker newJob方法完成工作。
IMap<String, String> map = hazelcastInstance.getMap( "articles" );
KeyValueSource<String, String> source = KeyValueSource.fromMap( map );
Job<String, String> job = jobTracker.newJob( source );
JobCompletableFuture<Map<String, Long>> future = job
.mapper( new TokenizerMapper() )
.combiner( new WordCountCombinerFactory() )
.reducer( new WordCountReducerFactory() )
.submit();
String jobId = future.getJobId();
TrackableJob trackableJob = jobTracker.getTrackableJob(jobId);
JobProcessInformation stats = trackableJob.getJobProcessInformation();
int processedRecords = stats.getProcessedRecords();
log( "ProcessedRecords: " + processedRecords );
JobPartitionState[] partitionStates = stats.getPartitionStates();
for ( JobPartitionState partitionState : partitionStates ) {
log( "PartitionOwner: " + partitionState.getOwner()
+ ", Processing state: " + partitionState.getState().name() );
}
JobProcessInformation的缓存在Java本机客户端上不起作用,因为在检索实例时检索当前值以最小化执行成员和客户端之间的流量。 |
您配置JobTracker配置以设置Hazelcast MapReduce框架的行为。
每个人JobTracker都能够同时运行多个MapReduce作业; 一个配置意味着由同一个配置创建的所有作业的共享资源JobTracker。该配置可以完全控制要使用的预期负载行为和线程计数。
以下代码段显示了典型JobTracker配置。配置属性在示例下面讨论。
<jobtracker name="default">
<max-thread-size>0</max-thread-size>
<!-- Queue size 0 means number of partitions * 2 -->
<queue-size>0</queue-size>
<retry-count>0</retry-count>
<chunk-size>1000</chunk-size>
<communicate-stats>true</communicate-stats>
<topology-changed-strategy>CANCEL_RUNNING_OPERATION</topology-changed-strategy>
</jobtracker>
- max-thread-size: JobTracker的最大线程池大小。
- queue-size:能够等待处理的最大任务数。值0表示无界队列。非常低的数字可能会阻止成功执行,因为作业可能未正确调度或中间块可能丢失。
- retry-count:目前未使用。保留供以后使用,框架将自动尝试从可用保存点重新启动/重试操作。
- chunk-size:块发送给reducers之前的发送值数。如果您的排放值很大,或者您希望更好地平衡工作,则可能需要将其更改为更低或更高的值。值0表示立即传输,但请记住,低值意味着更高的流量成本。如果在发送到reducers之前发出的值不适合堆内存,则非常高的值可能会导致出现OutOfMemoryError。为防止这种情况,您可能希望使用组合器预先减少映射成员的值。
- communic-stats:指定是否将统计信息(例如,有关已处理条目的统计信息)传输到作业发射器。这可以向UI系统内的用户显示进度,但会产生额外的流量。如果不需要,您可能希望停用此功能。
- topology-changed-strategy:指定MapReduce框架在执行作业时如何对拓扑更改做出反应。目前,仅完全支持CANCEL_RUNNING_OPERATION,这会向作业发射器抛出异常(将抛出a com.hazelcast.mapreduce.TopologyChangedException)。
本节介绍MapReduce框架的一些内部结构。这是更高级的信息。如果您对内部工作方式不感兴趣,可能需要跳过本节。
为了理解以下技术内部,我们首先简要介绍一下示例工作流程方面的情况。
举个简单的例子,想一下IMap<String, Integer>具有相同类型的和发出的键。想象一下,您有一个包含三个成员的集群,并在第一个成员上启动MapReduce作业。在您从运行/连接的Hazelcast请求JobTracker之后,我们提交任务并检索ICompletableFuture,这使我们有机会等待计算结果或添加回调(并且更具反应性)。
该示例期望块大小为0或1,因此将发出的值直接发送到reducer。在内部,作业在所有成员上准备,启动和执行,如下所示。第一个成员充当作业所有者(作业发起者)。
Member1 starts MapReduce job
Member1 emits key=Foo, value=1
Member1 does PartitionService::getKeyOwner(Foo) => results in Member3
Member2 emits key=Foo, value=14
Member2 asks jobOwner (Member1) for keyOwner of Foo => results in Member3
Member1 sends chunk for key=Foo to Member3
Member3 receives chunk for key=Foo and looks if there is already a Reducer,
if not creates one for key=Foo
Member3 processes chunk for key=Foo
Member2 sends chunk for key=Foo to Member3
Member3 receives chunk for key=Foo and looks if there is already a Reducer and uses
the previous one
Member3 processes chunk for key=Foo
Member1 send LastChunk information to Member3 because processing local values finished
Member2 emits key=Foo, value=27
Member2 has cached keyOwner of Foo => results in Member3
Member2 sends chunk for key=Foo to Member3
Member3 receives chunk for key=Foo and looks if there is already a Reducer and uses
the previous one
Member3 processes chunk for key=Foo
Member2 send LastChunk information to Member3 because processing local values finished
Member3 finishes reducing for key=Foo
Member1 registers its local partitions are processed
Member2 registers its local partitions are processed
Member1 sees all partitions processed and requests reducing from all members
Member1 merges all reduced results together in a final structure and returns it
流程非常复杂,但非常强大,因为所有内容都是并行执行的。减速器不会等到所有值都被发出,但它们会立即开始减少(当发出的密钥的第一个块到达时)。
从包级别开始,有一个基本包:com.hazelcast.mapreduce。这包括外部API和impl包,它本身包含内部实现。
- 所述IMPL包中包含所有的默认KeyValueSource的实现和抽象基和支持类对已曝光的API。
- 该客户端软件包包含所需要的,当客户端提供的MapReduce作业的客户端和成员双方的所有类。
- 该通知包中包含通知有关业务进展其他成员的所有“通知”或事件类。
- 该操作包中包含用于由工人或作业所有者协调工作,并同步分区或减速处理所有操作。
- 该任务包中包含执行实际精简操作的所有类。它具有管理程序,映射阶段实现以及映射和减少任务。
现在进行技术演练:始终从命名中检索MapReduce作业,该命名JobTracker在NodeJobTracker(扩展AbstractJobTracker)中实现并使用配置DSL进行配置。所有内部实现都完全由ICompletableFuture驱动,并且在设计中大部分都是非阻塞的。
在提交时,作业创建一个唯一的UUID,该UUID后来充当jobId,并与JobTracker的名称组合在一起,可在集群内唯一标识。然后,在集群周围发送准备,每个成员通过创建JobSupervisor,MapCombineTask和ReducerTask来准备其执行。发布作业的JobSupervisor获得了特殊功能,可以在同一作业的其他成员上同步和控制JobSupervisors。
如果在所有成员上完成准备,则通过对每个成员执行StartProcessingJobOperation来启动作业本身。这将启动MappingPhase实现(默认为KeyValueSourceMappingPhase)并启动成员的实际映射。
映射过程当前是每个成员的单线程操作,但是将在未来版本中扩展为在多个分区(每个作业可配置)上并行运行。现在,对分区上的每个可用值调用Mapper,并最终发出值。对于每个发出的值,要么调用已配置的CombinerFactory来创建组合器,要么使用高速缓存的组合(或者使用默认的CollectingCombinerFactory来创建组合器)。当成员达到块限制时,通过将发出的密钥收集到其对应的成员来准备IntermediateChunkNotification。这可以通过要求作业所有者分配成员或通过已经缓存的作业来完成。在更高版本中,PartitionStrategy也可以是可配置的。
然后将IntermediateChunkNotification发送到reducers(仅包含此成员的值)并提供给ReducerTask。在每个商品上,ReducerTask检查它是否已经在运行,如果没有,它会将自己提交给配置的ExecutorService(来自JobTracker配置)。
如果reducer队列失去工作,则从ExecutorService中删除ReducerTask以不阻塞线程,但最终将在下一个工作块上重新提交。
在每个阶段,更改分区状态以跟踪当前运行的操作。JobPartitionState可以处于以下状态之一,具有不言自明的标题:[WAITING, MAPPING, REDUCING, PROCESSED, CANCELLED]。如果您对这些州更感兴趣,请查看Javadoc。
- 会员要求处理新分区:等待⇒映射
- 成员向减速器发出第一个块:映射⇒减少
- 所有成员都表示他们完成了映射阶段并且还原完成了:减少⇒已处理
最终,所有JobPartitionStates都达到PROCESSED状态。然后,作业发起者的JobSupervisor要求所有成员减少结果并执行可能提供的Collator。使用此Collator,在从JobTracker中删除自身,进行最终清理并将结果返回给请求者(使用内部TrackableJobFuture)之前,计算整体结果。
如果在执行时取消作业,则立即将所有分区设置为CANCELED状态,并对所有成员执行CancelJobSupervisorOperation以终止正在运行的进程。
除了默认操作之外还运行了一些操作,还有一些操作,如ProcessStatsUpdateOperation(更新已处理的记录统计信息)或NotifyRemoteExceptionOperation(通知成员发送成员遇到不可恢复的情况,并且需要取消Job,例如,NullPointerException内部对作业所有者执行Mapper以跟踪进程。
本节向Hazelcast用户介绍MapReduce的弃用情况,以及它的动机和替代品。
我们决定在Hazelcast IMDG 3.8中弃用MapReduce框架。MapReduce框架提供了分布式计算模型,它用于支持旧的Aggregations系统。不幸的是,实施没有达到预期并且采用率不高,所以它永远不会超出Beta状态。除了目前的开发从类似M / R的处理转向更接近实时的流式处理方法,我们决定弃用并最终从Hazelcast IMDG中删除MapReduce框架。话虽如此,我们想介绍接班人和替补人员; 在Query基础架构和Hazelcast Jet分布式计算平台之上进行快速聚合。
MapReduce是一个非常强大的工具,但它在空间,时间和带宽方面要求很高。我们意识到,当我们只想找到一个简单的指标,例如匹配谓词的条目数时,我们就不需要那么多的权力。因此,内置聚合在现有Query基础结构(计数,总和,最小值,最大值,均值,方差)之上重建,自动利用任何匹配的查询索引。聚合在这些阶段计算:
- 第一阶段:每个成员(分散)
- 第二阶段:一名成员汇总成员的回复(聚集)
由于第二阶段是单一成员,它不像完整的M / R系统那样灵活,并且在某些用例中输入可能很大。执行第二步的成员需要足够的容量来保存第一步中所有成员的所有中间结果,但实际上,对于许多聚合任务,例如“查找平均值”或“找到最高”以及其他常见示例就足够了。
好处是:
- 改善了性能
- 简化的API
- 利用现有索引
您可以参考快速聚合以获取示例。如果你需要一个像MapReduce这样更强大的工具,那就是Hazelcast Jet!
Hazelcast Jet是在Hazelcast IMDG之上构建的新型分布式计算框架。它使用有向非循环图(DAG)来模拟数据处理流水线中各个步骤之间的关系。从概念上讲,MapReduce模型简单地说明大型数据集上的分布式计算可以归结为两种计算步骤 - 映射步骤和缩减步骤。一对map和reduce对数据进行一级聚合。复杂计算通常需要多个这样的步骤。多个M / R步骤基本上形成操作的DAG,因此DAG执行模型归结为MapReduce模型的推广。因此,总是可以在不进行概念更改的情况下将MapReduce应用程序重写为Hazelcast Jet DAG或“任务管道”。
优点:
- M / R步骤完全隔离(根据定义)。将整个计算建模为DAG,Jet调度程序可以优化操作管道
- Hazelcast Jet提供了方便的高级API(分布式justream)。代码保持紧凑,但也提供了更具体的API,以充分利用DAG的全部功能。
我们将使用“字数”应用程序的示例,该应用程序将一组文档汇总到从每个单词到文档中出现的总数的映射中。这涉及将一个文档转换为单词流的映射阶段和对流执行COUNT DISTINCT操作的缩减阶段,并使用结果填充Hazelcast IMap。
这是MapReduce中的单词计数代码(也可用于hazelcast-jet-code-samples):
JobTracker t = hz.getJobTracker("word-count");
IMap<Long, String> documents = hz.getMap("documents");
LongSumAggregation<String, String> aggr = new LongSumAggregation<>();
Map<String, Long> counts =
t.newJob(KeyValueSource.fromMap(documents))
.mapper((Long x, String document, Context<String, Long> ctx) ->
Stream.of(document.toLowerCase().split("\\W+"))
.filter(w -> !w.isEmpty())
.forEach(w -> ctx.emit(w, 1L)))
.combiner(aggr.getCombinerFactory())
.reducer(aggr.getReducerFactory())
.submit()
.get();
Jet的Core API严格低于MapReduce,因为它可用于构建可驱动MapReduce映射器,组合器和reducer的整个基础架构,完全保留MapReduce作业的语义。但是,这种将代码迁移到Jet的方法并不是一个好的选择,因为MapReduce API强制执行非常不理想的计算模型。最简单的方法是根据Jet的java.util.stream支持(简称Jet JUS)实现这项工作:
IStreamMap<String, String> documents = jet.getMap("documents");
IMap<String, Long> counts = documents
.stream()
.flatMap(m -> Stream.of(m.getValue().toLowerCase().split("\\W+"))
.filter(w -> !w.isEmpty()))
.collect(DistributedCollectors.toIMap(w -> w, w -> 1L, (left, right) -> left + right));
这可以作为一个通用模板来表达JetJUS方面的MapReduce作业:mapper的逻辑在flatMap内部,组合器和reducer的逻辑都在collect内。Jet将自动应用优化,其中数据流首先在本地“合并”在每个成员上,然后在通过网络发送后,最终步骤中的部分结果“减少”。
请记住,MapReduce和java.util.stream使用相同的术语,但含义完全不同:在JUS中,最后一步称为“combine”(MapReduce称之为“reduce”),中间步骤称为“reduce”( MapReduce将此称为“组合”)。MapReduce的“combine”以固定大小的批次折叠流,而在Jet JUS中,“reduce”折叠整个本地数据集,并且每个不同的密钥只发送一个项目到最后一步。在Jet JUS中,最后的“组合”步骤仅将每个成员的一个部分结果合并到总结果中,而在MR中,最后一步将所有每批次项目“减少”到最终结果。因此,
使用Streams API完成的单词计数的完整示例可以在hazelcast-jet-code-samples存储库中找到,该存储库位于WordCount.java文件的“java.util.stream/wordcount-jus”模块中。一个细微的差别是GitHub上的代码逐行存储文档,并在整个集群中实现更细粒度的分布。
为了更好地理解Jet如何执行JUS管道,请查看“core / wordcount”模块中的文件WordCount.java,该模块构建与Jet JUS实现相同的DAG,但使用Jet Core API。以下是此示例中稍微简化的DAG:
DAG dag = new DAG();
Vertex source = dag.newVertex("source", Processors.readMap("documents"))
.localParallelism(1);
Vertex map = dag.newVertex("map", Processors.flatMap(
(String document) -> traverseArray(document.split("\\W+"))));
Vertex reduce = dag.newVertex("reduce", Processors.groupAndAccumulate(
() -> 0L, (count, x) -> count + 1));
Vertex combine = dag.newVertex("combine", Processors.groupAndAccumulate(
Entry::getKey,
() -> 0L,
(Long count, Entry<String, Long> wordAndCount) ->
count + wordAndCount.getValue())
);
Vertex sink = dag.newVertex("sink", writeMap("counts"));
dag.edge(between(source, map))
.edge(between(map, reduce).partitioned(wholeItem(), HASH_CODE))
.edge(between(reduce, combine).partitioned(entryKey()).distributed())
.edge(between(combine, sink));
它是一个简单的顶点级联:source→map→reduce→combine→sink,与MapReduce作业的工作流程非常接近。在每个成员上,源顶点摄取不同的数据片段(本地存储的IMap分区),并将其发送到本地成员上的映射。地图的输出是单词,它们在分区边缘上移动以减少。请注意,与MapReduce相反,处理器的单个实例不计算仅出现一个单词的次数,而是负责整个分区。localParallelism属性只配置了多少个处理器。这是Jet的DAG向用户公开计算的性能关键属性的几个示例之一。
另一个例子可以在传递给partitioned(wholeItem(),HASH_CODE)的参数中看到。用户可以精确控制分区键以及用于将键映射到分区ID的算法。在这种情况下,我们使用整个项(单词)作为键并应用快速HASH_CODE策略,该策略从对象的hashCode()派生分区ID。
reduce→组合边缘是分区和分布的。尽管每个集群成员对于任何给定的单词都有自己的缩减处理器,但对于给定单词,整个集群中只有一个合并处理器。这是网络流量发生的地方:reduce将一个字的本地结果发送到集群中的一个组合处理器。请注意,这里我们没有指定HASH_CODE:它不能保证在分布式边缘上是安全的,因为在目标成员上,哈希码可以以不同方式出现。对于许多值类(如String和Integer),它可以保证工作,因为它们的hashCode显式指定了所使用的函数。默认情况下,Jet应用更慢但更安全的Hazelcast策略:1。序列化,2。计算生成的blob的MurmurHash3。用户应确保更快的策略是安全的,
在上面的例子中,我们可以看到使用了许多开箱即用的处理器:
- readMap从IMap中提取数据
- flatMap对传入的项目执行平面操作(与MapReduce的映射器密切对应)
- groupAndAccumulate执行缩减和组合
Processors类中还有一些。为了获得更大的灵活性,我们现在将展示如何自己实现处理器(也可以在代码示例存储库中使用):
public class MapReduce {
public static void main(String[] args) throws Exception {
Jet.newJetInstance();
JetInstance jet = Jet.newJetInstance();
try {
DAG dag = new DAG();
Vertex source = dag.newVertex("source", readMap("sourceMap"));
Vertex map = dag.newVertex("map", MapP::new);
Vertex reduce = dag.newVertex("reduce", ReduceP::new);
Vertex combine = dag.newVertex("combine", CombineP::new);
Vertex sink = dag.newVertex("sink", writeMap("sinkMap"));
dag.edge(between(source, map))
.edge(between(map, reduce).partitioned(wholeItem(), HASH_CODE))
.edge(between(reduce, combine).partitioned(entryKey()).distributed())
.edge(between(combine, sink.localParallelism(1)));
jet.newJob(dag).execute().get();
} finally {
Jet.shutdownAll();
}
}
private static class MapP extends AbstractProcessor {
private final FlatMapper<Entry<Long, String>, String> flatMapper = flatMapper(
(Entry<Long, String> e) -> new WordTraverser(e.getValue())
);
@Override
protected boolean tryProcess0(@Nonnull Object item) {
return flatMapper.tryProcess((Entry<Long, String>) item);
}
}
private static class WordTraverser implements Traverser<String> {
private final StringTokenizer tokenizer;
WordTraverser(String document) {
this.tokenizer = new StringTokenizer(document.toLowerCase());
}
@Override
public String next() {
return tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
}
}
private static class ReduceP extends AbstractProcessor {
private final Map<String, Long> wordToCount = new HashMap<>();
private final Traverser<Entry<String, Long>> resultTraverser =
lazy(() -> traverseIterable(wordToCount.entrySet()));
@Override
protected boolean tryProcess0(@Nonnull Object item) {
wordToCount.compute((String) item, (x, count) -> 1 + (count != null ? count : 0L));
return true;
}
@Override
public boolean complete() {
return emitCooperatively(resultTraverser);
}
}
private static class CombineP extends AbstractProcessor {
private final Map<String, Long> wordToCount = new HashMap<>();
private final Traverser<Entry<String, Long>> resultTraverser =
lazy(() -> traverseIterable(wordToCount.entrySet()));
@Override
protected boolean tryProcess0(@Nonnull Object item) {
final Entry<String, Long> e = (Entry<String, Long>) item;
wordToCount.compute(e.getKey(),
(x, count) -> e.getValue() + (count != null ? count : 0L));
return true;
}
@Override
public boolean complete() {
return emitCooperatively(resultTraverser);
}
}
}
实现自定义处理器的挑战之一是协作性:只要输出缓冲区(发件箱)中没有空间,它就必须退出。这个例子展示了如何利用在这个较低级别提供的另一种便利线,它可以处理几乎所有相关的机制。一个问题是,必须将简单的for循环转换为有状态迭代器样式的对象,如上面代码中的WordTraverser。为了使这种转换尽可能轻松,我们选择不需要Java Iterator,而是定义了我们自己的Traverser接口,只需要一个方法来实现。这意味着Traverser是一个功能界面,通常可以使用单线程lambda实现。
Hazelcast本身支持对其分布式数据结构内容的聚合操作。它们假设聚合函数是可交换和关联的,它允许采用双层方法,首先聚合本地数据,然后将所有本地子结果发送给一个成员,在那里将它们组合并返回给用户。只要结果具有可管理的大小,这种方法就能很好地工作。许多有趣的聚合产生O(1)结果,对于那些,本机聚合是一个很好的匹配。
本机聚合可能不足的主要区域是按键对数据进行分组并生成大小为O(keyCount)的结果的操作。Hazelcast聚合的体系结构并不能很好地适应这个用例,尽管它仍然适用于中等大小的结果(高达100 MB,作为一个大概的数字)。除了这些数字之外,只要需要多个聚合步骤,Jet就成为首选。在上述用例中,Jet有帮助,因为它不会以序列化形式发送整个哈希表,并且会在用户的计算机上实现所有结果,而是将键值对直接流式传输到目标IMap中。由于它是分布式结构,因此不会将其负载集中在单个成员上。
Jet的DAG范例提供的不仅仅是基本的map-reduce-combine级联。在其他设置中,它可以组成几个这样的级联,并且还以复杂的组合执行共同分组,连接和许多其他操作。
此功能已被弃用。请改用Fast-Aggregations。 |
基于Hazelcast MapReduce框架,聚合器是即用型数据聚合。这些是典型的操作,如求和值,查找最小值或最大值,计算平均值以及您在关系数据库世界中期望的其他操作。
如上所述,聚合操作在MapReduce框架之上实现,并且所有操作都可以使用纯MapReduce调用来实现。但是,使用聚合功能对于大量标准操作更方便。
本节将快速指导您了解Aggregations框架及其一些可用类的基础知识。我们还将实现第一个基本示例。
可以在两种类型的地图界面上使用聚合,com.hazelcast.core.IMap并com.hazelcast .core.MultiMap使用这些aggregate方法。有两种重载方法可用于通过提供自定义配置com.hazelcast.mapreduce.JobTracker实例来自定义底层MapReduce框架的资源管理 。要了解如何配置MapReduce框架,请参阅配置JobTracker。稍后我们将看到另一种配置自动使用的MapReduce框架的方法,如果没有JobTracker提供特殊的。
为了使聚合更方便使用和未来证明,API针对Java 8和未来版本进行了大量优化。该API仍然与Hazelcast支持的任何Java版本(Java 6和Java 7)完全兼容。最大的区别在于您如何使用Java泛型:在Java 6和7上,解决泛型的过程不如Java 8和未来的Java版本强。此外,整个Aggregations API具有完整的Java 8 Project Lambda(或Closure, JSR 335)支持。
为了说明Java 6和Java 7与Java 8的差异,我们将快速浏览两者的代码示例。在此之后,我们将专注于使用Java 8语法来保持示例简洁易懂,我们将看到一些关于Java 6或7中代码的一些提示。
第一个例子将产生int存储在Hazelcast IMap中的一些值的总和。此示例不使用Aggregations框架的大部分功能,但它将显示主要区别。
IMap<String, Integer> personAgeMapping = hazelcastInstance.getMap( "person-age" );
for ( int i = 0; i < 1000; i++ ) {
String lastName = RandomUtil.randomLastName();
int age = RandomUtil.randomAgeBetween( 20, 80 );
personAgeMapping.put( lastName, Integer.valueOf( age ) );
}
通过编写我们的演示数据,我们可以看到如何在不同的Java版本中生成总和。
由于Java 6和7在解析Java 8的泛型方面不够强大,因此您需要对编写的代码更加冗长。您也可以考虑使用原始类型,但要破坏类型安全性以简化此过程。
有关以下代码示例的含义的简短介绍,请查看源代码注释。我们稍后将深入探讨不同的选择。
// No filter applied, select all entries
Supplier<String, Integer, Integer> supplier = Supplier.all();
// Choose the sum aggregation
Aggregation<String, Integer, Integer> aggregation = Aggregations.integerSum();
// Execute the aggregation
int sum = personAgeMapping.aggregate( supplier, aggregation );
使用Java 8,Aggregations API看起来更简单,因为Java 8可以为我们解析通用参数。这意味着Java 6/7示例代码的上述行将最终只在Java 8上的一个简单的行中。
int sum = personAgeMapping.aggregate( Supplier.all(), Aggregations.integerSum() );
如前所述,Aggregations实现基于Hazelcast MapReduce框架,因此您可能会在其API中发现重叠。该aggregate方法的一个重载可以提供a JobTracker,它是MapReduce框架的一部分。
如果您实现自己的聚合,则将使用Aggregations和MapReduce API的混合。如果您这样做,例如,为了让同事的生活更轻松,请阅读实施聚合部分。
我们现在研究使用Aggregations API可以实现的目标。为了研究更深层的例子,让我们快速浏览一下可用的类和接口,并讨论它们的用法。
的com.hazelcast.mapreduce.aggregation.Supplier提供过滤和数据提取到聚合操作。这个类已经提供了一些不同的静态方法来实现最常见的情况。Supplier.all() 接受所有传入值,并且在将它们提供给聚合函数本身之前不对它们应用任何数据提取或转换。
对于过滤数据集,默认情况下有两个不同的选项:
- com.hazelcast.query.Predicate如果要过滤值和/或键,则可以提供a ,或者
- 你可以提供一个com.hazelcast.mapreduce.KeyPredicate,如果你能在关键数据直接决定,而不需要反序列化值。
如上所述,所有API都完全兼容Java 8和Lambda。我们来看看如何使用这两个选项进行基本过滤。
首先,我们看看a KeyPredicate,我们只接受姓氏为“琼斯”的人。
Supplier<...> supplier = Supplier.fromKeyPredicate(
lastName -> "Jones".equalsIgnoreCase( lastName )
);
class JonesKeyPredicate implements KeyPredicate<String> {
public boolean evaluate( String key ) {
return "Jones".equalsIgnoreCase( key );
}
}
使用标准Hazelcast Predicate接口,我们还可以根据数据条目的值进行过滤。在以下示例中,您只能选择可被4整除的值,而不包含余数。
Supplier<...> supplier = Supplier.fromPredicate(
entry -> entry.getValue() % 4 == 0
);
class DivisiblePredicate implements Predicate<String, Integer> {
public boolean apply( Map.Entry<String, Integer> entry ) {
return entry.getValue() % 4 == 0;
}
}
除了过滤之外,Supplier还可以在将数据提供给聚合操作本身之前提取或转换数据。以下示例显示如何将输入值转换为字符串。
Supplier<String, Integer, String> supplier = Supplier.all(
value -> Integer.toString(value)
);
您可以在Aggregations Examples部分中看到Java 6/7示例。
除了我们将type int(或Integer)的输入值转换为字符串之外,我们还可以看到结果的一般信息Supplier也发生了变化。这表明我们现在有一个聚合字符串值。
另一个特点Supplier是它能够链接多个过滤规则。让我们将所有上述示例合并为一个规则集:
Supplier<String, Integer, String> supplier =
Supplier.fromKeyPredicate(
lastName -> "Jones".equalsIgnoreCase( lastName ),
Supplier.fromPredicate(
entry -> entry.getValue() % 4 == 0,
Supplier.all( value -> Integer.toString(value) )
)
);
您可能更喜欢或需要Supplier根据特殊要求实施您的。这是一项非常基本的任务。该Supplier抽象类只有一个方法:该apply方法。
由于Java Lambda API的限制,您无法使用Lambdas实现抽象类。相反,建议您创建一个标准的命名类。 |
class MyCustomSupplier extends Supplier<String, Integer, String> {
public String apply( Map.Entry<String, Integer> entry ) {
Integer value = entry.getValue();
if (value == null) {
return null;
}
return value % 4 == 0 ? String.valueOf( value ) : null;
}
}
该Supplier apply方法有望返回null每当输入值不应该被映射到聚集过程。如上例所示,这可以用于直接实现过滤规则。使用KeyPredicate和Predicate接口实现过滤器 可能更方便。
要使用您自己的Supplier,只需将其传递给聚合方法或与其他“供应商”结合使用。
int sum = personAgeMapping.aggregate( new MyCustomSupplier(), Aggregations.count() );
Supplier<String, Integer, String> supplier =
Supplier.fromKeyPredicate(
lastName -> "Jones".equalsIgnoreCase( lastName ),
new MyCustomSupplier()
);
int sum = personAgeMapping.aggregate( supplier, Aggregations.count() );
该com.hazelcast.mapreduce.aggregation.Aggregation接口定义了聚合操作本身。它包含了一系列的MapReduce API实现类似的Mapper,Combiner,Reducer,和Collator。这些实现通常是所选择的唯一Aggregation。此接口也可以使用基于MapReduce调用的聚合操作来实现。有关更多信息,请参阅实现聚合部分。
的com.hazelcast.mapreduce.aggregation.Aggregations类提供一个共同的预定的一组聚合的。此类包含以下类型的类型安全聚合:
- 平均值(整数,长整数,双精度,BigInteger,BigDecimal)
- Sum(整数,长整数,双精度,BigInteger,BigDecimal)
- Min(整数,长整数,双整数,BigInteger,BigDecimal,可比)
- Max(整数,长整数,双整数,BigInteger,BigDecimal,可比较)
- DistinctValues
- 计数
这些聚合与关系数据库中的对应类似,可以等同于下面列出的SQL语句。
平均:
根据所有选定的值计算平均值。
map.aggregate( Supplier.all( person -> person.getAge() ),
Aggregations.integerAvg() );
SELECT AVG(person.age) FROM person;
和:
根据所有选定的值计算总和。
map.aggregate( Supplier.all( person -> person.getAge() ),
Aggregations.integerSum() );
SELECT SUM(person.age) FROM person;
最小(最小):
查找所有选定值的最小值。
map.aggregate( Supplier.all( person -> person.getAge() ),
Aggregations.integerMin() );
SELECT MIN(person.age) FROM person;
最大(最大):
查找所有选定值的最大值。
map.aggregate( Supplier.all( person -> person.getAge() ),
Aggregations.integerMax() );
SELECT MAX(person.age) FROM person;
不同的价值观:
返回所选值的不同值的集合
map.aggregate( Supplier.all( person -> person.getAge() ),
Aggregations.distinctValues() );
SELECT DISTINCT person.age FROM person;
计数:
返回所有选定值的元素计数
map.aggregate( Supplier.all(), Aggregations.count() );
SELECT COUNT(*) FROM person;
我们使用com.hazelcast.mapreduce.aggregation.PropertyExtractor的时候我们有一个看看如何使用这个例子之前接口Supplier到一个值转换为另一种类型。它还可用于从值中提取属性。
class Person {
private String firstName;
private String lastName;
private int age;
// getters and setters
}
PropertyExtractor<Person, Integer> propertyExtractor = (person) -> person.getAge();
class AgeExtractor implements PropertyExtractor<Person, Integer> {
public Integer extract( Person value ) {
return value.getAge();
}
}
在此示例中,我们从person的age属性中提取值。值类型从Person更改Integer为反映在泛型信息中以保持类型安全。
您可以使用`PropertyExtractor`s进行任何类型的数据转换。您甚至可能希望一个接一个地链接多个转换步骤。
如前所述,配置底层MapReduce框架使用的资源的最简单方法是JobTracker 通过将聚合调用传递给其中一个IMap.aggregate()或来提供聚合调用MultiMap.aggregate()。
还有另一种隐式配置底层使用方法JobTracker。如果没有JobTracker为聚合调用传递特定内容,则将使用以下命名规范在内部创建一个:
对于IMap聚合调用,命名规范创建为:
- hz::aggregation-map- 和地图的连接名称。
因为MultiMap它非常相似:
- hz::aggregation-multimap- 和MultiMap的串联名称。
知道了名称的规范,我们可以使用我们刚学到的命名规范来配置JobTracker预期(如检索JobTracker实例中所述)。有关配置的更多信息 JobTracker,请参阅配置Jobtracker。
要完成此部分,让我们快速举例说明上述命名规范:
IMap<String, Integer> map = hazelcastInstance.getMap( "mymap" );
// The internal JobTracker name resolves to 'hz::aggregation-map-mymap'
map.aggregate( ... );
MultiMap<String, Integer> multimap = hazelcastInstance.getMultiMap( "mymultimap" );
// The internal JobTracker name resolves to 'hz::aggregation-multimap-mymultimap'
multimap.aggregate( ... );
最后一个例子,假设您正在为一家国际公司工作,并且您在IMap全球所有员工中都有一个存储在Hazelcast中的员工数据库 MultiMap,并将员工分配到他们的某些地点或办公室。此外,还有另一个IMap持有每位员工的工资。
我们来看看我们的数据模型。
class Employee implements Serializable {
private String firstName;
private String lastName;
private String companyName;
private String address;
private String city;
private String county;
private String state;
private int zip;
private String phone1;
private String phone2;
private String email;
private String web;
// getters and setters
}
class SalaryMonth implements Serializable {
private Month month;
private int salary;
// getters and setters
}
class SalaryYear implements Serializable {
private String email;
private int year;
private List<SalaryMonth> months;
// getters and setters
public int getAnnualSalary() {
int sum = 0;
for ( SalaryMonth salaryMonth : getMonths() ) {
sum += salaryMonth.getSalary();
}
return sum;
}
}
这两个IMap`s and the `MultiMap是由电子邮件串密钥。它们的定义如下:
IMap<String, Employee> employees = hz.getMap( "employees" );
IMap<String, SalaryYear> salaries = hz.getMap( "salaries" );
MultiMap<String, String> officeAssignment = hz.getMultiMap( "office-employee" );
到目前为止,我们知道所有重要的信息来制定一些示例聚合。我们将研究一些更深入的实现细节以及我们如何解决将来在API版本中消除的一些当前限制。
让我们从一个非常基本的例子开始。我们想知道所有员工的平均工资。为此,我们需要a PropertyExtractor和类型的平均聚合Integer。
IMap<String, SalaryYear> salaries = hazelcastInstance.getMap( "salaries" );
PropertyExtractor<SalaryYear, Integer> extractor =
(salaryYear) -> salaryYear.getAnnualSalary();
int avgSalary = salaries.aggregate( Supplier.all( extractor ),
Aggregations.integerAvg() );
而已。在内部,我们基于预定义的聚合创建了MapReduce任务,并立即将其启动。目前,所有聚合调用都是阻塞操作,因此无法以被动方式(使用com.hazelcast.core.ICompletableFuture)执行聚合,但这将是即将发布的版本的一部分。
以下示例稍微复杂一些。我们只希望将我们在美国的员工选入平均工资计算,因此我们需要在员工和工资地图之间执行联接操作。
class USEmployeeFilter implements KeyPredicate<String>, HazelcastInstanceAware {
private transient HazelcastInstance hazelcastInstance;
public void setHazelcastInstance( HazelcastInstance hazelcastInstance ) {
this.hazelcastInstance = hazelcastInstance;
}
public boolean evaluate( String email ) {
IMap<String, Employee> employees = hazelcastInstance.getMap( "employees" );
Employee employee = employees.get( email );
return "US".equals( employee.getCountry() );
}
}
使用该HazelcastInstanceAware接口,我们将当前的Hazelcast实例注入到我们的过滤器中,并且我们可以在群集的其他数据结构上执行数据连接。我们现在只选择在美国办事处工作的员工加入汇总。
IMap<String, SalaryYear> salaries = hazelcastInstance.getMap( "salaries" );
PropertyExtractor<SalaryYear, Integer> extractor =
(salaryYear) -> salaryYear.getAnnualSalary();
int avgSalary = salaries.aggregate( Supplier.fromKeyPredicate(
new USEmployeeFilter(), extractor
), Aggregations.integerAvg() );
对于我们的下一个示例,我们将根据不同的全球办事处进行一些分组。目前,组聚合器尚不可用,因此我们需要一个小的解决方法来实现此目标。(在Aggregations API的更高版本中,这不是必需的,因为它将以更方便的方式提供开箱即用。)
再次,让我们从我们的过滤器开始。这次,我们想要根据办公室名称进行过滤,我们需要做一些数据连接来实现这种过滤。
简短提示:为了最大限度地减少聚合上的数据传输,我们可以使用 数据相关性规则来影响数据的分区。请注意,这是Hazelcast的专家功能。
class OfficeEmployeeFilter implements KeyPredicate<String>, HazelcastInstanceAware {
private transient HazelcastInstance hazelcastInstance;
private String office;
// Deserialization Constructor
public OfficeEmployeeFilter() {
}
public OfficeEmployeeFilter( String office ) {
this.office = office;
}
public void setHazelcastInstance( HazelcastInstance hazelcastInstance ) {
this.hazelcastInstance = hazelcastInstance;
}
public boolean evaluate( String email ) {
MultiMap<String, String> officeAssignment = hazelcastInstance
.getMultiMap( "office-employee" );
return officeAssignment.containsEntry( office, email );
}
}
现在我们可以执行我们的聚合。如前所述,我们目前需要通过连续执行多个聚合来自行进行分组。
Map<String, Integer> avgSalariesPerOffice = new HashMap<String, Integer>();
IMap<String, SalaryYear> salaries = hazelcastInstance.getMap( "salaries" );
MultiMap<String, String> officeAssignment =
hazelcastInstance.getMultiMap( "office-employee" );
PropertyExtractor<SalaryYear, Integer> extractor =
(salaryYear) -> salaryYear.getAnnualSalary();
for ( String office : officeAssignment.keySet() ) {
OfficeEmployeeFilter filter = new OfficeEmployeeFilter( office );
int avgSalary = salaries.aggregate( Supplier.fromKeyPredicate( filter, extractor ),
Aggregations.integerAvg() );
avgSalariesPerOffice.put( office, avgSalary );
}
我们希望通过执行一个最终的简单聚合来结束本节。我们想知道我们目前在全球拥有多少员工。在阅读下一行示例代码之前,您可以尝试自己完成,看看您是否了解如何执行聚合。
IMap<String, Employee> employees = hazelcastInstance.getMap( "employees" );
int count = employees.size();
好的,在前两个代码行的快速笑话之后,我们看看真正的两个代码行:
IMap<String, Employee> employees = hazelcastInstance.getMap( "employees" );
int count = employees.aggregate( Supplier.all(), Aggregations.count() );
我们现在概述了如何在现实生活中使用聚合。如果您想帮助同事,您可能需要编写自己的其他聚合集。如果是,请阅读下一节“ 实现聚合”。
本节介绍如何在自己的应用程序中实现自己的聚合。这是一个高级部分,因此如果您不打算实现自己的聚合,您可能希望在此处停止阅读,并在以后需要知道如何实现自己的聚合时返回。
一个Aggregation实现定义的MapReduce任务的,但有一个小的区别:Mapper 总是希望在一个工作Supplier是过滤器和/或转换映射的输入值的一些输出值。
用于进行自己聚合的主界面是com.hazelcast.mapreduce.aggregation.Aggregation。它由四种方法组成。
interface Aggregation<Key, Supplied, Result> {
Mapper getMapper(Supplier<Key, ?, Supplied> supplier);
CombinerFactory getCombinerFactory();
ReducerFactory getReducerFactory();
Collator<Map.Entry, Result> getCollator();
}
该getMapper和getReducerFactory方法应返回非空值。getCombinerFactory并且getCollator是可选操作,您不需要实现它们。您可以根据要实现的用例决定实现它们。
快速聚合功能的继任聚合器。它们在大多数用例中等同于MapReduce聚合器,但它们不是在MapReduce引擎上运行,而是在Query基础结构上运行。它们的性能要好几十到几百倍,因为它们并行运行每个分区,并且针对速度和低内存消耗进行了高度优化。
快速聚合包括由三种方法表示的三个阶段:
- accumulate(),
- combine(),
- aggregate()。
还有两个回调:
- onAccumulationFinished() 在累积阶段结束时调用。
- onCombinationFinished() 在组合阶段结束时调用。
这些回调可以释放可能已初始化并存储在聚合器中的状态 - 以减少网络流量。
下面介绍了每个阶段,您还可以参考Aggregator Javadoc获取API的详细信息。
积累:
在累积阶段,每个聚合器累积查询引擎传递给它的所有条目。它仅累积在最后阶段计算聚合结果所需的那些信息 - 这是特定于实现的。
在DoubleAverage聚合的情况下,聚合器将累积:
- 它积累的元素的总和
- 它积累的元素的数量
组合:
由于快速聚合在群集的每个分区上并行执行,因此需要在累积阶段之后组合结果,以便能够计算最终结果。
在DoubleAverage聚合的情况下,聚合器将总和元素和所有计数的所有总和。
聚合:
聚合是计算最后结果的最后阶段,这些结果是在前面阶段累积和合并的结果。
在DoubleAverage聚合的情况下,聚合器只是将元素的总和除以它们的计数(如果非零)。
快速聚合仅适用于com.hazelcast.core.IMap。IMap提供了aggregate在地图条目上应用聚合逻辑的方法。可以使用或不使用谓词来调用此方法。您可以参考其Javadoc以查看方法详细信息。
以下是Aggregator的示例实现:
public class DoubleAverageAggregator<I> extends AbstractAggregator<I, Double> {
private double sum;
private long count;
public DoubleAverageAggregator() {
super();
}
public DoubleAverageAggregator(String attributePath) {
super(attributePath);
}
@Override
public void accumulate(I entry) {
count++;
Double extractedValue = (Double) extract(entry);
sum += extractedValue;
}
@Override
public void combine(Aggregator aggregator) {
DoubleAverageAggregator doubleAverageAggregator = (DoubleAverageAggregator) aggregator;
this.sum += doubleAverageAggregator.sum;
this.count += doubleAverageAggregator.count;
}
@Override
public Double aggregate() {
if (count == 0) {
return null;
}
return (sum / (double) count);
}
}
如你看到的:
- 该accumulate()方法计算元素的总和和计数。
- 该combine()方法结合了所有累积的结果。
- 该aggregate()方法计算最终结果。
本com.hazelcast.aggregation.Aggregators类提供了多种内置的聚合器。完整清单如下:
- 计数
- 不同
- bigDecimal sum / avg / min / max
- bigInteger sum / avg / min / max
- 双倍数/平均/分/最大值
- 整数和/平均/最小/最大
- 长和/平均/分/最大
- 号码平均
- 相当的最小/最大
- fixedPointSum,floatingPointSum
要使用任何这些聚合器,请使用Aggregators工厂类实例化它们。
每个内置聚合器也可以导航到传递给accumulate()方法的对象的属性(通过反射)。例如,Aggregators.distinct("address.city")将从address.city传递给聚合器的对象中提取属性并累积提取的值。
在每个分区上,在条目传递给聚合器之后,累积并行运行。这意味着克隆每个聚合器并接收从分区接收的条目的子集。然后,它在所有克隆聚合器中运行累积阶段 - 最后,结果合并为单个累积结果。它将处理速度提高了至少两倍 - 即使在简单聚合的情况下也是如此。如果累积逻辑更“重”,则加速可能更重要。
要将累积切换为顺序模式,只需将hazelcast.aggregation.accumulation.parallel.evaluation属性false设置为true(默认情况下设置为)。
在某些情况下,您希望转换(剥离)每个结果对象,以避免冗余网络流量,而不是从成员发送查询返回的所有数据。
例如,您根据某些条件选择所有员工,但您只想返回其名称而不是整个Employee对象。使用Projection API很容易实现。
Projection API提供了transform()在每个结果对象上调用的方法。然后将其结果收集为最终查询结果实体。您可以参考Projection Javadoc获取API的详细信息。
预测仅适用于com.hazelcast.core.IMap。IMap提供了project在地图条目上应用投影逻辑的方法。可以使用或不使用谓词来调用此方法。您可以参考其Javadoc以查看方法详细信息。
让我们考虑以下域对象存储在IMap中:
public class Employee implements Serializable {
private String name;
public Employee() {
}
public String getName() {
return name;
}
public void setName(String firstName) {
this.name = name;
}
}
要仅返回Employees的名称,可以按以下方式运行查询:
Collection<String> names = employees.project(new Projection<Map.Entry<String, Employee>, String>() {
@Override
public String transform(Map.Entry<String, Employee> entry) {
return entry.getValue().getName();
}
}, somePredicate);
该com.hazelcast.projection.Projections课程提供两个内置投影:
- singleAttribute
- 多属性
的singleAttribute投影使得能够提取的对象(通过反射)的单个属性。例如,Projection.singleAttribute("address.city")将从address.city传递给Projection的对象中提取属性。
该multiAttribute投影能使提取倍数从物体(经由反射)属性。例如,Projection.multiAttribute("address.city", "postalAddress.city")将从传递给Projection的对象中提取这两个属性,并将它们返回到Object[]数组中。
连续查询缓存用于缓存连续查询的结果。构建连续查询缓存后,底层的所有更改都会IMap立即作为事件流反映到此缓存中。因此,此缓存将始终是最新的视图IMap。您可以在客户端或成员上创建连续查询缓存。
当您需要以IMap非常频繁和快速的方式查询分布式数据时,连续查询缓存非常有用。通过使用连续查询缓存,查询的结果将始终准备好并且应用程序是本地的。
以下代码段显示了如何从成员访问连续查询缓存。
QueryCacheConfig queryCacheConfig = new QueryCacheConfig("cache-name");
queryCacheConfig.getPredicateConfig().setImplementation(new OddKeysPredicate());
MapConfig mapConfig = new MapConfig("map-name");
mapConfig.addQueryCacheConfig(queryCacheConfig);
Config config = new Config();
config.addMapConfig(mapConfig);
HazelcastInstance node = Hazelcast.newHazelcastInstance(config);
IEnterpriseMap<Integer, String> map = (IEnterpriseMap) node.getMap("map-name");
QueryCache<Integer, String> cache = map.getQueryCache("cache-name");
以下代码段显示了如何从客户端访问连续查询缓存。此代码与上面的成员端代码的不同之处在于您配置并实例化客户端实例而不是成员实例。
QueryCacheConfig queryCacheConfig = new QueryCacheConfig("cache-name");
queryCacheConfig.getPredicateConfig().setImplementation(new OddKeysPredicate());
ClientConfig clientConfig = new ClientConfig();
clientConfig.addQueryCacheConfig("map-name", queryCacheConfig);
HazelcastInstance client = HazelcastClient.newHazelcastClient(clientConfig);
IEnterpriseMap<Integer, Integer> clientMap = (IEnterpriseMap) client.getMap("map-name");
QueryCache<Integer, Integer> cache = clientMap.getQueryCache("cache-name");
连续查询缓存的以下功能对成员和客户端都有效。
- IMap可以根据提供的谓词via启用/禁用在连续查询缓存构造期间对现有数据运行的初始查询QueryCacheConfig.setPopulate()。
- 连续查询缓存允许您使用索引运行查询,并执行事件批处理和合并。
- 连续查询缓存是可驱逐的。请注意,连续查询缓存的默认最大容量为10000.如果需要不可逐出的缓存,则应配置驱逐QueryCacheConfig.setEvictionConfig()。
- 可以使用侦听器将侦听器添加到连续查询缓存中QueryCache.addEntryListener()。
- IMap事件以与在映射条目上生成的顺序相同的顺序反映在连续查询缓存中。由于事件是在存储在分区中的条目上创建的,因此根据分区内的顺序维护事件的顺序。您可以添加侦听器以捕获丢失的事件,EventLostListener并使用该方法恢复丢失的事件QueryCache.tryRecover()。丢失事件的恢复在很大程度上取决于Hazelcast成员上缓冲区的大小。每个分区的默认缓冲区大小为16; 即每个分区可以在缓冲区中维护16个事件。如果事件生成很高,则将缓冲区大小设置为更高的数字将提供更好的恢复丢失事件的机会。您可以使用设置缓冲区大小QueryCacheConfig.setBufferSize()。您可以将以下示例代码用于恢复案例。
- QueryCache queryCache = map.getQueryCache("cache-name", new SqlPredicate("this > 20"), true);
- queryCache.addEntryListener(new EventLostListener() {
- @Override
- public void eventLost(EventLostEvent event) {
- queryCache.tryRecover();
- }
}, false);
- 您可以仅使用其条目的键填充连续查询缓存,并直接QueryCache.get()从底层检索后续值IMap。这有助于在值非常大时减少初始填充时间。
您可以以声明方式或编程方式配置连续查询缓存; 后者主要在前一节中解释过。父配置元素<query-caches>应放在您的<map>配置中。您可以使用下面的<query-cache>子元素创建查询缓存 <query-caches>。
以下是声明性配置示例。
<map>
<query-caches>
<query-cache name="myContQueryCache">
<include-value>true</include-value>
<predicate type="class-name">com.hazelcast.examples.ExamplePredicate</predicate>
<entry-listeners>
<entry-listener>...</entry-listener>
</entry-listeners>
<in-memory-format>BINARY</in-memory-format>
<populate>true</populate>
<coalesce>false</coalesce>
<batch-size>2</batch-size>
<delay-seconds>3</delay-seconds>
<buffer-size>32</buffer-size>
<eviction size="1000" max-size-policy="ENTRY_COUNT" eviction-policy="LFU"/>
<indexes>
<index ordered="true">...</index>
</indexes>
</query-cache>
</query-caches>
</map>
以下是配置元素和属性的描述:
- name:连续查询缓存的名称。
- <include-value>:指定是否也将缓存该值。其默认值为true。
- <predicate>:谓词来过滤将应用于查询缓存的事件。
- <entry-listeners>:为查询缓存条目添加侦听器(侦听器类)。请参阅“ 注册地图监听器”部分。
- <in-memory-format>:要存储在查询缓存中的数据类型。请参阅设置内存格式部分。它的默认值是BINARY。
- <populate>:指定是否启用查询缓存的初始填充。其默认值为true。
- <coalesce>:指定是否启用查询缓存的合并。其默认值为false。
- <delay-seconds>:事件在成员缓冲区中等待的最短时间(以秒为单位)。其默认值为0。
- <batch-size>:批处理大小用于确定批处理中发送到查询缓存的事件数。其默认值为1。
- <buffer-size>:可以存储在分区缓冲区中的最大事件数。其默认值为16。
- <eviction>:用于驱逐查询缓存的配置。请参阅配置地图驱逐部分。
- <indexes>:使用此元素的<index>子元素定义的查询缓存的索引。请参阅配置IMap索引部分。
请考虑以下配置注意事项和发布逻辑:
如果 <delay-seconds>等于或小于0,则<batch-size>失去其功能。每次有事件时,缓冲区中的所有条目都被推送到订户。
如果<delay-seconds>大于0,则适用以下逻辑:
- 如果<coalesce>设置为true,则检查缓冲区是否具有相同的键; 如果是这样,它将被当前事件覆盖。然后:
- 检查缓冲区的当前大小:如果缓冲区的当前大小等于或大于<batch-size>,则将计数的事件<batch-size>推送到订户。否则,不会发送任何事件。
- 完成检查后<batch-size>,<delay-seconds>检查。从最旧到最年轻的条目扫描缓冲区; 所有早于旧的条目<delay-seconds>都被推送到订阅者。