如概述部分所述,Hazelcast提供Java接口的分布式实现。以下是这些实现的列表,其中包含指向本手册中相应部分的链接。
- 标准实用程序集合
- Map是分布式实现的java.util.Map。它可以让你从读取和写入到Hazelcast地图方法,如get和put。
- 队列是分布式实现的java.util.concurrent.BlockingQueue。您可以在一个成员中添加项目,然后将其从另一个成员中删除。
- Ringbuffer用于可靠的事件系统。
- Set是分布式和并发的实现java.util.Set。它不允许重复元素,也不保留其顺序。
- 列表类似于Hazelcast Set。唯一的区别是它允许重复元素并保留它们的顺序。
- Multimap是一个专门的Hazelcast地图。它是一种分布式数据结构,您可以在其中存储单个键的多个值。
- 复制的映射不会对数据进行分区。它不会将数据传播到不同的集群成员。相反,它将数据复制到所有成员。
- Cardinality Estimator是一种实现Flajolet的HyperLogLog算法的数据结构。
- 主题是用于发布传递给多个订阅者的消息的分布式机制。它也称为发布/订阅(发布/订阅)消息传递模型。有关更多信息,请参阅主题部分。Hazelcast还有一个名为Reliable Topic的结构,它使用与Hazelcast Topic相同的界面。不同之处在于它由Ringbuffer数据结构支持。请参阅可靠主题部分。
- 并发实用程序
- Lock是分布式实现的java.util.concurrent.locks.Lock。当您使用锁时,Hazelcast Lock保护的关键部分保证只能由整个集群中的一个线程执行。
- ISemaphore是分布式实现的java.util.concurrent.Semaphore。执行并发活动时,信号量提供控制线程计数的许可。
- IAtomicLong是分布式实现的java.util.concurrent.atomic.AtomicLong。AtomicLong的大多数操作都可用。然而,这些操作涉及远程呼叫,因此由于分发,它们的性能与AtomicLong不同。
- IAtomicReference是分布式实现的java.util.concurrent.atomic.AtomicReference。当您需要在分布式环境中处理引用时,可以使用Hazelcast IAtomicReference。
- IdGenerator用于生成群集范围的唯一标识符。ID生成几乎以速度发生AtomicLong.incrementAndGet()。不推荐使用此功能,请改用FlakeIdGenerator。
- ICountdownLatch是分布式实现的java.util.concurrent.CountDownLatch。Hazelcast CountDownLatch是并发活动的守门员。它使线程能够等待其他线程完成其操作。
- PN计数器是一种分布式数据结构,其中每个Hazelcast实例可以递增和递减计数器值,并且这些更新将传播到所有副本。
- Event Journal是一种分布式数据结构,用于在地图或缓存上存储变异操作的历史记录。
Hazelcast在分区策略方面有两种类型的分布式对象:
- 每个分区存储实例的一部分的数据结构,即分区数据结构。
- 单个分区存储整个实例的数据结构,即非分区数据结构。
分区Hazelcast数据结构是:
- 地图
- 多重映射
- 缓存(Hazelcast JCache实现)
- PN计数器
- 活动期刊
非分区Hazelcast数据结构是:
- 队列
- 组
- 名单
- Ringbuffer
- 锁
- ISemaphore
- IAtomicLong
- IAtomicReference
- FlakeIdGenerator
- ICountdownLatch
- 基数估算器
除此之外,Hazelcast还提供了复制地图结构,如上面的标准实用程序集合列表中所述。
Hazelcast get为大多数分布式对象提供了一种方法。要加载对象,首先要创建一个Hazelcast实例,然后get在此实例上使用相关方法。以下示例代码段在此实例上创建Hazelcast实例和地图。
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
Map<Integer, String> customers = hazelcastInstance.getMap( "customers" );
至于分布式对象的配置,Hazelcast使用hazelcast.xmlHazelcast下载文件中的默认设置。当然,您可以在此XML中提供显式配置,也可以根据需要以编程方式提供。请参阅“ 了解配置”部分。
请注意,Hazelcast的大多数分布式对象都是懒惰创建的,即一旦第一个操作访问它就会创建分布式对象。
如果要使用在其他位置加载的对象,可以使用其引用安全地重新加载它,而无需创建新的Hazelcast实例(customers在上面的示例中)。
要销毁Hazelcast分布式对象,可以使用该方法destroy。此方法清除并释放对象的所有资源。因此,必须小心使用它,因为在销毁对象后使用相同的对象引用重新加载会创建一个没有错误的新数据结构。请参阅以下示例代码,其中一个队列被销毁,另一个队列被访问。
public class Member {
public static void main(String[] args) throws Exception {
HazelcastInstance hz1 = Hazelcast.newHazelcastInstance();
HazelcastInstance hz2 = Hazelcast.newHazelcastInstance();
IQueue<String> q1 = hz1.getQueue("q");
IQueue<String> q2 = hz2.getQueue("q");
q1.add("foo");
System.out.println("q1.size: "+q1.size()+ " q2.size:"+q2.size());
q1.destroy();
System.out.println("q1.size: "+q1.size() + " q2.size:"+q2.size());
}
}
如果你开始Member上面的操作,输出将如下所示:
q1.size: 1 q2.size:1
q1.size: 0 q2.size:0
如您所见,不会生成错误并创建新的队列资源。
Hazelcast使用分布式对象的名称来确定将放置哪个分区。让我们加载两个信号量,如下所示:
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
ISemaphore s1 = hazelcastInstance.getSemaphore("s1");
ISemaphore s2 = hazelcastInstance.getSemaphore("s2");
由于这些信号量具有不同的名称,因此它们将被放置在不同的分区中。如果要将这两个放在同一个分区中,可以使用@如下所示的符号:
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
ISemaphore s1 = hazelcastInstance.getSemaphore("s1@foo");
ISemaphore s2 = hazelcastInstance.getSemaphore("s2@foo");
现在,这两个信号量将被放入分区键所在的同一分区foo。请注意,您可以使用该方法getPartitionKey来学习分布式对象的分区键。当您想要在现有对象的同一分区中创建对象时,它可能很有用。请查看其用法,如下所示:
String partitionKey = s1.getPartitionKey();
ISemaphore s3 = hazelcastInstance.getSemaphore("s3@"+partitionKey);
- 如果成员发生故障,其备份副本(包含相同数据)将动态地将数据(包括所有权和锁定)重新分发给剩余的实时成员。因此,不会有任何数据丢失。
- 没有单个集群主机可以是单点故障。集群中的每个成员都享有平等的权利和责任。没有一个成员优越。不依赖于外部“服务器”或“主”。
下面是一个示例,说明如何检索现有数据结构实例(映射,队列,集,锁,主题等)以及如何侦听实例事件,例如正在创建或销毁的实例。
public class Sample implements DistributedObjectListener {
public static void main(String[] args) {
Sample sample = new Sample();
Config config = new Config();
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
hazelcastInstance.addDistributedObjectListener(sample);
Collection<DistributedObject> distributedObjects = hazelcastInstance.getDistributedObjects();
for (DistributedObject distributedObject : distributedObjects) {
System.out.println(distributedObject.getName());
}
}
@Override
public void distributedObjectCreated(DistributedObjectEvent event) {
DistributedObject instance = event.getDistributedObject();
System.out.println("Created " + instance.getName());
}
@Override
public void distributedObjectDestroyed(DistributedObjectEvent event) {
DistributedObject instance = event.getDistributedObject();
System.out.println("Destroyed " + instance.getName());
}
}
Hazelcast Map(IMap)扩展了界面java.util.concurrent.ConcurrentMap,因此java.util.Map。它是Java map的分布式实现。您可以使用众所周知的get和put方法执行从Hazelcast地图读取和写入操作等操作。
Hazelcast Jet 还可以使用IMap数据结构进行实时流处理(通过在地图上启用事件日志)和快速批处理。Hazelcast Jet使用IMap作为源(从IMap读取数据)和作为接收器(将数据写入IMap)。请参阅Hazelcast Jet 的快速批处理和实时流处理用例。另请参阅这里的Hazelcast喷气参考手册中学习如何喷气机使用IMAP,即它如何从读取和写入IMAP。 |
Hazelcast将对您的地图条目及其备份进行分区,并将它们几乎均匀地分配到所有Hazelcast成员上。每个成员携带大约“数量的映射条目* 2 * 1 / n”条目,其中n是集群中的成员数。例如,如果您有一个具有1000个对象的成员要存储在群集中,然后启动第二个成员,则每个成员将同时存储500个对象并备份另一个成员中的500个对象。
让我们创建一个Hazelcast实例,并Capitals使用以下代码填充以键值对命名的地图。使用HazelcastInstance getMap方法获取地图,然后使用map put方法将条目放入地图。
HazelcastInstance hzInstance = Hazelcast.newHazelcastInstance();
Map<String, String> capitalcities = hzInstance.getMap( "capitals" );
capitalcities.put( "1", "Tokyo" );
capitalcities.put( "2", "Paris" );
capitalcities.put( "3", "Washington" );
capitalcities.put( "4", "Ankara" );
capitalcities.put( "5", "Brussels" );
capitalcities.put( "6", "Amsterdam" );
capitalcities.put( "7", "New Delhi" );
capitalcities.put( "8", "London" );
capitalcities.put( "9", "Berlin" );
capitalcities.put( "10", "Oslo" );
capitalcities.put( "11", "Moscow" );
...
capitalcities.put( "120", "Stockholm" );
运行此代码时,将创建一个集群成员,其中的映射的条目分布在成员的分区中。请参见下图。目前,这是一个单一成员集群。
请注意,某些分区不包含任何数据条目,因为我们只有120个对象,默认情况下分区计数为271。此计数是可配置的,可以使用系统属性进行更改hazelcast.partition.count。请参阅系统属性附录。 |
现在让我们再次运行上面的代码来创建第二个成员。这将创建一个包含两个成员的集群。这也是创建条目备份的位置 - 请记住“ Hazelcast概述”部分中提到的备份分区。下图显示了两个成员以及如何分发数据及其备份。
如您所见,当新成员加入群集时,它将获得所有权并加载群集中的一些数据。最终,它将携带几乎“(1 / n *总数据)+备份”的数据,减少其他成员的负担。
HazelcastInstance.getMap()返回com.hazelcast.core.IMap扩展java.util.concurrent.ConcurrentMap接口的实例。类似于ConcurrentMap.putIfAbsent(key,value)和ConcurrentMap.replace(key,value)可以在分布式地图上使用的方法 ,如下面的示例所示。
public class BasicMapOperations {
private HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
public Customer getCustomer(String id) {
ConcurrentMap<String, Customer> customers = hazelcastInstance.getMap("customers");
Customer customer = customers.get(id);
if (customer == null) {
customer = new Customer(id);
customer = customers.putIfAbsent(id, customer);
}
return customer;
}
public boolean updateCustomer(Customer customer) {
ConcurrentMap<String, Customer> customers = hazelcastInstance.getMap("customers");
return (customers.replace(customer.getId(), customer) != null);
}
public boolean removeCustomer(Customer customer) {
ConcurrentMap<String, Customer> customers = hazelcastInstance.getMap("customers");
return customers.remove(customer.getId(), customer);
}
}
所有ConcurrentMap如操作put和remove如果该键被另一个线程在本地或远程JVM锁定,则可能等待。但是,他们最终会成功回归。ConcurrentMap操作永远不会抛出java.util.ConcurrentModificationException。
另见:
Hazelcast将映射条目分发到多个集群成员(JVM)。每个成员都拥有部分数据。
分布式地图默认具有一个备份。如果成员发生故障,则使用群集中的备份恢复数据。有两种类型的备份,如下所述:sync和async。
为了提供数据安全性,Hazelcast允许您指定要拥有的备份副本数。这样,集群成员上的数据将被复制到其他成员上。
要创建同步备份,请使用该backup-count属性选择备份副本的数量。
<hazelcast>
<map name="default">
<backup-count>1</backup-count>
</map>
</hazelcast>
当此计数为1时,映射条目将在群集中的另一个成员上进行备份。如果将其设置为2,则映射条目将在其他两个成员上进行备份。如果您不希望备份条目,则可以将其设置为0,例如,如果性能比备份更重要。备份计数的最大值为6。
Hazelcast支持同步和异步备份。默认情况下,备份操作是同步的并配置为backup-count。在这种情况下,备份操作会阻止操作,直到备份成功复制到备份成员(或在删除时从备份成员中删除)并收到确认。因此,put只要群集稳定,就会在操作完成之前更新备份。同步备份操作具有阻塞成本,这可能导致延迟问题。
另一方面,异步备份不会阻止操作。它们是火灾和遗忘,不需要确认; 备份操作在某个时间点执行。
要创建异步备份,请选择具有该async-backup-count属性的异步备份数。一个例子如下所示。
<hazelcast>
<map name="default">
<backup-count>0</backup-count>
<async-backup-count>1</async-backup-count>
</map>
</hazelcast>
有关更多详细信息,请参阅一致性和复制模型。
备份会增加内存使用量,因为它们也会保留在内存中。 | |
映射可以同时具有同步和异步备份。 |
默认情况下,Hazelcast具有一个同步备份副本。如果backup-count设置为大于1,则每个成员将携带两个拥有的条目和其他成员的备份副本。因此,对于map.get(key)呼叫,呼叫成员可能具有该密钥的备份副本。默认情况下,map.get(key)将始终从密钥的实际所有者读取值以保持一致性。
要启用备份读取(读取本地备份条目),请将该read-backup-data属性的值设置为true。其一致性的默认值为false。启用备份读取可以提高性能,但另一方面,它可能会导致过时读取,同时仍保留单调读取属性。
<hazelcast>
<map name="default">
<backup-count>0</backup-count>
<async-backup-count>1</async-backup-count>
<read-backup-data>true</read-backup-data>
</map>
</hazelcast>
当至少有一个同步或异步备份时,此功能可用。
请注意,如果您正在执行备份读取,则应考虑到备份中的密钥命中率不会反映为主要成员上的原始密钥的命中。这会影响IMap的最大空闲秒数或生存时间秒数。因此,即使备份中的某个键出现命中,主要成员上的原始密钥也可能会过期。
从Hazelcast 3.7开始,Hazelcast Map使用新的驱逐机制,该机制基于条目的抽样。有关详细信息,请参阅“ 驱逐算法”部分。 |
除非您手动删除地图条目或使用驱逐政策,否则它们将保留在地图中。Hazelcast支持基于策略的分布式地图驱逐。目前支持的策略是LRU(最近最少使用)和LFU(最少使用)。
Hazelcast Map基于分区执行驱逐。例如,当您使用PER_NODE属性指定大小max-size(请参阅配置地图驱逐)时,Hazelcast会在内部计算每个分区的最大大小。Hazelcast使用以下等式计算分区的最大大小:
partition-maximum-size = max-size * member-count / partition-count
如果在partition-maximum-size上面的等式中小于1,则将其设置为1(否则,由于超过max-size小于1 ,因此驱逐立即清空分区)。 |
当您尝试输入条目时,驱逐过程根据此计算的分区最大大小开始。当该分区中的条目计数超过分区最大大小时,将在该分区上开始逐出。
假设您有以下数字作为示例:
- 分区数:200
- 每个分区的条目数:100
- max-size (PER_NODE):20000
此处的条目总数为20000(每个分区的分区计数*条目计数)。这意味着您设置max-size为20000时处于逐出阈值。当您尝试输入条目时
- 该条目转到相关分区;
- 分区检查是否达到驱逐阈值(max-size);
- 只有一个条目将被驱逐。
由于这个驱逐过程,当您检查地图的大小时,它是19999.在此驱逐之后,后续的放置操作将不会触发下一次驱逐,直到地图大小再次接近max-size。
上面的场景只是一个描述驱逐过程如何工作的例子。Hazelcast根据您的群集大小和所选策略查找要驱逐的最佳条目数。 |
以下是地图驱逐的示例声明配置。
<hazelcast>
<map name="default">
...
<time-to-live-seconds>0</time-to-live-seconds>
<max-idle-seconds>0</max-idle-seconds>
<eviction-policy>LRU</eviction-policy>
<max-size policy="PER_NODE">5000</max-size>
...
</map>
</hazelcast>
让我们描述每个元素:
- time-to-live-seconds:每个条目保留在地图中的最长时间(以秒为单位)。如果不为0,则自动逐出清除此时间之前且未更新的条目。有效值是0到0之间的整数Integer.MAX VALUE。默认值为0,表示无限。如果不为0,则无论设置如何都要逐出条目eviction-policy。
- max-idle-seconds:每个条目在映射中保持空闲的最长时间(以秒为单位)。空闲时间超过此时间的条目将被自动驱逐。如果没有一个条目是空闲的get,put,EntryProcessor.process或containsKey叫就可以了。有效值是0到0之间的整数Integer.MAX VALUE。默认值为0,表示无限。
- eviction-policy:有效值如下所述。
- 无:默认政策。如果设置,则不会驱逐任何项目,并且max-size将忽略该属性。你仍然可以将它与time-to-live-seconds和结合起来max-idle-seconds。
- LRU:最近最少使用。
- LFU:最少使用。
除上述值外,您还可以开发和使用自己的驱逐政策。请参阅自定义驱逐政策部分。
- max-size:地图的最大大小。达到最大大小时,将根据定义的策略逐出地图。有效值是0到0之间的整数Integer.MAX VALUE。默认值为0,表示无限。如果要max-size使用,请将该eviction-policy属性设置为NONE以外的值。其属性如下所述。
- PER_NODE:每个集群成员中的最大映射条目数。这是默认策略。
<max-size policy="PER_NODE">5000</max-size>
-
- PER_PARTITION:每个分区中的最大映射条目数。存储大小取决于集群成员中的分区计数。不应经常使用此属性。例如,避免将此属性与小群集一起使用。如果群集很小,它将托管更多分区,因此将映射条目,而不是更大群集的分区。因此,对于小型集群,条目的逐出将降低性能(条目的数量很大)。
<max-size policy="PER_PARTITION">27100</max-size>
- USED_HEAP_SIZE:每个Hazelcast实例的最大使用堆大小(以兆字节为单位)。请注意,当内存格式设置为时,此策略不起作用OBJECT,因为在放置数据时无法确定内存占用OBJECT。
<max-size policy="USED_HEAP_SIZE">4096</max-size>
-
- USED_HEAP_PERCENTAGE:每个Hazelcast实例的每个映射的最大堆大小百分比。例如,如果JVM配置为具有1000 MB且此值为10,则当使用的堆大小超过100 MB时,将逐出映射条目。请注意,当内存格式设置为时,此策略不起作用OBJECT,因为在放置数据时无法确定内存占用OBJECT。
<max-size policy="USED_HEAP_PERCENTAGE">10</max-size>
-
- FREE_HEAP_SIZE:每个JVM的最小可用堆大小(以兆字节为单位)。
<max-size policy="FREE_HEAP_SIZE">512</max-size>
-
- FREE_HEAP_PERCENTAGE:每个JVM的最小可用堆大小百分比。例如,如果JVM配置为具有1000 MB且此值为10,则当空闲堆大小低于100 MB时,将清除映射条目。
<max-size policy="FREE_HEAP_PERCENTAGE">10</max-size>
-
- USED_NATIVE_MEMORY_SIZE:( Hazelcast IMDG Enterprise HD)每个Hazelcast实例的最大使用本机内存大小(以兆字节为单位)。
<max-size policy="USED_NATIVE_MEMORY_SIZE">1024</max-size>
-
- USED_NATIVE_MEMORY_PERCENTAGE:( Hazelcast IMDG Enterprise HD)每个Hazelcast实例的每个映射的最大使用本机内存大小百分比。
<max-size policy="USED_NATIVE_MEMORY_PERCENTAGE">65</max-size>
-
- FREE_NATIVE_MEMORY_SIZE:( Hazelcast IMDG Enterprise HD)每个Hazelcast实例的最小免费本机内存大小(以兆字节为单位)。
<max-size policy="FREE_NATIVE_MEMORY_SIZE">256</max-size>
-
- FREE_NATIVE_MEMORY_PERCENTAGE:( Hazelcast IMDG Enterprise HD)每个Hazelcast实例的最小免费本机内存大小百分比。
<max-size policy="FREE_NATIVE_MEMORY_PERCENTAGE">5</max-size>
截至Hazelcast 3.7,元素eviction-percentage并被min-eviction-check-millis弃用。如果配置,则将忽略它们,因为地图逐出基于条目的抽样。有关详细信息,请参阅“ 驱逐算法”部分。 |
<map name="documents">
<max-size policy="PER_NODE">10000</max-size>
<eviction-policy>LRU</eviction-policy>
<max-idle-seconds>60</max-idle-seconds>
</map>
在上面的示例中,documents当映射大小超过10000时,映射开始从成员中逐出其条目。然后,最近最少使用的条目将被驱逐。未使用超过60秒的条目也将被驱逐。
以下是具有NATIVE内存格式的地图的示例逐出配置:
<map name="nativeMap*">
<in-memory-format>NATIVE</in-memory-format>
<eviction-policy>LFU</eviction-policy>
<max-size policy="USED_NATIVE_MEMORY_PERCENTAGE">99</max-size>
</map>
上面解释的驱逐策略和配置适用于地图的所有条目。符合指定驱逐条件的条目被驱逐。
您可能还想要逐出某些特定的地图条目。为此,您可以使用方法的ttl和timeunit参数map.put()。下面给出了示例代码行。
myMap.put( "1", "John", 50, TimeUnit.SECONDS )
带有键“1”的地图条目将在放入后50秒被逐出myMap。
要从地图中逐出除锁定的键之外的所有键,请使用该方法evictAll()。如果为地图定义了MapStore,deleteAll则不会调用evictAll。如果要调用该方法deleteAll,请使用clear()。
下面给出一个例子。
public class EvictAll {
public static void main(String[] args) {
final int numberOfKeysToLock = 4;
final int numberOfEntriesToAdd = 1000;
HazelcastInstance node1 = Hazelcast.newHazelcastInstance();
HazelcastInstance node2 = Hazelcast.newHazelcastInstance();
IMap<Integer, Integer> map = node1.getMap( "map" );
for (int i = 0; i < numberOfEntriesToAdd; i++) {
map.put(i, i);
}
for (int i = 0; i < numberOfKeysToLock; i++) {
map.lock(i);
}
// should keep locked keys and evict all others.
map.evictAll();
System.out.printf("# After calling evictAll...\n");
System.out.printf("# Expected map size\t: %d\n", numberOfKeysToLock);
System.out.printf("# Actual map size\t: %d\n", map.size());
}
}
对于任何已注册的侦听器,仅触发EVICT_ALL事件。 |
Hazelcast IMDG Enterprise
当理解地图驱逐中解释的驱逐不足以释放你的记忆时,Hazelcast可能会使用强制驱逐。请注意,如果您使用Hazelcast IMDG Enterprise并将内存格式设置为,则此选项有效NATIVE。
强制驱逐机制在下面按给定顺序解释为步骤:
- 当正常驱逐不够时,强制驱逐被触发,首先它试图驱逐大约。来自当前分区的20%的条目。它重试了五次。
- 如果上述步骤的结果仍然不够,则强制驱逐将上述步骤应用于所有地图。这次它也可以从其他一些分区执行驱逐,只要它们由同一个线程拥有。
- 如果这仍然不足以释放你的记忆,它不会驱逐20%,而是驱逐当前分区的所有条目。
- 如果这还不够,它将驱逐其他数据结构中的所有条目; 来自本地线程拥有的分区。
最后,当上述所有步骤都不够时,Hazelcast会抛出Native Out of Memory Exception。
本节适用于Hazelcast 3.7及更高版本。 |
除了Hazelcast开箱即用的LRU和LFU等政策外,您还可以开发和使用自己的驱逐政策。
要实现此目的,您需要提供MapEvictionPolicy以下OddEvictor示例中的实现:
public class MapCustomEvictionPolicy {
public static void main(String[] args) {
Config config = new Config();
config.getMapConfig("test")
.setMapEvictionPolicy(new OddEvictor())
.getMaxSizeConfig()
.setMaxSizePolicy(PER_NODE).setSize(10000);
HazelcastInstance instance = Hazelcast.newHazelcastInstance(config);
IMap<Integer, Integer> map = instance.getMap("test");
final Queue<Integer> oddKeys = new ConcurrentLinkedQueue<Integer>();
final Queue<Integer> evenKeys = new ConcurrentLinkedQueue<Integer>();
map.addEntryListener(new EntryEvictedListener<Integer, Integer>() {
@Override
public void entryEvicted(EntryEvent<Integer, Integer> event) {
Integer key = event.getKey();
if (key % 2 == 0) {
evenKeys.add(key);
} else {
oddKeys.add(key);
}
}
}, false);
// Wait some more time to receive evicted events.
parkNanos(SECONDS.toNanos(5));
for (int i = 0; i < 15000; i++) {
map.put(i, i);
}
String msg = "IMap uses sampling based eviction. After eviction is completed, we are expecting " +
"number of evicted-odd-keys should be greater than number of evicted-even-keys" +
"\nNumber of evicted-odd-keys = %d, number of evicted-even-keys = %d";
out.println(format(msg, oddKeys.size(), evenKeys.size()));
instance.shutdown();
}
/**
* Odd evictor tries to evict odd keys first.
*/
private static class OddEvictor extends MapEvictionPolicy {
@Override
public int compare(EntryView o1, EntryView o2) {
Integer key = (Integer) o1.getKey();
if (key % 2 != 0) {
return -1;
}
return 1;
}
}
}
然后,您可以通过MapConfig.setMapEvictionPolicy() 编程方式或通过XML声明性地设置方法来启用策略。以下是OddEvictor上面实施的驱逐策略的示例声明配置:
<map name="test">
...
<map-eviction-policy-class-name>com.package.OddEvictor</map-eviction-policy-class-name>
....
</map>
如果您使用Spring进行Hazelcast,则可以启用您的策略,如下所示。
<hz:map name="test">
<hz:map-eviction-policy class-name="com.package.OddEvictor"/>
</hz:map>
IMap(和一些其他Hazelcast数据结构,如ICache)有一个in-memory-format配置选项。默认情况下,Hazelcast以二进制(序列化)格式将数据存储到内存中。有时以条目形式存储条目会很有效,特别是在本地处理的情况下,例如条目处理器和查询。
要设置数据在内存中的存储方式,请in-memory-format在配置中进行设置。您有以下格式选项:
- BINARY(默认值):数据(键和值)将以序列化二进制格式存储。如果您主要执行常规的地图操作,例如put和,则可以使用此选项get。
- OBJECT:数据将以反序列化的形式存储。此配置适用于其中条目处理和查询构成所有操作的大部分且对象复杂的映射,使得序列化成本相对较高。通过存储对象,条目处理将不包含反序列化成本。请注意,当您OBJECT用作内存格式时,密钥仍将以二进制格式存储,并且值将以对象格式存储。
- NATIVE:( Hazelcast IMDG Enterprise HD)此格式与BINARY的行为相同,但是,密钥和值将存储在堆外内存中,而不是堆内存。
像get依赖对象实例那样的常规操作。使用OBJECT格式并get执行a时,映射不会返回存储的实例,而是创建克隆。因此,整个get操作首先包括对拥有实例的成员进行序列化,然后对调用该实例的成员进行反序列化。使用该BINARY格式时,只需要反序列化; BINARY是比较快的。
类似地,使用格式put时操作更快BINARY。如果格式是OBJECT,则映射将创建实例的克隆,并且首先是序列化然后是反序列化。使用BINARY时,只需要反序列化。
如果值以OBJECT格式存储,则对返回值的更改不会影响存储的实例。在这种情况下,返回的实例不是实际的实例,而是克隆。因此,返回对象后所做的更改将不会反映在实际存储的数据上。类似地,当将值写入映射并且值以OBJECT格式存储时,它将是put值的副本。因此,存储后对对象所做的更改不会反映在存储的数据上。 | |||
Hazelcast IMDG企业高清
Hazelcast实例是Java程序。在内存BINARY和OBJECT内存格式的情况下,Hazelcast将您的分布式数据存储到其服务器实例的堆中。Java堆受垃圾收集(GC)的限制。如果堆数较多,垃圾收集可能会导致应用程序暂停几十秒(即使是非常大的堆数),也会严重影响应用程序性能和响应时间。
随着数据变得越来越大,您要么运行具有更大堆的应用程序,这将导致更长的GC暂停或运行具有更小堆的多个实例,如果此类实例的数量变得非常高,则可能变成操作噩梦。
为了克服这一挑战,Hazelcast为您的地图提供高密度记忆存储。您可以通过将内存格式设置为,将地图配置为使用高密度存储器NATIVE。以下代码段是声明性配置示例。
<map name="nativeMap*">
<in-memory-format>NATIVE</in-memory-format>
</map>
请记住,您应该已经为群集启用了高密度内存存储库使用。请参阅<<配置高密度存储器,配置高密度存储器部分>。
请注意,驱逐机制对于NATIVE内存格式是不同的。具有高密度存储器的地图的新驱逐算法类似于具有高密度存储器的JCache的驱逐算法,并且在此处描述。
- 驱逐百分比无效。
- <map name="nativeMap*">
- <in-memory-format>NATIVE</in-memory-format>
- <eviction-percentage>25</eviction-percentage> <--! NO IMPACT with NATIVE -->
</map>
- 这些对于IMAP驱逐政策max-size不能使用:FREE_HEAP_PERCENTAGE,FREE_HEAP_SIZE,USED_HEAP_PERCENTAGE,USED_HEAP_SIZE。
- 对于NATIVE内存格式,高速缓存逐出配置也是不同的。对于内存格式设置为的近缓存配置BINARY:
- <map name="nativeMap*">
- <near-cache>
- <in-memory-format>BINARY</in-memory-format>
- <max-size>10000</max-size> <--! NO IMPACT with NATIVE -->
- <eviction-policy>LFU</eviction-policy> <--! NO IMPACT with NATIVE -->
- </near-cache>
</map>
NATIVE内存格式的等效配置类似于以下内容:
<map name="nativeMap*">
<near-cache>
<in-memory-format>NATIVE</in-memory-format>
<eviction size="10000" eviction-policy="LFU" max-size-policy="USED_NATIVE_MEMORY_SIZE"/> <--! Correct configuration with NATIVE -->
</near-cache>
</map>
- 近缓存驱逐策略ENTRY_COUNT不能用于max-size-policy。
有关更多信息, 请参阅高密度存储器部分。 |
Hazelcast允许您从/向持久性数据存储(如关系数据库)加载和存储分布式映射条目。为此,您可以使用Hazelcast MapStore和MapLoader接口。
当您提供MapLoader实现并请求IMap.get()内存中不存在的entry()时,MapLoader’s `load或者loadAll方法将从数据存储中加载该条目。此加载的条目将放置在地图中,并将保留在此处,直到将其删除或逐出。
当提供MapStore实现时,还将条目放入用户定义的数据存储中。
数据存储需要是一个可从所有Hazelcast成员访问的集中式系统。不支持对本地文件系统的持久性。 | |
另请注意,MapStore界面扩展了MapLoader界面,您可以在界面代码中看到。 |
以下是一个MapStore例子。
public class PersonMapStore implements MapStore<Long, Person> {
private final Connection con;
public PersonMapStore() {
try {
con = DriverManager.getConnection("jdbc:hsqldb:mydatabase", "SA", "");
con.createStatement().executeUpdate(
"create table if not exists person (id bigint, name varchar(45))");
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public synchronized void delete(Long key) {
System.out.println("Delete:" + key);
try {
con.createStatement().executeUpdate(
format("delete from person where id = %s", key));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public synchronized void store(Long key, Person value) {
try {
con.createStatement().executeUpdate(
format("insert into person values(%s,'%s')", key, value.name));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public synchronized void storeAll(Map<Long, Person> map) {
for (Map.Entry<Long, Person> entry : map.entrySet())
store(entry.getKey(), entry.getValue());
}
public synchronized void deleteAll(Collection<Long> keys) {
for (Long key : keys) delete(key);
}
public synchronized Person load(Long key) {
try {
ResultSet resultSet = con.createStatement().executeQuery(
format("select name from person where id =%s", key));
try {
if (!resultSet.next()) return null;
String name = resultSet.getString(1);
return new Person(name);
} finally {
resultSet.close();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public synchronized Map<Long, Person> loadAll(Collection<Long> keys) {
Map<Long, Person> result = new HashMap<Long, Person>();
for (Long key : keys) result.put(key, load(key));
return result;
}
public Iterable<Long> loadAllKeys() {
return null;
}
}
在初始加载过程中,MapStore使用与ExecutorService使用的分区线程不同的线程。初始化完成后,该map.get方法在分区线程中从数据库中查找任何不存在的值,或者该map.put方法查找数据库以在分区线程中返回先前关联的键值。 | |
有关更多MapStore / MapLoader代码示例,请参阅此处。 |
Hazelcast支持直读,直写和后写持久性模式,这些模式将在下面的小节中介绍。
如果应用程序请求内存中的条目不存在,Hazelcast会要求加载器实现从数据存储加载该条目。如果该条目存在,则加载器实现获取它,将其交给Hazelcast,Hazelcast将其放入内存。这是直读持久性模式。
MapStore可以通过将write-delay-seconds属性设置为0来配置为直写。这意味着条目将同步放入数据存储。
在此模式下,当map.put(key,value)呼叫返回时:
- MapStore.store(key,value) 成功调用,以便持久化条目。
- 内存条目已更新。
- 在其他集群成员上成功创建内存中备份副本(如果backup-count大于0)。
如果MapStore抛出异常,则异常将传播到原始put或remove以形式调用RuntimeException。
存在的行为的一个关键差异map.remove(key)和map.delete(key),即,后者的结果MapStore.delete(key)被调用,而前者只会从IMAP的条目。 |
您可以MapStore通过将write-delay-seconds属性设置为大于0的值来配置为后写。这意味着修改后的条目将在配置的延迟后异步放入数据存储区。
在后写模式下,默认情况下,Hazelcast会合并特定键的更新,这意味着它仅应用该键上的最后一次更新。但是,您可以设置MapStoreConfig.setWriteCoalescing()为,FALSE并且可以将对密钥执行的所有更新存储到数据存储中。 | |
设置MapStoreConfig.setWriteCoalescing()为FALSE,在达到每节点最大后写队列容量后,后续的put操作将失败ReachedMaxSizeException。将抛出此异常以防止后写队列的不受控制的增长。您可以使用系统属性设置每节点的最大容量hazelcast.map.write.behind.queue.capacity。有关此属性以及如何设置系统属性的信息,请参阅“ 系统属性”附录。 |
在后写模式下,当map.put(key,value)调用返回时:
- 内存条目已更新。
- 在其他集群成员上成功创建内存中备份副本(如果backup-count大于0)。
- 该条目被标记为脏,以便之后write-delay-seconds,它可以通过MapStore.store(key,value)调用保持。
- 对于容错,脏条目存储在主成员的队列中以及备份成员上。
同样的行为也是如此map.remove(key),唯一的区别是 MapStore.delete(key)当条目被删除时被调用。
如果MapStore抛出异常,则Hazelcast会再次尝试存储该条目。如果仍无法存储该条目,则会打印一条日志消息并重新排队该条目。
对于仅在后写模式下允许的批量写入操作,Hazelcast将在一次调用中调用MapStore.storeAll(map)并MapStore.deleteAll(collection)执行所有写操作。
如果映射条目被标记为脏,意味着它等待持久保存到MapStore后写方案中,则逐出过程会强制存储条目。这样您就可以控制等待存储的条目数,从而可以防止可能的OutOfMemory异常。 | |
MapStore或MapLoader实现不应使用Hazelcast Map / Queue / MultiMap / List / Set操作。您的实现应该只适用于您的数据存储。否则,您可能会遇到死锁情况。 |
这是一个示例配置:
<hazelcast>
...
<map name="default">
...
<map-store enabled="true" initial-mode="LAZY">
<class-name>com.hazelcast.examples.DummyStore</class-name>
<write-delay-seconds>60</write-delay-seconds>
<write-batch-size>1000</write-batch-size>
<write-coalescing>true</write-coalescing>
</map-store>
</map>
</hazelcast>
以下是MapStore配置元素和属性的描述:
- class-name:实现MapLoader和/或MapStore的类的名称。
- write-delay-seconds:延迟调用MapStore.store(键,值)的秒数。如果值为零,那么它是直写的,因此一旦条目更新,就会调用MapStore.store(key,value)。否则它是后写,因此通过调用Hazelcast.storeAll(map),将在write-delay-seconds值之后存储更新。默认值为0。
- write-batch-size:用于在编写地图存储时创建批处理块。在默认模式下,将尝试一次性写入所有映射条目。要创建批处理块,write-batch-size的最小有意义值为2.对于小于2的值,它在默认模式下工作。
- write-coalescing:在后写模式下,默认情况下,Hazelcast会合并特定键的更新; 它只适用于它的最后一次更新。您可以将此元素设置false为将对密钥执行的所有更新存储到数据存储。
- enabled:True表示启用此map-store,false表示禁用。默认值为true。
- initial-mode:设置初始加载模式。LAZY是默认的加载模式,其中加载是异步的。EAGER表示加载被阻止,直到加载所有分区。有关详细信息,请参阅“ 启动时初始化映射”部分。
可以使用通配符将配置应用于多个映射(请参阅使用通配符),这意味着配置在映射之间共享。但是MapStore,当有一个配置应用于多个映射时,不知道要存储哪些条目。
要在将一个配置应用于多个地图时存储条目,请使用Hazelcast的MapStoreFactory界面。使用该MapStoreFactory接口,可以在使用通配符配置时创建每个映射的MapStore。示例代码如下所示。
Config config = new Config();
MapConfig mapConfig = config.getMapConfig( "*" );
MapStoreConfig mapStoreConfig = mapConfig.getMapStoreConfig();
mapStoreConfig.setFactoryImplementation( new MapStoreFactory<Object, Object>() {
@Override
public MapLoader<Object, Object> newMapStore( String mapName, Properties properties ) {
return null;
}
});
要MapLoader使用给定的映射名称,配置属性和Hazelcast实例初始化实现,请实现该MapLoaderLifecycleSupport接口。这个接口有方法init()和destroy()。
该方法init()初始化MapLoader实现。当Hazelcast实例首次使用地图时,Hazelcast会调用此方法。该MapLoader实现可以初始化实现所需的资源,MapLoader例如读取配置文件或创建数据库连接。
Hazelcast destroy()在关闭之前调用该方法。您可以覆盖此方法以清除此MapLoader实现所拥有的资源,例如关闭数据库连接。
要在首次触摸/使用地图时预先填充内存中的地图,请使用MapLoader.loadAllKeysAPI。
如果MapLoader.loadAllKeys返回NULL,则不会加载任何内容。您的MapLoader.loadAllKeys实现可以返回全部或部分密钥。例如,您可以在初始化地图时仅选择并返回要加载它们的最重要的键。MapLoader.loadAllKeys是预填充地图的最快方法,因为Hazelcast将通过让每个集群成员加载其拥有的条目部分来优化加载过程。
MapStoreConfigInitialLoadMode类中的配置参数有两个值:和。如果设置为,则在创建地图期间不会加载数据。如果设置为,则在创建地图时加载所有数据,并且一切都可以使用。此外,如果使用MapIndexConfig类或方法向地图添加索引,则会被覆盖并且行为就像启用了模式一样。LAZYEAGERInitialLoadModeLAZYEAGERaddIndexInitialLoadModeMapStoreConfigEAGER
这是MapLoader初始化流程:
- 当getMap()首先从任何成员呼吁,初始化将开始根据的价值InitialLoadMode。如果设置为EAGER,则一旦触摸了地图,就会在所有分区上开始初始化,即,在getMap调用时将加载所有分区。如果设置为LAZY,则将按分区加载数据,即每个分区将加载其第一次触摸。
- Hazelcast将致电MapLoader.loadAllKeys()将其所有钥匙交给其中一名成员。
- 该成员将批量分发给所有其他成员。
- 每个成员将通过调用加载其拥有的所有密钥的值MapLoader.loadAll(keys)。
- 每个成员通过调用将其拥有的条目放入映射中IMap.putTransient(key,value)。
如果负载模式LAZY和clear()方法被调用(触发MapStore.deleteAll()),Hazelcast将删除只从你的地图和数据存储的加载项。由于在这种情况下(LAZY模式)未加载所有数据,请注意您的数据存储区中可能仍有条目。* | |
如果您不希望第一个集群成员启动后立即加载MapStore,则可以使用系统属性hazelcast.initial.min.cluster.size。例如,如果将其值设置为3,则将阻止加载过程,直到所有三个成员完全启动。* |
返回类型loadAllKeys()从改变Set到Iterable与Hazelcast 3.5的发布。还支持以前版本中的MapLoader实现,无需进行调整。 |
如果要加载的密钥数量很大,则以递增方式加载它们更有效,而不是一次加载它们。为了支持增量加载,该MapLoader.loadAllKeys()方法返回一个Iterable可以使用数据库查询的结果进行延迟填充的方法。
Hazelcast遍历,Iterable并在执行此操作时,将密钥发送给各自的所有者成员。将Iterator得到的从MapLoader.loadAllKeys()也可以实现Closeable接口,在这种情况下Iterator是封闭的一次迭代结束。这用于释放资源,例如关闭JDBC结果集。
该方法loadAll将一些或所有密钥加载到数据存储中以优化多个加载操作。该方法有两个签名; 相同的方法可以采用两个不同的参数列表。一个签名加载给定的键,另一个加载所有键。请参阅下面的示例代码。
public class LoadAll {
public static void main(String[] args) {
final int numberOfEntriesToAdd = 1000;
final String mapName = LoadAll.class.getCanonicalName();
final Config config = createNewConfig(mapName);
final HazelcastInstance node = Hazelcast.newHazelcastInstance(config);
final IMap<Integer, Integer> map = node.getMap(mapName);
populateMap(map, numberOfEntriesToAdd);
System.out.printf("# Map store has %d elements\n", numberOfEntriesToAdd);
map.evictAll();
System.out.printf("# After evictAll map size\t: %d\n", map.size());
map.loadAll(true);
System.out.printf("# After loadAll map size\t: %d\n", map.size());
}
}
在某些情况下,您可能需要在将对象存储到地图存储中后对其进行修改。例如,您可以获取数据库自动生成的ID或版本,然后需要修改存储在分布式映射中的对象,但不能破坏数据库和数据网格之间的同步。
要在地图存储中对对象进行后处理,请实现PostProcessingMapStore接口以将修改后的对象放入分布式地图中。这将触发额外的步骤Serialization,因此仅在需要时使用它。(仅在使用write-through地图存储配置时有效。)
以下是后期处理地图商店的示例:
class ProcessingStore implements MapStore<Integer, Employee>, PostProcessingMapStore {
@Override
public void store( Integer key, Employee employee ) {
EmployeeId id = saveEmployee();
employee.setId( id.getId() );
}
}
请注意,如果您将后处理地图存储与条目处理器结合使用,则不会将后处理值传送到备份。 |
您可以自己准备MapLoader访问Cassandra和MongoDB等数据库。为此,您可以首先在hazelcast.xml配置文件中以声明方式指定数据库属性,然后实现MapLoaderLifecycleSupport接口以传递这些属性。
您可以使用properties配置元素定义数据库属性,例如URL和名称。以下是MongoDB的配置示例:
<map name="supplements">
<map-store enabled="true" initial-mode="LAZY">
<class-name>com.hazelcast.loader.YourMapStoreImplementation</class-name>
<properties>
<property name="mongo.url">mongodb://localhost:27017</property>
<property name="mongo.db">mydb</property>
<property name="mongo.collection">supplements</property>
</properties>
</map-store>
</map>
在配置中指定数据库属性后,需要实现MapLoaderLifecycleSupport接口并在init()方法中提供这些属性,如下所示:
public class YourMapStoreImplementation implements MapStore<String, Supplement>, MapLoaderLifecycleSupport {
private MongoClient mongoClient;
private MongoCollection collection;
public YourMapStoreImplementation() {
}
@Override
public void init(HazelcastInstance hazelcastInstance, Properties properties, String mapName) {
String mongoUrl = (String) properties.get("mongo.url");
String dbName = (String) properties.get("mongo.db");
String collectionName = (String) properties.get("mongo.collection");
this.mongoClient = new MongoClient(new MongoClientURI(mongoUrl));
this.collection = mongoClient.getDatabase(dbName).getCollection(collectionName);
}
您可以参考此处的完整示例。
Hazelcast分布式映射支持远程存储条目的本地Near Cache,以提高本地读取操作的性能。有关Near Cache功能及其配置的详细说明,请参阅Near Cache部分。
Hazelcast分布式映射(IMap)是线程安全的,可满足您的线程安全要求。当这些要求增加或者您希望对并发性有更多控制时,请考虑此处描述的Hazelcast解决方案。
让我们研究一个示例案例,如下所示。
public class RacyUpdateMember {
public static void main( String[] args ) throws Exception {
HazelcastInstance hz = Hazelcast.newHazelcastInstance();
IMap<String, Value> map = hz.getMap( "map" );
String key = "1";
map.put( key, new Value() );
System.out.println( "Starting" );
for ( int k = 0; k < 1000; k++ ) {
if ( k % 100 == 0 ) System.out.println( "At: " + k );
Value value = map.get( key );
Thread.sleep( 10 );
value.amount++;
map.put( key, value );
}
System.out.println( "Finished! Result = " + map.get(key).amount );
}
static class Value implements Serializable {
public int amount;
}
}
如果上述代码同时由多个集群成员运行,则可能存在竞争条件。您可以使用悲观锁定或乐观锁定使用Hazelcast解决此问题。
解决竞争问题的一种方法是使用悲观锁定 - 锁定地图条目,直到完成它。
要执行悲观锁定,请使用Hazelcast分布式地图提供的锁定机制,即map.lock和map.unlock方法。请参阅以下示例代码。
public class PessimisticUpdateMember {
public static void main( String[] args ) throws Exception {
HazelcastInstance hz = Hazelcast.newHazelcastInstance();
IMap<String, Value> map = hz.getMap( "map" );
String key = "1";
map.put( key, new Value() );
System.out.println( "Starting" );
for ( int k = 0; k < 1000; k++ ) {
map.lock( key );
try {
Value value = map.get( key );
Thread.sleep( 10 );
value.amount++;
map.put( key, value );
} finally {
map.unlock( key );
}
}
System.out.println( "Finished! Result = " + map.get( key ).amount );
}
static class Value implements Serializable {
public int amount;
}
}
当锁被释放且锁上没有其他等待条件时,垃圾收集器将自动收集IMap锁。
IMap锁是可重入的,但它不支持公平性。
解决竞赛问题的另一种方法是Lock从Hazelcast 获取可预测的对象。这样,可以为映射中的每个值提供锁定,也可以创建锁定条带。
在Hazelcast中,您可以使用map的replace方法应用乐观锁定策略。此方法根据内存格式配置比较对象或数据形式的值。如果值相等,则将旧值替换为新值。如果你想使用你定义的equals方法,in-memory-format应该是OBJECT。否则,Hazelcast将对象序列化为BINARY表单并进行比较。
请参阅以下示例代码。
以下示例代码被故意破坏。 |
public class OptimisticMember {
public static void main( String[] args ) throws Exception {
HazelcastInstance hz = Hazelcast.newHazelcastInstance();
IMap<String, Value> map = hz.getMap( "map" );
String key = "1";
map.put( key, new Value() );
System.out.println( "Starting" );
for ( int k = 0; k < 1000; k++ ) {
if ( k % 10 == 0 ) System.out.println( "At: " + k );
for (; ; ) {
Value oldValue = map.get( key );
Value newValue = new Value( oldValue );
Thread.sleep( 10 );
newValue.amount++;
if ( map.replace( key, oldValue, newValue ) )
break;
}
}
System.out.println( "Finished! Result = " + map.get( key ).amount );
}
static class Value implements Serializable {
public int amount;
public Value() {
}
public Value( Value that ) {
this.amount = that.amount;
}
public boolean equals( Object o ) {
if ( o == this ) return true;
if ( !( o instanceof Value ) ) return false;
Value that = ( Value ) o;
return that.amount == this.amount;
}
}
}
您选择的锁定策略取决于您的锁定要求。
对于大多数只读系统,乐观锁定更好。与悲观锁定相比,它具有性能提升。
如果同一个密钥上有大量更新,则悲观锁定很好。从数据一致性的角度来看,它比乐观锁定更强大。
在Hazelcast中,用于IExecutorService向关键所有者或成员提交任务。这是执行任务执行的推荐方法,而不是使用悲观或乐观锁定技术。IExecutorService网络跳数减少,线路数据减少,任务将非常接近数据执行。请参阅数据亲和力部分。
当共享资源打开以由多个线程更改时,ABA问题发生在环境中。即使一个线程在连续读取中看到特定键的相同值,也不意味着读取之间没有任何变化。另一个线程可能会更改值,执行工作并更改值,而第一个线程认为没有任何更改。
为了防止出现这类问题,您可以在任何写入之前分配版本号并进行检查,以确保连续读取之间没有任何变化。虽然所有其他字段都相同,但版本字段将阻止对象被视为相等。这是乐观锁定策略,它用于不期望对特定密钥进行密集并发更改的环境中。
在Hazelcast中,您可以使用map 方法应用乐观锁定策略replace。
可以将锁配置为在应用锁定操作之前检查当前存在的成员的数量。如果检查失败,则锁定操作将失败并显示QuorumException(参见裂脑保护)。由于悲观锁定在内部使用锁定操作,它还将使用配置的锁定仲裁。这意味着您可以配置具有相同名称的锁定仲裁或与地图名称匹配的模式。请注意,IMap锁定操作的仲裁可能与其他IMap操作的仲裁不同。
然后,以下操作将在应用之前检查锁定仲裁:
- IMap.lock(K) 和 IMap.lock(K, long, java.util.concurrent.TimeUnit)
- IMap.isLocked()
- IMap.tryLock(K),IMap.tryLock(K, long, java.util.concurrent.TimeUnit)和IMap.tryLock(K, long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit)
- IMap.unlock()
- IMap.forceUnlock()
- MultiMap.lock(K) 和 MultiMap.lock(K, long, java.util.concurrent.TimeUnit)
- MultiMap.isLocked()
- MultiMap.tryLock(K),MultiMap.tryLock(K, long, java.util.concurrent.TimeUnit)和MultiMap.tryLock(K, long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit)
- MultiMap.unlock()
- MultiMap.forceUnlock()
声明性配置的示例:
<map name="myMap">
<quorum-ref>map-actions-quorum</quorum-ref>
</map>
<lock name="myMap">
<quorum-ref>map-lock-actions-quorum</quorum-ref>
</lock>
此处配置的地图将使用map-lock-actions-quorum法定数量进行地图锁定操作,使用map-actions-quorum法定数量进行其他地图操作。
Hazelcast保留有关每个映射条目的统计信息,例如创建时间,上次更新时间,上次访问时间,命中数和版本。要访问地图条目统计信息,请使用IMap.getEntryView(key)呼叫。这是一个例子。
HazelcastInstance hz = Hazelcast.newHazelcastInstance();
EntryView entry = hz.getMap( "quotes" ).getEntryView( "1" );
System.out.println ( "size in memory : " + entry.getCost() );
System.out.println ( "creationTime : " + entry.getCreationTime() );
System.out.println ( "expirationTime : " + entry.getExpirationTime() );
System.out.println ( "number of hits : " + entry.getHits() );
System.out.println ( "lastAccessedTime: " + entry.getLastAccessTime() );
System.out.println ( "lastUpdateTime : " + entry.getLastUpdateTime() );
System.out.println ( "version : " + entry.getVersion() );
System.out.println ( "key : " + entry.getKey() );
System.out.println ( "value : " + entry.getValue() );
请参阅倾听地图事件部分。
您可以收听对特定地图条目执行的修改。您可以将其视为具有谓词的条目侦听器。有关如何向地图添加条目侦听器的信息,请参阅“ 侦听地图事件”部分。
默认的向后兼容事件发布策略仅UPDATED在将映射条目更新为与监听器所注册的谓词匹配的值时才发布 事件。这意味着在使用默认事件发布策略时,不会通知您的侦听器,其值将从与谓词匹配的条目更新为与谓词不匹配的新值。 |
从版本3.7开始,当您将属性hazelcast.map.entry.filtering.natural.event.types设置为的Hazelcast成员配置为时true,条目更新的处理在概念上将值转换视为关于谓词值空间的条目,更新或退出。下表比较了在默认的向后兼容Hazelcast行为(当属性hazelcast.map.entry.filtering.natural.event.types未设置或设置为false)时与侦听器通知如何更新映射条目值与设置为true:
默认 | hazelcast.map.entry.filtering.natural.event.types = true | |
当旧值与谓词匹配时,新值与谓词不匹配 | 没有事件传递给入口监听器 | REMOVED 事件被传递给入门监听器 |
当旧值与谓词匹配时,新值与谓词匹配 | UPDATED 事件被传递给入门监听器 | UPDATED 事件被传递给入门监听器 |
当旧值与谓词不匹配时,新值与谓词不匹配 | 没有事件传递给入口监听器 | 没有事件传递给入口监听器 |
当旧值与谓词不匹配时,新值与谓词匹配 | UPDATED 事件被传递给入门监听器 | ADDED 事件被传递给入门监听器 |
例如,让我们听一下姓氏为“Smith”的员工所做的更改。首先,让我们创建一个Employee类。
public class Employee implements Serializable {
private final String surname;
public Employee(String surname) {
this.surname = surname;
}
@Override
public String toString() {
return "Employee{" +
"surname='" + surname + '\'' +
'}';
}
}
然后,让我们添加监听跟踪创建断言监听器ADDED,UPDATED并REMOVED与进入事件surname断言。
public class ListenerWithPredicate {
public static void main(String[] args) {
Config config = new Config();
config.setProperty("hazelcast.map.entry.filtering.natural.event.types", "true");
HazelcastInstance hz = Hazelcast.newHazelcastInstance(config);
IMap<String, String> map = hz.getMap("map");
map.addEntryListener(new MyEntryListener(),
new SqlPredicate("surname=smith"), true);
System.out.println("Entry Listener registered");
}
static class MyEntryListener
implements EntryAddedListener<String, String>,
EntryUpdatedListener<String, String>,
EntryRemovedListener<String, String> {
@Override
public void entryAdded(EntryEvent<String, String> event) {
System.out.println("Entry Added:" + event);
}
@Override
public void entryRemoved(EntryEvent<String, String> event) {
System.out.println("Entry Removed:" + event);
}
@Override
public void entryUpdated(EntryEvent<String, String> event) {
System.out.println("Entry Updated:" + event);
}
}
}
现在,让我们与员工“史密斯”一起玩,看看该员工将如何收听。
public class Modify {
public static void main(String[] args) {
Config config = new Config();
config.setProperty("hazelcast.map.entry.filtering.natural.event.types", "true");
HazelcastInstance hz = Hazelcast.newHazelcastInstance(config);
IMap<String, Employee> map = hz.getMap("map");
map.put("1", new Employee("smith"));
map.put("2", new Employee("jordan"));
System.out.println("done");
System.exit(0);
}
}
当您第一次运行该类ListenerWithPredicate然后运行时Modify,您将看到类似于下面列表的输出。
entryAdded:EntryEvent {Address[192.168.178.10]:5702} key=1,oldValue=null,
value=Person{name= smith }, event=ADDED, by Member [192.168.178.10]:5702
有关更多信息, 请参阅Continuous Query Cache。 |
您可以删除与谓词匹配的所有映射条目。为此,Hazelcast提供了这种方法removeAll()。其语法如下:
void removeAll(Predicate<K, V> predicate);
通常,匹配谓词的地图条目会在地图的完整扫描中找到。如果条目被编入索引,Hazelcast使用索引搜索来查找它们。使用索引,您可以期望找到条目更快。
当removeAll()被调用时,主叫方成员的近缓存中的所有条目也将被删除。 |
您可以添加拦截操作并同步执行您自己的业务逻辑来阻止操作。您可以通过抛出异常来更改操作的返回值get,更改值put或cancel操作。
拦截器与听众不同。使用侦听器,您可以在操作完成后执行操作。拦截器操作是同步的,您可以更改操作行为,更改其值或完全取消它。
地图拦截器是链接的,因此将相同的拦截器多次添加到同一个地图可能会导致重复的效果。当拦截器在成员初始化时添加到地图中时,很容易发生这种情况,因此每个成员都会添加相同的拦截器。以这种方式添加拦截器时,请确保实现该hashCode()方法以为拦截器的每个实例返回相同的值。这不是绝对必要的,但是也可以实现,equals()因为这将确保可以可靠地删除地图拦截器。
IMap API有两种方法可以向地图添加和删除拦截器:addInterceptor和removeInterceptor。另请参阅MapInterceptor界面以查看用于拦截地图中更改的方法。
以下是一个示例用法。
public class InterceptorTest {
@org.junit.Test
public void testMapInterceptor() throws InterruptedException {
HazelcastInstance hazelcastInstance1 = Hazelcast.newHazelcastInstance();
HazelcastInstance hazelcastInstance2 = Hazelcast.newHazelcastInstance();
IMap<Object, Object> map = hazelcastInstance1.getMap( "testMapInterceptor" );
SimpleInterceptor interceptor = new SimpleInterceptor();
String interceptorId = map.addInterceptor( interceptor );
map.put( 1, "New York" );
map.put( 2, "Istanbul" );
map.put( 3, "Tokyo" );
map.put( 4, "London" );
map.put( 5, "Paris" );
map.put( 6, "Cairo" );
map.put( 7, "Hong Kong" );
try {
map.remove( 1 );
} catch ( Exception ignore ) {
}
try {
map.remove( 2 );
} catch ( Exception ignore ) {
}
assertEquals( map.size(), 6) ;
assertEquals( map.get( 1 ), null );
assertEquals( map.get( 2 ), "ISTANBUL:" );
assertEquals( map.get( 3 ), "TOKYO:" );
assertEquals( map.get( 4 ), "LONDON:" );
assertEquals( map.get( 5 ), "PARIS:" );
assertEquals( map.get( 6 ), "CAIRO:" );
assertEquals( map.get( 7 ), "HONG KONG:" );
map.removeInterceptor( interceptorId );
map.put( 8, "Moscow" );
assertEquals( map.get( 8 ), "Moscow" );
assertEquals( map.get( 1 ), null );
assertEquals( map.get( 2 ), "ISTANBUL" );
assertEquals( map.get( 3 ), "TOKYO" );
assertEquals( map.get( 4 ), "LONDON" );
assertEquals( map.get( 5 ), "PARIS" );
assertEquals( map.get( 6 ), "CAIRO" );
assertEquals( map.get( 7 ), "HONG KONG" );
}
static class SimpleInterceptor implements MapInterceptor, Serializable {
@Override
public Object interceptGet( Object value ) {
if (value == null)
return null;
return value + ":";
}
@Override
public void afterGet( Object value ) {
}
@Override
public Object interceptPut( Object oldValue, Object newValue ) {
return newValue.toString().toUpperCase();
}
@Override
public void afterPut( Object value ) {
}
@Override
public Object interceptRemove( Object removedValue ) {
if(removedValue.equals( "ISTANBUL" ))
throw new RuntimeException( "you can not remove this" );
return removedValue;
}
@Override
public void afterRemove( Object value ) {
// do something
}
}
}
使用基于查询的映射方法触发内存不足异常(OOME)非常容易,尤其是对于大型集群或堆大小。例如,在具有5个成员且每个成员具有10 GB数据和25 GB堆大小的群集上,单次调用将IMap.entrySet()获取50 GB数据并使调用实例崩溃。
调用IMap.values()可能会为单个成员返回太多数据。这也可能发生在真实查询和不幸的谓词选择上,尤其是当您的应用程序的用户选择参数时。
为防止这种情况,您可以为基于查询的操作配置最大结果大小限制。这不是一个限制SELECT * FROM map LIMIT 100,你可以通过Paging Predicate实现。基于查询的操作的最大结果大小限制意味着防止您的成员检索的数据超出其可处理范围的最后一道防线。
计算此限制的Hazelcast组件是QueryResultSizeLimiter。
如果QueryResultSizeLimiter激活,则计算每个分区的结果大小限制。每个都QueryOperation在成员的所有分区上运行,因此只要不超过成员限制,它就会收集结果条目。如果发生这种情况,QueryResultSizeExceededException则抛出a并传播到调用实例。
此功能取决于集群成员上的数据的相等分布,以计算每个成员的结果大小限制。因此,有一个最小值定义QueryResultSizeLimiter.MINIMUM_MAX_RESULT_LIMIT。低于最小值的配置值将增加到最小值。
除了分布式结果大小检查之外QueryOperations,还有对调用实例的本地预检查。如果从客户端调用该方法,则会对调用该成员的成员执行预检查QueryOperations。
由于本地预检可以增加a的延迟QueryOperation,因此您可以配置应该考虑多少本地分区进行预检,或者您可以完全停用该功能。
除了指定的查询操作外,还有其他在内部使用谓词的操作。那些方法调用也会抛出QueryResultSizeExceededException。请参阅以下矩阵以查看查询结果大小限制所涵盖的方法。
查询结果大小限制是通过以下系统属性配置的。
- hazelcast.query.result.size.limit:地图上查询操作的结果大小限制。此值定义单个查询结果的最大返回元素数。如果查询超过此数量的元素,则抛出QueryResultSizeExceededException。
- hazelcast.query.max.local.partition.limit.for.precheck:本地分区的最大值,用于触发对映射上的TruePredicate查询操作的本地预检查。
请参阅“ 系统属性”部分以查看这些属性的完整说明以及如何设置它们。
Hazelcast分布式队列是一种实现java.util.concurrent.BlockingQueue。分布式Hazelcast分布式队列使所有集群成员能够与其进行交互。使用Hazelcast分布式队列,您可以在一个集群成员中添加项目并将其从另一个集群成员中删除。
使用Hazelcast实例的getQueue方法获取队列,然后使用队列的put方法将项目放入队列。
public class SampleQueue {
public static void main(String[] args) throws Exception {
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
BlockingQueue<MyTask> queue = hazelcastInstance.getQueue( "tasks" );
queue.put( new MyTask() );
MyTask task = queue.take();
boolean offered = queue.offer( new MyTask(), 10, TimeUnit.SECONDS );
task = queue.poll( 5, TimeUnit.SECONDS );
if ( task != null ) {
//process task
}
}
}
FIFO排序将应用于整个群集中的所有队列操作。MyTask必须是排队或出列的用户对象(例如在上面的示例中)Serializable。
Hazelcast分布式队列在迭代队列时不执行批处理。所有项目都将在本地复制,迭代将在本地进行。
Hazelcast分布式队列用于ItemListener侦听在队列中添加和删除项目时发生的事件。有关如何创建项侦听器类并进行注册的信息,请参阅“ 侦听项目事件”部分。
以下示例代码说明了连接生产者和使用者的分布式队列。
让我们put每秒在队列上有一个整数,总共100个整数。
public class ProducerMember {
public static void main( String[] args ) throws Exception {
HazelcastInstance hz = Hazelcast.newHazelcastInstance();
IQueue<Integer> queue = hz.getQueue( "queue" );
for ( int k = 1; k < 100; k++ ) {
queue.put( k );
System.out.println( "Producing: " + k );
Thread.sleep(1000);
}
queue.put( -1 );
System.out.println( "Producer Finished!" );
}
}
Producer在队列中放置-1以显示`put`s已完成。
现在,让我们为这个队列中的消息创建一个Consumer类take,如下所示。
public class ConsumerMember {
public static void main( String[] args ) throws Exception {
HazelcastInstance hz = Hazelcast.newHazelcastInstance();
IQueue<Integer> queue = hz.getQueue( "queue" );
while ( true ) {
int item = queue.take();
System.out.println( "Consumed: " + item );
if ( item == -1 ) {
queue.put( -1 );
break;
}
Thread.sleep( 5000 );
}
System.out.println( "Consumer Finished!" );
}
}
如上面的示例代码所示,Consumer在消耗下一条消息之前等待五秒钟。一旦收到-1,它就会停止。另请注意,在循环结束之前Consumer将-1放回队列。
首次启动Producer然后启动时Consumer,队列中生成的项目将从同一队列中使用。
从上面的示例代码中,您可以看到每秒生成一个项目并且每五秒钟消耗一次。因此,消费者不断增长。为了平衡生产/消费操作,让我们开始另一个消费者。这样,消费就分配给这两个消费者,如下面的样本输出所示。
第二个消费者开始了。过了一会儿,这是第一个消费者输出:
...
Consumed 13
Consumed 15
Consumer 17
...
这是第二个消费者输出:
...
Consumed 14
Consumed 16
Consumer 18
...
在队列的许多生产者和消费者的情况下,使用队列列表可以解决队列瓶颈。在这种情况下,请注意不保证发送到不同队列的消息的顺序。由于在大多数情况下严格排序并不重要,因此队列列表是一个很好的解决方案。
这些项目是按照它们放入队列的顺序从队列中获取的。但是,如果有多个消费者,则无法保证此订单。 |
Hazelcast为itemId您提供的每个项目提供一个,这是队列项目的递增序列标识。您应该考虑以下内容来理解itemId赋值行为:
- 当Hazelcast成员具有队列,并且该队列配置为至少具有一个备份,并且该成员重新启动时,该itemId分配将从itemId重新启动之前的最后一个已知最高位置恢复; itemId对于新项目,分配不从头开始。
- 重新启动整个群集时,如果您的队列具有持久数据存储(QueueStore),则上述考虑中解释的相同行为也适用。如果队列有QueueStore,itemId则为新项目提供,从itemId方法返回的ID中找到的最高项开始loadAllKeys。如果该方法loadAllKeys没有返回任何内容,则`itemId将在集群重启后从头开始。
- 上述两个考虑因素意味着内存或持久数据存储中不会出现重复的`itemId`。
有界队列是容量有限的队列。当有界队列已满时,在取出某些项目之前,不能再将任何项目放入队列。
要将Hazelcast分布式队列转换为有界队列,请使用该max-size属性设置容量限制。您可以max-size在配置中设置属性,如下所示。max-size指定队列的最大大小。一旦队列大小达到此值,put操作将被阻止,直到队列大小低于此值,max-size这发生在消费者从队列中删除项目时。
让我们在创建示例队列中将10设置为示例队列的最大大小。
<hazelcast>
...
<queue name="queue">
<max-size>10</max-size>
</queue>
...
</hazelcast>
生成器启动时,将十个项放入队列,然后队列将不允许更多put操作。启动使用者后,它将从队列中删除项目。这意味着生产者可以将put更多项目放入队列,直到队列中再有十个项目,此时put操作再次被阻止。
在此示例代码中,生产者比消费者快五倍。它实际上总是等待消费者在可以将更多内容放在队列上之前删除项目。对于此示例代码,如果最大吞吐量是目标,则启动多个使用者以防止队列填满是一个不错的选择。
Hazelcast允许您使用该接口从/向持久性数据存储区加载和存储分布式队列项QueueStore。如果启用了队列存储,则添加到队列的每个项目也将存储在已配置的队列存储中。当队列中的项目数超过内存限制时,后续项目将保留在队列存储中,它们不会存储在队列内存中。
该QueueStore接口使您能够存储,负载,并用类似的方法删除队列中的项目store,storeAll,load和delete。以下示例类包括所有QueueStore方法。
public class TheQueueStore implements QueueStore<Item> {
@Override
public void delete(Long key) {
System.out.println("delete");
}
@Override
public void store(Long key, Item value) {
System.out.println("store");
}
@Override
public void storeAll(Map<Long, Item> map) {
System.out.println("store all");
}
@Override
public void deleteAll(Collection<Long> keys) {
System.out.println("deleteAll");
}
@Override
public Item load(Long key) {
System.out.println("load");
return null;
}
@Override
public Map<Long, Item> loadAll(Collection<Long> keys) {
System.out.println("loadAll");
return null;
}
@Override
public Set<Long> loadAllKeys() {
System.out.println("loadAllKeys");
return null;
}
}
Item必须是可序列化的。以下是示例队列存储配置。
<queue-store>
<class-name>com.hazelcast.QueueStoreImpl</class-name>
<properties>
<property name="binary">false</property>
<property name="memory-limit">1000</property>
<property name="bulk-load">500</property>
</properties>
</queue-store>
我们来解释一下队列存储属性。
- 二进制:默认情况下,Hazelcast以序列化形式存储队列项,并在将队列项插入队列存储之前对其进行反序列化。如果您没有从外部应用程序到达队列存储,您可能更喜欢以二进制形式插入项目。通过将binary属性设置为true来执行此操作:然后您可以摆脱反序列化步骤,这是性能优化。binary默认情况下该属性为false。
- 内存限制:这是Hazelcast仅将项目存储到数据存储区的项目数。例如,如果内存限制为1000,则第1001项将仅放入数据存储区。当您想要避免内存不足的情况时,此功能非常有用。如果要始终使用内存,可以将其设置为Integer.MAX_VALUE。默认数量为memory-limit1000。
- 批量加载:初始化队列时,将从批量加载项目QueueStore。批量加载是这些批量的大小。默认值为bulk-load250。
可以将队列配置为在应用队列操作之前检查可用成员的最小数量(请参阅裂脑保护)。这是一项检查,以避免在网络分区期间对群集的所有部分执行成功的队列操作。
以下是现在支持裂脑保护检查的方法列表。该列表按仲裁类型分组。
- 写,READ_WRITE
- Collection.addAll()
- Collection.removeAll(), Collection.retainAll()
- BlockingQueue.offer(),BlockingQueue.add(),BlockingQueue.put()
- BlockingQueue.drainTo()
- IQueue.poll(),Queue.remove(),IQueue.take()
- BlockingQueue.remove()
- READ,READ_WRITE
- Collection.clear()
- Collection.containsAll(), BlockingQueue.contains()
- Collection.isEmpty()
- Collection.iterator(), Collection.toArray()
- Queue.peek(), Queue.element()
- Collection.size()
- BlockingQueue.remainingCapacity()
以下是队列配置的示例。它包括QueueStore配置,在“ 使用持久数据存储区排队”部分中对此进行了说明。
声明:
<queue name="default">
<max-size>0</max-size>
<backup-count>1</backup-count>
<async-backup-count>0</async-backup-count>
<empty-queue-ttl>-1</empty-queue-ttl>
<item-listeners>
<item-listener>com.hazelcast.examples.ItemListener</item-listener>
</item-listeners>
<statistics-enabled>true</statistics-enabled>
<queue-store>
<class-name>com.hazelcast.QueueStoreImpl</class-name>
<properties>
<property name="binary">false</property>
<property name="memory-limit">10000</property>
<property name="bulk-load">500</property>
</properties>
</queue-store>
<quorum-ref>quorumname</quorum-ref>
</queue>
程序化:
Config config = new Config();
QueueConfig queueConfig = config.getQueueConfig("default");
queueConfig.setName("MyQueue")
.setBackupCount(1)
.setMaxSize(0)
.setStatisticsEnabled(true)
.setQuorumName("quorumname");
queueConfig.getQueueStoreConfig()
.setEnabled(true)
.setClassName("com.hazelcast.QueueStoreImpl")
.setProperty("binary", "false");
config.addQueueConfig(queueConfig);
Hazelcast分布式队列默认具有一个同步备份。通过此备份,当具有队列的集群成员关闭时,具有该队列备份的另一个成员将继续。因此,没有物品丢失。您可以使用backup-count声明性配置中的元素定义队列的同步备份数。队列也可以具有异步备份:您可以使用该async-backup-count元素定义异步备份的数量。
要设置队列的最大大小,请使用该max-size元素。要在一段时间后清除未使用或空的队列,请使用该empty-queue-ttl元素。如果您为empty-queue-ttl元素定义一个值(以秒为单位的时间),那么如果您的队列在您提供的时间内保持为空或未使用,则会销毁该队列。
以下是队列配置元素的完整列表及其说明。
- max-size:队列中的最大项目数。它用于设置队列的上限。无论是否配置了队列存储,当队列达到此最大大小时,您将无法放置更多项目。
- backup-count:同步备份的数量。队列是一种非分区数据结构,因此队列的所有条目都驻留在一个分区中。当此参数为“1”时,表示该群集中的另一个成员中将存在该队列的一个备份。当它为'2'时,两个成员将拥有备份。
- async-backup-count:异步备份的数量。
- empty-queue-ttl:用于清除未使用或空的队列。如果为此元素定义一个值(以秒为单位的时间),那么如果该队列在该时间内保持为空或未使用,则将销毁该队列。
- item-listeners:为队列项添加侦听器(侦听器类)。您还可以将属性设置include-value到true,如果你想要的项目活动包含的项目值,并且可以设置local到true,如果你要听本地成员的项目。
- queue-store:包括队列存储工厂类名称和属性 二进制,内存限制和批量加载。请参阅使用持久性数据存储区排队。
- statistics-enabled:如果设置为true,则可以使用该方法检索此队列的统计信息getLocalQueueStats()。
- quorum-ref :您希望此队列使用的仲裁配置的名称。
Hazelcast MultiMap是一个专门的地图,您可以在一个键下存储多个值。就像Hazelcast中的任何其他分布式数据结构实现一样,MultiMap是分布式和线程安全的。
由于方法签名的不同,Hazelcast MultiMap不是一种实现java.util.Map方式。它支持Hazelcast Map的大多数功能,但索引,谓词和MapLoader / MapStore除外。然而,与Hazelcast Map一样,条目几乎均匀地分布在所有集群成员上。当新成员加入群集时,将应用分布式映射中使用的相同所有权逻辑。
以下示例创建MultiMap并将项目放入其中。使用HazelcastInstance getMultiMap方法获取MultiMap,然后使用MultiMap put方法将条目放入MultiMap。
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
MultiMap <String , String > map = hazelcastInstance.getMultiMap( "map" );
map.put( "a", "1" );
map.put( "a", "2" );
map.put( "b", "3" );
System.out.println( "PutMember:Done" );
```
Now let's print the entries in this MultiMap.
```java
public class PrintMember {
public static void main(String[] args) {
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
MultiMap<String, String> map = hazelcastInstance.getMultiMap("map");
map.put("a", "1");
map.put("a", "2");
map.put("b", "3");
System.out.printf("PutMember:Done");
for (String key: map.keySet()){
Collection <String> values = map.get(key);
System.out.printf("%s -> %s\n", key, values);
}
}
}
运行第一个代码示例后,运行该PrintMember示例。您将看到该键a有两个值,如下所示。
b → [3]
a → [2, 1]
Hazelcast MultiMap用于EntryListener侦听在MultiMap 中添加,更新或删除条目时发生的事件。有关如何创建条目侦听器类并进行注册的信息,请参阅“ 侦听MultiMap事件”部分。
使用MultiMap时,值的集合类型可以是Set或List。使用valueCollectionType参数配置集合类型。如果选择Set,则不允许在集合中使用重复值和空值,并且排序无关紧要。如果您选择List,则排序是相关的,您的集合可以包含重复值和空值。
您还可以使用statisticsEnabled参数为MultiMap启用统计信息。如果启用statisticsEnabled,则可以使用getLocalMultiMapStats()方法检索统计信息。
目前,MultiMap数据结构不支持逐出。 |
以下是MultiMap配置示例。
声明:
<multimap name="default">
<backup-count>0</backup-count>
<async-backup-count>1</async-backup-count>
<value-collection-type>SET</value-collection-type>
<entry-listeners>
<entry-listener include-value="false" local="false" >com.hazelcast.examples.EntryListener</entry-listener>
</entry-listeners>
<quorum-ref>quorumname</quorum-ref>
</multimap>
程序化:
MultiMapConfig mmConfig = new MultiMapConfig();
mmConfig.setName( "default" )
.setBackupCount( "0" ).setAsyncBackupCount( "1" )
.setValueCollectionType( "SET" )
.setQuorumName( "quorumname" );
以下是配置元素及其说明:
- backup-count:定义同步备份的数量。例如,如果将其设置为1,则分区的备份将放置在另一个成员上。如果是2,它将被放置在另外两个成员上。
- async-backup-count:异步备份的数量。行为与backup-count元素的行为相同。
- statistics-enabled:您可以通过将此参数的值设置为“true”来检索某些统计信息,例如拥有的条目计数,备份条目计数,上次更新时间和锁定的条目计数。检索统计信息的方法是getLocalMultiMapStats()。
- value-collection-type:值集合的类型。它可以是SET或LIST。
- entry-listeners:允许您为映射条目添加侦听器(侦听器类)。如果希望item事件包含条目值,也可以将属性include-value设置为true;如果要监听本地成员上的条目,则可以将local设置为true。
- quorum-ref:您希望此MultiMap使用的仲裁配置的名称。请参阅MultiMap和TransactionalMultiMap的Split-Brain Protection部分。
7.4.3。MultiMap和TransactionalMultiMap的裂脑保护
可以将MultiMap和TransactionalMultiMap配置为在应用其操作之前检查可用成员的最小数量(请参阅裂脑保护)。这是一项检查,以避免在网络分区期间对群集的所有部分执行成功的队列操作。
以下是现在支持裂脑保护检查的方法列表。该列表按仲裁类型分组。
多重映射:
- WRITE,READ_WRITE:
- clear
- forceUnlock
- lock
- put
- remove
- tryLock
- unlock
- READ,READ_WRITE:
- containsEntry
- containsKey
- containsValue
- entrySet
- get
- isLocked
- keySet
- localKeySet
- size
- valueCount
- values
TransactionalMultiMap:
- WRITE,READ_WRITE:
- put
- remove
- READ,READ_WRITE:
- size
- get
- valueCount
配置裂脑保护
可以使用该方法以编程方式配置MultiMap的Split-Brain保护setQuorumName(),或以声明方式使用该元素quorum-ref。以下是声明性配置示例:
<multimap name="default">
...
<quorum-ref>quorumname</quorum-ref>
...
</multimap>
值quorum-ref应该是您在quorum元素下配置的仲裁配置名称,如“ 裂脑保护”部分中所述。
Hazelcast Set是一个分布式和并发的实现java.util.Set。
- Hazelcast Set不允许重复元素。
- Hazelcast Set不保留元素的顺序。
- Hazelcast Set是一种非分区数据结构 - 属于一个集合的所有数据都将存在于该成员中的一个分区上。
- Hazelcast Set无法超出单台机器的容量。由于整个集合位于单个分区上,因此在单个集合上存储大量数据可能会导致内存压力。因此,您应该使用多个集来存储大量数据。这样,所有集合将分布在集群中,共享负载。
- Hazelcast Set的备份存储在群集中另一个成员的分区上,以便在主成员发生故障时数据不会丢失。
- 所有项目都复制到本地成员,迭代在本地进行。
- Hazelcast Set中实现的equals方法使用序列化字节版本的对象,而不是java.util.HashSet。
使用HazelcastInstance getSet方法获取Set,然后使用该add方法将项目放入Set中。
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
Set<Price> set = hazelcastInstance.getSet( "IBM-Quote-History" );
set.add( new Price( 10, time1 ) );
set.add( new Price( 11, time2 ) );
set.add( new Price( 12, time3 ) );
set.add( new Price( 11, time4 ) );
//....
Iterator<Price> iterator = set.iterator();
while ( iterator.hasNext() ) {
Price price = iterator.next();
//analyze
}
Hazelcast Set用于ItemListener侦听在Set 中添加和删除项目时发生的事件。有关如何创建项侦听器类并进行注册的信息,请参阅“ 侦听项目事件”部分。
以下是示例集配置。
声明:
<set name="default">
<backup-count>1</backup-count>
<async-backup-count>0</async-backup-count>
<max-size>10</max-size>
<item-listeners>
<item-listener>com.hazelcast.examples.ItemListener</item-listener>
</item-listeners>
<quorum-ref>quorumname</quorum-ref>
</set>
程序化:
Config config = new Config();
CollectionConfig collectionSet = config.getSetConfig("MySet");
collectionSet.setBackupCount(1)
.setMaxSize(10)
.setQuorumName( "quorumname" );
设置配置具有以下元素。
- statistics-enabled:如果在Set上启用了统计信息收集,则为True(默认值),否则为false。
- backup-count:同步备份计数。Set是一个非分区数据结构,因此Set的所有条目都驻留在一个分区中。当此参数为“1”时,表示该集群中的另一个成员中将存在该Set的一个备份。当它为'2'时,两个成员将拥有备份。
- async-backup-count:异步备份计数。
- max-size:此Set的最大条目数。它可以是介于0和Integer.MAX_VALUE之间的任何数字。其默认值为0,表示没有容量限制。
- item-listeners:允许您为列表项添加侦听器(侦听器类)。您还可以设置属性include-value来true,如果你想要的项目活动包含的项目值,并且可以设置local到true,如果你要听本地成员的项目。
- quorum-ref:您希望此Set使用的仲裁配置的名称。请参阅ISet和TransactionalSet的Split-Brain Protection部分。
7.5.3。ISet和TransactionalSet的分裂脑保护
可以将ISet和TransactionalSet配置为在应用队列操作之前检查可用成员的最小数量(请参阅裂脑保护)。这是一项检查,以避免在网络分区期间对群集的所有部分执行成功的队列操作。
以下是现在支持裂脑保护检查的方法列表。该列表按仲裁类型分组。
我设置:
- WRITE,READ_WRITE:
- add
- addAll
- clear
- remove
- removeAll
- READ,READ_WRITE:
- contains
- containsAll
- isEmpty
- iterator
- size
- toArray
TransactionalSet:
- WRITE,READ_WRITE:
- add
- remove
- READ,READ_WRITE:
- size
配置裂脑保护
可以使用该方法以编程方式配置ISet的Split-Brain保护setQuorumName(),或以声明方式使用该元素quorum-ref。以下是声明性配置示例:
<set name="default">
...
<quorum-ref>quorumname</quorum-ref>
...
</set>
值quorum-ref应该是您在quorum元素下配置的仲裁配置名称,如“ 裂脑保护”部分中所述。
Hazelcast List(IList)类似于Hazelcast Set,但Hazelcast List也允许重复元素。
- 除了允许重复元素外,Hazelcast List还保留了元素的顺序。
- Hazelcast List是一种非分区数据结构,其中值和每个备份由它们自己的单个分区表示。
- Hazelcast List无法超出单台机器的容量。
- 所有项目都复制到本地,迭代在本地进行。
虽然IMap和ICache是Hazelcast Jet使用的推荐数据结构,但IList也可以用于单元测试或类似的非生产情况。请参阅这里的Hazelcast喷气参考手册中学习喷气如何使用IList的,例如,它怎么能填充数据的IList,消耗它在一个Jet工作,结果漏到另一个IList中。另请参阅Hazelcast Jet 的快速批处理和实时流处理用例。 |
使用HazelcastInstance getList方法获取List,然后使用该add方法将项目放入List。
HazelcastInstance hz = Hazelcast.newHazelcastInstance();
List<Price> list = hz.getList( "IBM-Quote-Frequency" );
list.add( new Price( 10 ) );
list.add( new Price( 11 ) );
list.add( new Price( 12 ) );
list.add( new Price( 11 ) );
list.add( new Price( 12 ) );
//....
Iterator<Price> iterator = list.iterator();
while ( iterator.hasNext() ) {
Price price = iterator.next();
//analyze
}
Hazelcast List用于ItemListener侦听在列表中添加和删除项目时发生的事件。有关如何创建项侦听器类并进行注册的信息,请参阅“ 侦听项目事件”部分。
以下是示例列表配置。
声明:
<list name="default">
<backup-count>1</backup-count>
<async-backup-count>0</async-backup-count>
<max-size>10</max-size>
<item-listeners>
<item-listener>
com.hazelcast.examples.ItemListener
</item-listener>
</item-listeners>
<quorum-ref>quorumname</quorum-ref>
</list>
程序化:
Config config = new Config();
CollectionConfig collectionList = config.getListConfig("MyList");
collectionList.setBackupCount(1)
.setMaxSize(10)
.setQuorumName( "quorumname" );
列表配置具有以下元素。
- statistics-enabled:如果在列表上启用了统计信息收集,则为True(默认值),否则为false。
- backup-count:同步备份的数量。List是一个非分区数据结构,因此List的所有条目都驻留在一个分区中。当此参数为“1”时,群集中的另一个成员中将有一个该列表的备份。当它为'2'时,两个成员将拥有备份。
- async-backup-count:异步备份的数量。
- max-size:此List的最大条目数。
- item-listeners:允许您为列表项添加侦听器(侦听器类)。您还可以设置该属性include-value来true,如果你想要的项目活动包含的项目值,并且可以设置该属性local来true,如果你要听本地成员的项目。
- quorum-ref:您希望此List使用的仲裁配置的名称。请参阅IList和TransactionalList的Split-Brain Protection部分。
7.6.3。IList和TransactionalList的裂脑保护
可以将IList和TransactionalList配置为在应用队列操作之前检查可用成员的最小数量(请参阅裂脑保护)。这是一项检查,以避免在网络分区期间对群集的所有部分执行成功的队列操作。
以下是现在支持裂脑保护检查的方法列表。该列表按仲裁类型分组。
IList中:
- WRITE,READ_WRITE:
- add
- addAll
- clear
- remove
- removeAll
- set
- READ,READ_WRITE:
- add
- contains
- containsAll
- get
- indexOf
- isEmpty
- iterator
- lastIndexOf
- listIterator
- size
- subList
- toArray
TransactionalList:
- WRITE,READ_WRITE:
- add
- remove
- READ,READ_WRITE:
- size
配置裂脑保护
可以使用该方法以编程方式配置IList的Split-Brain保护setQuorumName(),或以声明方式使用该元素quorum-ref。以下是声明性配置示例:
<list name="default">
...
<quorum-ref>quorumname</quorum-ref>
...
</list>
值quorum-ref应该是您在quorum元素下配置的仲裁配置名称,如“ 裂脑保护”部分中所述。
Hazelcast Ringbuffer是一种复制但未分区的数据结构,可将其数据存储在类似环状的结构中。您可以将其视为具有给定容量的圆形阵列。每个Ringbuffer都有尾巴和头部。尾部是添加项目的位置,头部是项目被覆盖或过期的位置。您可以使用序列ID到达Ringbuffer中的每个元素,序列ID映射到Ringbuffer的head和tail(包括)之间的元素。
从Ringbuffer读取很简单:使用HazelcastInstance getRingbuffer方法获取Ringbuffer,使用该方法获取其当前头部headSequence,然后开始阅读。使用该方法readOne以给定的顺序返回项目; readOne如果没有项目可用则阻止 要读取下一个项目,请将序列递增1。
Ringbuffer<String> ringbuffer = hz.getRingbuffer("rb");
long sequence = ringbuffer.headSequence();
while(true){
String item = ringbuffer.readOne(sequence);
sequence++;
// process item
}
通过显示序列,只要该项目仍然可用,您现在可以从Ringbuffer移动项目。如果该项目不再可用,StaleSequenceException则抛出。
使用Ringbuffer add方法也可以轻松地将项添加到Ringbuffer :
Ringbuffer<String> ringbuffer = hz.getRingbuffer("rb");
ringbuffer.add("someitem");
使用该方法add返回插入项的序列; 序列值将始终是唯一的。如果您已经在使用Ringbuffer,则可以将此作为生成唯一ID的非常便宜的方式。
Hazelcast Ringbuffer有时候比Hazelcast IQueue更好。与IQueue不同,Ringbuffer不会删除项目,它只使用特定位置读取项目。这种方法有许多优点:
- 同一个项目可以由同一个线程多次读取。这对于实现read-at-least-once或read-at-most-once的语义非常有用。
- 多个线程可以读取相同的项目。通常,您可以为每个线程使用相同语义的IQueue,但由于远程处理增加,效率较低。从IQueue中获取是破坏性的,因此需要将更改应用于备份,这就是为什么a queue.take()比a更贵ringBuffer.read(…)。
- 读取非常便宜,因为Ringbuffer没有变化。因此,不需要复制。
- 可以批量读取和写入以加快性能。批处理可以显着提高Ringbuffer的性能。
默认情况下,Ringbuffer配置有capacity10000个项目。这将创建一个大小为10000的数组。如果time-to-live配置了a ,则还会创建一个long数组,用于存储每个项的到期时间。在很多情况下,您可能希望将此capacity数字更改为更符合您需求的数字。
下面是一个具有capacity2000个项目的Ringbuffer的声明性配置示例。
<ringbuffer name="rb">
<capacity>2000</capacity>
</ringbuffer>
目前,Hazelcast Ringbuffer不是分区数据结构; 其数据存储在单个分区中,副本存储在另一个分区中。因此,创建一个可以安全地适合单个集群成员的Ringbuffer。
Hazelcast Ringbuffer默认具有单个同步备份。您可以通过设置同步和异步备份来控制Ringbuffer备份,就像大多数其他Hazelcast分布式数据结构一样:backup-count和async-backup-count。在下面的示例中,Ringbuffer配置为没有同步备份和一个异步备份:
<ringbuffer name="rb">
<backup-count>0</backup-count>
<async-backup-count>1</async-backup-count>
</ringbuffer>
异步备份可能会为您提供更好的性能。但是,如果成员在备份完成之前拥有主要崩溃,则添加的项目可能会丢失。如果您需要高性能但不想放弃一致性,您可能需要考虑批处理方法。
您可以在几秒钟内配置Hazelcast Ringbuffer。使用此设置,您可以控制项目在过期前保留在Ringbuffer中的时间。默认情况下,生存时间设置为0,这意味着除非项目被覆盖,否则它将无限期地保留在Ringbuffer中。如果您设置了生存时间并添加了项目,则根据溢出策略,覆盖最旧的项目,或者拒绝呼叫。
在下面的示例中,Ringbuffer配置的生存时间为180秒。
<ringbuffer name="rb">
<time-to-live-seconds>180</time-to-live-seconds>
</ringbuffer>
使用溢出策略,您可以确定如果Ringbuffer中最旧的项目不够大,可以在添加的项目超过配置的Ringbuffer容量时过期。以下选项目前可用。
- OverflowPolicy.OVERWRITE:最旧的项目被覆盖。
- OverflowPolicy.FAIL:呼叫中止。使用OverflowPolicy返回的-1方法表示添加项目失败。
如果Ringbuffer已满,溢出策略可以很好地控制该怎么做。您还可以使用溢出策略应用反压机制。以下示例代码显示了指数退避的用法。
long sleepMs = 100;
for (; ; ) {
long result = ringbuffer.addAsync(item, OverflowPolicy.FAIL).get();
if (result != -1) {
break;
}
TimeUnit.MILLISECONDS.sleep(sleepMs);
sleepMs = min(5000, sleepMs * 2);
}
Hazelcast允许您使用该界面从/向持久性数据存储区加载和存储Ringbuffer项目RingbufferStore。如果启用了Ringbuffer存储,则添加到Ringbuffer的每个项目也将存储在配置的Ringbuffer存储中。
如果配置了Ringbuffer存储,您可以获得序列不再在实际Ringbuffer中但仅在Ringbuffer存储中的项目。这可能会慢很多,但仍然允许你继续使用Ringbuffer中的项目,即使它们被Ringbuffer中的新项目覆盖。
当一个Ringbuffer被实例化时,它将检查是否配置了Ringbuffer存储并将在Ringbuffer存储中请求最新的序列。这是为了使Ringbuffer能够以大于Ringbuffer存储中的序列开始。在这种情况下,Ringbuffer为空,但您仍然可以从中请求较旧的项目(将从Ringbuffer存储中加载)。
Ringbuffer商店将以与Ringbuffer相同的格式存储商品。如果BINARY使用内存格式,则Ringbuffer存储必须实现接口,RingbufferStore<byte[]>这意味着Ringbuffer将接收二进制格式的项目。如果OBJECT使用内存格式,则Ringbuffer存储必须实现接口RingbufferStore<K>,其中K是存储的项的类型(意味着Ringbuffer存储将接收反序列化的对象)。
将项目添加到Ringbuffer时,该方法storeAll允许您批量存储项目。
以下示例类包括所有RingbufferStore方法。
public class TheRingbufferObjectStore implements RingbufferStore<Item> {
@Override
public void store(long sequence, Item data) {
System.out.println("Object store");
}
@Override
public void storeAll(long firstItemSequence, Item[] items) {
System.out.println("Object store all");
}
@Override
public Item load(long sequence) {
System.out.println("Object load");
return null;
}
@Override
public long getLargestSequence() {
System.out.println("Object get largest sequence");
return -1;
}
}
Item必须是可序列化的。以下是配置并启用了Ringbuffer存储的Ringbuffer示例。
<ringbuffer name="default">
<capacity>10000</capacity>
<time-to-live-seconds>30</time-to-live-seconds>
<backup-count>1</backup-count>
<async-backup-count>0</async-backup-count>
<in-memory-format>BINARY</in-memory-format>
<ringbuffer-store>
<class-name>com.hazelcast.RingbufferStoreImpl</class-name>
</ringbuffer-store>
</ringbuffer>
以下是Ringbuffer商店配置元素的说明:
- class-name:实现RingbufferStore接口的类的名称。
- factory-class-name:实现RingbufferStoreFactory接口的类的名称。此接口允许注册工厂类,而不是实现RingbufferStore接口的类。
无论是class-name或factory-class-name应该使用元件。
您可以使用内存格式配置Hazelcast Ringbuffer,以控制Ringbuffer存储项目的格式。默认情况下,BINARY使用内存中格式,这意味着对象以序列化形式存储。您可以选择OBJECT内存中的格式,这在应用过滤或OBJECT内存中格式的内存占用量小时非常有用BINARY。
在下面的声明性配置示例中,Ringbuffer配置了OBJECT内存格式:
<ringbuffer name="rb">
<in-memory-format>OBJECT</in-memory-format>
</ringbuffer>
7.7.10。配置Ringbuffer的Split-Brain保护
Ringbuffer可以配置为在应用Ringbuffer操作之前检查可用成员的最小数量。这是一项检查,以避免在网络分区期间对群集的所有部分执行成功的Ringbuffer操作,并且可以使用该元素进行配置quorum-ref。您应该将此元素的值设置为仲裁的名称,您在该quorum元素下配置,如“ 裂脑保护”部分中所述。以下是一个示例代码段:
<ringbuffer name="rb">
<quorum-ref>quorumname</quorum-ref>
</ringbuffer>
以下是现在支持裂脑保护检查的方法列表。该列表按仲裁类型分组。
- WRITE,READ_WRITE:
- add
- addAllAsync
- addAsync
- READ,READ_WRITE:
- capacity
- headSequence
- readManyAsync
- readOne
- remainingCapacity
- size
- tailSequence
在前面的示例中,该方法ringBuffer.add()用于将项添加到Ringbuffer。这种方法的问题是它总是覆盖它并且它不支持批处理。批处理可以对性能产生巨大影响。您可以使用该方法addAllAsync来支持批处理。
请参阅以下示例代码。
List<String> items = Arrays.asList("1","2","3");
ICompletableFuture<Long> f = rb.addAllAsync(items, OverflowPolicy.OVERWRITE);
f.get();
在上面的例子中,使用策略将三个字符串添加到Ringbuffer OverflowPolicy.OVERWRITE。有关详细信息,请参阅溢出策略部分。
在前面的示例中,该readOne方法从Ringbuffer中读取项目。readOne由于以下原因,它很简单但效率不高:
- readOne 不使用批处理。
- readOne不能过滤来源的物品; 在过滤之前需要检索项目。
该方法readManyAsync可以读取一批项目,并可以在源处过滤项目。
请参阅以下示例代码。
ICompletableFuture<ReadResultSet<E>> readManyAsync(
long startSequence,
int minCount,
int maxCount,
IFunction<E, Boolean> filter);
readManyAsync论证的含义如下。
- startSequence:要读取的第一个项目的序列。
- minCount:要读取的最小项目数。如果您不想阻止,请将其设置为0.如果要阻止至少一个项目,请将其设置为1。
- maxCount:要检索的最大项目数。它的价值不能超过1000。
- filter:一个接受项目并检查是否应该返回的函数。如果不应用过滤,请将其设置为null。
下面给出一个完整的例子。
long sequence = rb.headSequence();
for(;;) {
ICompletableFuture<ReadResultSet<String>> f = rb.readManyAsync(sequence, 1, 10, null);
ReadResultSet<String> rs = f.get();
for (String s : rs) {
System.out.println(s);
}
sequence+=rs.readCount();
}
请仔细查看序列的递增方式。如果过滤掉项目,则不能总是依赖于返回的项目数。
Hazelcast Ringbuffer为更强大的操作提供异步方法,例如批量写入或带过滤的批量读取。要使这些方法同步,只需get()在返回的将来调用该方法。
请参阅以下示例代码。
ICompletableFuture f = ringbuffer.addAsync(item, OverflowPolicy.FAIL);
f.get();
但是,您也可以使用ICompletableFuture在操作完成时收到通知。优点ICompletableFuture是在返回响应之前不会阻塞用于调用的线程。
请参阅以下代码,作为您希望在完成一批读取时收到通知的示例。
ICompletableFuture<ReadResultSet<String>> f = rb.readManyAsync(sequence, min, max, someFilter);
f.andThen(new ExecutionCallback<ReadResultSet<String>>() {
@Override
public void onResponse(ReadResultSet<String> response) {
for (String s : response) {
System.out.println("Received:" + s);
}
}
@Override
public void onFailure(Throwable t) {
t.printStackTrace();
}
});
以下显示了一个名为的Ringbuffer的声明性配置rb。配置是在Ringbuffer默认设置之后建模的。
<ringbuffer name="rb">
<capacity>10000</capacity>
<backup-count>1</backup-count>
<async-backup-count>0</async-backup-count>
<time-to-live-seconds>0</time-to-live-seconds>
<in-memory-format>BINARY</in-memory-format>
<quorum-ref>quorumname</quorum-ref>
</ringbuffer>
您还可以通过编程方式配置Ringbuffer。以下是上述声明性配置的编程版本。
RingbufferConfig rbConfig = new RingbufferConfig("rb")
.setCapacity(10000)
.setBackupCount(1)
.setAsyncBackupCount(0)
.setTimeToLiveSeconds(0)
.setInMemoryFormat(InMemoryFormat.BINARY)
.setQuorumName("quorumname");
Config config = new Config();
config.addRingbufferConfig(rbConfig);
Hazelcast提供了一种分发机制,用于发布传递给多个订阅者的消息。这也称为发布/订阅(发布/订阅)消息传递模型。发布和订阅操作是集群范围的。当成员订阅某个主题时,它实际上是在注册群集中任何成员发布的消息,包括添加侦听器后加入的新成员。
发布操作是异步的。它不等待操作在远程成员中运行; 它像火一样忘记。 |
使用HazelcastInstance getTopic方法获取主题,然后使用topic publish方法发布消息(messageObject)。
public class Sample implements MessageListener<MyEvent> {
public static void main( String[] args ) {
Sample sample = new Sample();
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
ITopic topic = hazelcastInstance.getTopic( "default" );
topic.addMessageListener( sample );
topic.publish( new MyEvent() );
}
public void onMessage( Message<MyEvent> message ) {
MyEvent myEvent = message.getMessageObject();
System.out.println( "Message received = " + myEvent.toString() );
if ( myEvent.isHeavyweight() ) {
messageExecutor.execute( new Runnable() {
public void run() {
doHeavyweightStuff( myEvent );
}
} );
}
}
// ...
private final Executor messageExecutor = Executors.newSingleThreadExecutor();
}
Hazelcast Topic使用该MessageListener接口来侦听收到消息时发生的事件。有关如何创建消息监听器类并进行注册的信息,请参阅“ 监听主题消息”部分。
主题有两个可以查询的统计变量。这些值是增量的并且是成员的本地值。
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
ITopic<Object> myTopic = hazelcastInstance.getTopic( "myTopicName" );
myTopic.getLocalTopicStats().getPublishOperationCount();
myTopic.getLocalTopicStats().getReceiveOperationCount();
getPublishOperationCount并分别getReceiveOperationCount返回自该成员开始以来发布和接收的消息总数。请注意,这些值不会备份,因此如果成员发生故障,这些值将会丢失。
您可以使用主题配置禁用此功能。请参阅配置主题部分。
也可以在管理中心中查看这些统计值。请参阅“ Hazelcast管理中心参考手册”中的“ 监控主题”部分。 |
每个集群成员都有一个集群中所有注册的列表。为主题注册新成员时,它会向群集中的所有成员发送注册消息。此外,当新成员加入群集时,它将接收到目前为止在群集中进行的所有注册。
主题的行为取决于配置参数的值globalOrderEnabled。
如果globalOrderEnabled禁用,则不会对消息进行排序,并且侦听器(订阅者)按照发布消息的顺序处理消息。如果集群成员M将消息m1,m2,m3,...,mn发布到主题T,则Hazelcast确保主题T的所有订阅者将接收并处理给定的m1,m2,m3,...,mn订购。
下面是它的工作原理。假设我们有三个成员(member1,member2和member3),并且member1和member2已注册到一个名为的主题news。请注意,所有三个成员都知道member1和member2已注册news。
在这个例子中,member1中发布两个消息,a1并且a2,和member3出版两个消息,c1和c2。当member1和member3发布消息时,他们将检查其注册成员的本地列表,他们将发现member1和member2在他们的列表中,然后他们将向这些成员发送消息。收到的消息的一种可能顺序可能如下。
member1中 → c1,a1,a2,c2
member2 → c1,c2,a1,a2
如果globalOrderEnabled启用,则所有收听同一主题的成员将以相同的顺序获取其消息。
下面是它的工作原理。假设我们有三个成员(member1,member2和member3),并且member1和member2已注册到一个名为的主题news。请注意,所有三个成员都知道member1和member2已注册news。
在此示例中,member1发布两条消息:a1和a2,member3发布两条消息:c1和c2。当成员通过主题发布消息时news,它首先计算newsID对应的分区。然后,它向该成员的分区所有者发送操作以发布消息。我们假设它news对应于member2拥有的分区。member1和member3首先将所有消息发送到member2。假设消息按以下顺序发布:
member1中 → a1,c1,a2,c2
然后,member2通过查看其本地列表中的注册来发布这些消息。它将这些消息发送到member1和member2(它为自己进行本地调度)。
member1中 → a1,c1,a2,c2
member2 → a1,c1,a2,c2
这样我们保证所有成员都能以相同的顺序查看事件。
在这两种情况下,StripedExecutorEventService中都有一个负责调度接收消息的事件。对于Hazelcast中的所有事件,生成事件的顺序以及它们发布给用户的顺序通过此保证是相同的StripedExecutor。
在StripedExecutor,属性中指定的线程数 hazelcast.event.thread.count(默认值为5)。对于特定事件源(对于特定主题名称),该源名称%5的哈希值给出了负责线程的ID。请注意,可以存在与同一线程对应的另一个事件源(映射的条目侦听器,集合的项侦听器等)。为了不阻止其他消息,不应在此线程中进行繁重的处理。如果需要完成耗时的工作,则应将工作移交给另一个线程。请参阅获取主题和发布消息部分。
要配置主题,请设置主题名称,确定统计信息和全局排序,以及设置消息侦听器。默认值为:
- global-ordering是错误的,这意味着默认情况下,不保证全球秩序。
- statistics是真实的,这意味着在默认情况下,统计数据进行计算。
您可以在下面看到示例配置代码段。
声明:
<hazelcast>
...
<topic name="yourTopicName">
<global-ordering-enabled>true</global-ordering-enabled>
<statistics-enabled>true</statistics-enabled>
<message-listeners>
<message-listener>MessageListenerImpl</message-listener>
</message-listeners>
</topic>
...
</hazelcast>
程序化:
TopicConfig topicConfig = new TopicConfig();
topicConfig.setGlobalOrderingEnabled( true );
topicConfig.setStatisticsEnabled( true );
topicConfig.setName( "yourTopicName" );
MessageListener<String> implementation = new MessageListener<String>() {
@Override
public void onMessage( Message<String> message ) {
// process the message
}
};
topicConfig.addMessageListenerConfig( new ListenerConfig( implementation ) );
HazelcastInstance instance = Hazelcast.newHazelcastInstance()
主题配置具有以下元素。
- statistics-enabled:默认为true,表示计算统计信息。
- global-ordering-enabled:默认为false,表示没有全局订单保证。
- message-listeners:允许您为主题消息添加侦听器(侦听器类)。
除上述元素外,还有以下与主题相关但不是主题特定的系统属性:
- hazelcast.event.queue.capacity 默认值为1,000,000
- hazelcast.event.queue.timeout.millis 默认值为250
- hazelcast.event.thread.count 默认值为5
有关这些参数的说明,请参阅<< global-event-configuration,全局事件配置部分。
可靠主题数据结构在Hazelcast 3.5中引入。Reliable Topic使用与ITopic常规主题相同的界面。主要区别在于Reliable Topic由Ringbuffer(也是Hazelcast 3.5引入)数据结构支持。以下是此方法的优点:
- 由于Ringbuffer默认配置了一个同步备份,因此事件不会丢失。
- 每个Reliable ITopic都有自己的Ringbuffer; 如果一个主题有一个非常快的制作人,它不会导致以较慢的速度运行的主题出现问题。
- 由于常规后面的事件系统ITopic与其他数据结构(例如集合侦听器)共享,因此可能会遇到隔离问题。Reliable不会发生这种情况ITopic。
public class Sample implements MessageListener<MyEvent> {
public static void main( String[] args ) {
Sample sample = new Sample();
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
ITopic topic = hazelcastInstance.getReliableTopic( "default" );
topic.addMessageListener( sample );
topic.publish( new MyEvent() );
}
public void onMessage( Message<MyEvent> message ) {
MyEvent myEvent = message.getMessageObject();
System.out.println( "Message received = " + myEvent.toString() );
}
}
您可以ITopic使用其Ringbuffer 配置Reliable 。如果可靠主题具有名称Foo,则可以通过添加ReliableTopicConfig具有名称的Ringbuffer 来配置此主题Foo。默认情况下,Ringbuffer没有任何TTL(生存时间)且容量有限; 您可能想要更改该配置。
默认情况下,Reliable ITopic使用共享线程池。如果需要更好的隔离,可以在上面配置自定义执行程序 ReliableTopicConfig。
因为Ringbuffer上的读取不具有破坏性,所以批处理很容易应用。ITopic默认情况下,使用读取批处理并一次读取10个项目(如果可用)。
Reliable ITopic提供控制和处理缓慢消费者的方式。将速度慢的消费者的事件无限期地保存在内存中是不明智的,因为你不知道慢速消费者什么时候会赶上来。您可以使用其容量来控制Ringbuffer的大小。对于Ringbuffer超出其容量的情况,您可以为TopicOverloadPolicy配置指定以下策略:
- DISCARD_OLDEST:即使设置了TTL,也会覆盖最旧的项目。在这种情况下,快速生产者取代了缓慢的消费者。
- DISCARD_NEWEST:丢弃最新的项目。
- BLOCK:等到Ringbuffer中的项目过期。
- ERROR:TopicOverloadException如果Ringbuffer中没有空格,立即抛出。
以下是可靠主题配置示例。
声明:
<reliable-topic name="default">
<statistics-enabled>true</statistics-enabled>
<message-listeners>
<message-listener>
...
</message-listener>
</message-listeners>
<read-batch-size>10</read-batch-size>
<topic-overload-policy>BLOCK</topic-overload-policy>
</reliable-topic>
程序化:
Config config = new Config();
ReliableTopicConfig rtConfig = config.getReliableTopicConfig( "default" );
rtConfig.setTopicOverloadPolicy( TopicOverloadPolicy.BLOCK )
.setReadBatchSize( 10 )
.setStatisticsEnabled( true );
可靠的主题配置具有以下元素。
- statistics-enabled:启用或禁用可靠主题的统计信息收集。默认值为true。
- message-listener:消息侦听器类,用于在添加或删除消息时侦听消息。
- read-batch-size:Reliable Topic将尝试批量读取的最小消息数。默认值为10。
- topic-overload-policy:处理重载主题的策略。可用值是DISCARD_OLDEST,DISCARD_NEWEST,BLOCK和ERROR。默认值为`BLOCK。有关这些策略的定义,请参阅Slow Consumers。
ILock是分布式实现java.util.concurrent.locks.Lock,意味着如果你使用ILock锁定,它保护的关键部分保证只能由整个集群中的一个线程执行,前提是没有网络故障。尽管锁定非常适合同步,但如果使用不当,它们可能会导致问题。另请注意,Hazelcast Lock不支持公平性。
始终使用带有try - catch块的锁。如果从关键部分的代码中抛出异常,这将确保释放锁。另请注意,该lock方法位于try - catch块之外,因为如果锁定操作本身失败,我们不想解锁。
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
Lock lock = hazelcastInstance.getLock( "myLock" );
lock.lock();
try {
// do something here
} finally {
lock.unlock();
}
如果群集中未释放锁定,则尝试获取锁定的另一个线程可以永久等待。要避免这种情况,请使用tryLock超时值。您可以设置一个较高的值(通常不应该花那么长时间)tryLock。您可以检查返回值,tryLock如下所示:
if ( lock.tryLock ( 10, TimeUnit.SECONDS ) ) {
try {
// do some stuff here..
} finally {
lock.unlock();
}
} else {
// warning
}
您还可以通过使用具有租约时间的锁来避免无限期地等待线程 - 锁将在给定的租约时间内释放。在租约期满之前,可以安全地解锁。请注意,IllegalMonitorStateException由于租约时间到期,解锁操作可以抛出锁定。如果是这种情况,则关键部分保证会被破坏。
请参阅以下示例。
lock.lock( 5, TimeUnit.SECONDS )
try {
// do some stuff here..
} finally {
try {
lock.unlock();
} catch ( IllegalMonitorStateException ex ){
// WARNING Critical section guarantee can be broken
}
}
您还可以在尝试获取锁定时指定租用时间:tryLock(time, unit, leaseTime, leaseUnit)。在这种情况下,它会尝试在指定的租约时间内获取锁定。如果锁定不可用,则当前线程将被禁用以进行线程调度,直到获取锁定或指定的等待时间过去为止。请注意,此租约时间不能超过您使用系统属性指定的时间hazelcast.lock.max.lease.time.seconds。请参阅“ 系统属性”附录以查看此属性的说明并了解如何设置系统属性。
- 锁是故障安全的。如果成员持有锁并且某些其他成员关闭,则群集将保持您的锁安全且可用。此外,当成员离开集群时,将删除该死成员获取的所有锁,以便这些锁立即可用于实时成员。
- 锁是可重入的。同一个线程可以在同一个锁上多次锁定。请注意,对于其他线程可以要求此锁定,锁定的所有者必须调用unlock所有者调用的次数lock。
- 在裂脑情景中,群集的行为就像是两个不同的群集。由于两个单独的集群彼此不了解,因此来自不同集群的两个成员可以获得相同的锁。有关可以处理裂脑综合症的地方的更多信息,请参阅Split-Brain综合症。
- 锁不会自动删除。如果不再使用锁,Hazelcast将不会自动垃圾收集锁。这可能会导致OutOfMemoryError。如果您动态创建锁定,请确保它们已被销毁。
- Hazelcast IMap还通过该方法在入门级提供锁定支持IMap.lock(key)。尽管使用了相同的基础结构,但IMap.lock(key)它不是ILock,也不可能直接公开它。
基于ILock的锁即使在未获取时也会使用系统资源。你必须打电话destroy(),但是当另一个线程试图获取它时,这会产生副作用。当您拥有有限数量的锁时,基于ILock的锁是一个不错的选择。基于IMap的锁是自动销毁的。没有获得资源时,他们不使用资源。这意味着它们对于监视是不可见的,除非它们被某个线程保留。
ICondition是分布式实施notify,notifyAll以及wait操作的Java对象。您可以使用它来同步群集中的线程。更具体地说,ICondition当线程的工作依赖于另一个线程的输出时使用。一个很好的例子是生产者/消费者方法。
请参阅以下代码示例,了解生产者/消费者实现。
制片人主题:
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
Lock lock = hazelcastInstance.getLock( "myLockId" );
ICondition condition = lock.newCondition( "myConditionId" );
lock.lock();
try {
while ( !shouldProduce() ) {
condition.await(); // frees the lock and waits for signal
// when it wakes up it re-acquires the lock
// if available or waits for it to become
// available
}
produce();
condition.signalAll();
} finally {
lock.unlock();
}
该方法await()将时间值和时间单位作为参数。如果为时间指定负值,则将其解释为无限。 |
消费者主题:
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
Lock lock = hazelcastInstance.getLock( "myLockId" );
ICondition condition = lock.newCondition( "myConditionId" );
lock.lock();
try {
while ( !canConsume() ) {
condition.await(); // frees the lock and waits for signal
// when it wakes up it re-acquires the lock if
// available or waits for it to become
// available
}
consume();
condition.signalAll();
} finally {
lock.unlock();
}
可以将锁配置为在应用锁定操作之前检查可用成员的最小数量(请参阅裂脑保护)。这是一项检查,以避免在网络分区期间对群集的所有部分执行成功的锁定操作。由于实现细节,检查不保证锁在网络分区的所有条件下都会失败,并且可能发生两个成员可以获得相同的锁。
一旦检测到成员资格更改,锁定操作将失败,QuorumException如果没有足够大小的成员。从本质上讲,这并不能提供正确性,而是缩小了锁定可以同时继续对多个成员进行操作的机会之窗。
尽管检查不能提供正确性,但它仍然有用。如果成员获取锁以执行某些昂贵但幂等的操作,则配置锁定仲裁可以进一步防止某些情况,其中群集已被拆分为多个子群集,并且多个成员执行相同的操作。
以下是现在支持仲裁检查的方法列表。该列表按仲裁类型分组。此外,由于Hazelcast IMap还提供锁定支持,因此某些map和multimap方法也允许进行仲裁检查。
- 写,READ_WRITE
- Condition.await(()),Condition.awaitUninterruptibly(),Condition.awaitNanos(),Condition.awaitUntil()
- Lock.lockInterruptibly(),ILock.lock(),IMap.tryLock(),IMap.lock(),MultiMap.lock(),MultiMap.tryLock()
- Condition.signal(), Condition.signalAll()
- READ,READ_WRITE
- ILock.getLockCount()
- ILock.getRemainingLeaseTime()
- ILock.isLocked(),IMap.isLocked(),MultiMap.isLocked()
- ILock.forceUnlock(),IMap.forceUnlock(),MultiMap.forceUnlock(),ILock.unlock(),IMap.unlock(),ObjectMultiMapProxy.unlock()
如锁定裂脑保护部分所述,Lock允许进行裂脑保护。
声明性配置的示例如下:
<lock name="myLock">
<quorum-ref>quorum-name</quorum-ref>
</lock>
- quorum-ref :您希望此锁使用的仲裁配置的名称。
程序化配置的示例如下:
Config config = new Config();
LockConfig lockConfig = new LockConfig();
lockConfig.setName("myLock")
.setQuorumName("quorum-name");
config.addLockConfig(lockConfig);
如上所述,具有相同名称或与映射名称匹配的模式的锁定的仲裁定义将强制映射锁定操作使用定义的仲裁。在使用锁定仲裁和映射锁定操作时,请务必牢记这一点。 |
Hazelcast IAtomicLong是分布式实现的java.util.concurrent.atomic.AtomicLong。它提供了最的AtomicLong的操作,如get,set,getAndSet,compareAndSet和incrementAndGet。由于IAtomicLong是一个分布式实现,这些操作涉及远程调用,因此它们的性能与AtomicLong不同。
以下示例代码创建一个实例,将其递增一百万,然后打印计数。
public class Member {
public static void main( String[] args ) {
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
IAtomicLong counter = hazelcastInstance.getAtomicLong( "counter" );
for ( int k = 0; k < 1000 * 1000; k++ ) {
if ( k % 500000 == 0 ) {
System.out.println( "At: " + k );
}
counter.incrementAndGet();
}
System.out.printf( "Count is %s\n", counter.get() );
}
}
当您使用上面的代码启动其他实例时,您将看到计数为成员计数乘以一百万。
您可以将功能发送到IAtomicLong。IFunction是一个Hazelcast拥有的单一方法接口。以下示例IFunction实现将两个原始值添加到原始值。
private static class Add2Function implements IFunction <Long, Long> {
@Override
public Long apply( Long input ) {
return input + 2;
}
}
您可以使用以下方法在IAtomicLong上执行函数。
- apply:将函数应用于IAtomicLong中的值,而不更改实际值并返回结果。
- alter:通过应用函数更改存储在IAtomicLong中的值。它不会发回结果。
- alterAndGet:通过应用函数更改存储在IAtomicLong中的值,将结果存储在IAtomicLong中并返回结果。
- getAndAlter:通过应用函数并返回原始值来更改存储在IAtomicLong中的值。
以下示例代码包括这些方法。
public class Member {
public static void main( String[] args ) {
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
IAtomicLong atomicLong = hazelcastInstance.getAtomicLong( "counter" );
atomicLong.set( 1 );
long result = atomicLong.apply( new Add2Function() );
System.out.println( "apply.result: " + result);
System.out.println( "apply.value: " + atomicLong.get() );
atomicLong.set( 1 );
atomicLong.alter( new Add2Function() );
System.out.println( "alter.value: " + atomicLong.get() );
atomicLong.set( 1 );
result = atomicLong.alterAndGet( new Add2Function() );
System.out.println( "alterAndGet.result: " + result );
System.out.println( "alterAndGet.value: " + atomicLong.get() );
atomicLong.set( 1 );
result = atomicLong.getAndAlter( new Add2Function() );
System.out.println( "getAndAlter.result: " + result );
System.out.println( "getAndAlter.value: " + atomicLong.get() );
}
}
运行时上述类的输出如下:
apply.result: 3
apply.value: 1
alter.value: 3
alterAndGet.result: 3
alterAndGet.value: 3
getAndAlter.result: 1
getAndAlter.value: 3
使用函数而不是简单代码行的原因atomicLong.set(atomicLong.get() + 2));是IAtomicLong读取和写入操作不是原子的。由于IAtomicLong是分布式实现,这些操作可能是远程操作,这可能导致竞争问题。通过使用函数,数据不会被拉入代码,但代码将被发送到数据。这使它更具可扩展性。
IAtomicLong有一个同步备份,没有异步备份。其备份计数不可配置。 |
可以将IAtomicLong配置为在应用其操作之前检查可用成员的最小数量(请参阅裂脑保护)。这是为了避免在网络分区期间对群集的所有部分执行成功操作的检查。以下是现在支持裂脑保护检查的方法列表。该列表按仲裁类型分组。
- WRITE,READ_WRITE:
- addAndGet
- addAndGetAsync
- alter
- alterAndGet
- alterAndGetAsync
- alterAsync
- apply
- applyAsync
- compareAndSet
- compareAndSetAsync
- decrementAndGet
- decrementAndGetAsync
- getAndAdd
- getAndAddAsync
- getAndAlter
- getAndAlterAsync
- getAndIncrement
- getAndIncrementAsync
- getAndSet
- getAndSetAsync
- incrementAndGet
- incrementAndGetAsync
- set
- setAsync
- READ,READ_WRITE:
- get
- getAsync
配置裂脑保护
可以使用该方法以编程方式配置IAtomicLong的Split-Brain保护setQuorumName(),或以声明方式使用该元素quorum-ref。以下是声明性配置示例:
<atomic-long name="default">
...
<quorum-ref>quorumname</quorum-ref>
...
</atomic-long>
值quorum-ref应该是您在quorum元素下配置的仲裁配置名称,如“ 裂脑保护”部分中所述。
Hazelcast ISemaphore是分布式实现的java.util.concurrent.Semaphore。
信号量提供许可证以在执行并发活动时控制线程计数。要执行并发活动,线程会授予许可或等待许可变为可用。执行完成后,许可证将被释放。
拥有单一许可证的ISemaphore可被视为锁定。然而,与锁不同,当使用信号量时,任何线程都可以释放许可,信号量可以有多个许可。 | |
Hazelcast ISemaphore在任何时候都不支持公平。存在一些不公平的边缘情况,例如,当内部超时发生时许可变得可用时。 |
在ISemaphore上获得许可时:
- 如果有许可证,则信号量中的许可数量减少一个,并且调用线程执行其活动。如果存在争用,则最长等待线程将在所有其他线程之前获取许可。
- 如果没有可用许可证,则调用线程将阻塞,直到许可证可用。在此块期间发生超时时,线程将被中断。
以下示例代码使用IAtomicLong资源1000次,在线程开始使用时递增资源,并在线程完成时递减。
public class SemaphoreMember {
public static void main( String[] args ) throws Exception{
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
ISemaphore semaphore = hazelcastInstance.getSemaphore( "semaphore" );
IAtomicLong resource = hazelcastInstance.getAtomicLong( "resource" );
for ( int k = 0 ; k < 1000 ; k++ ) {
System.out.println( "At iteration: " + k + ", Active Threads: " + resource.get() );
semaphore.acquire();
try {
resource.incrementAndGet();
Thread.sleep( 1000 );
resource.decrementAndGet();
} finally {
semaphore.release();
}
}
System.out.println("Finished");
}
}
让我们通过允许最多三个线程来限制对此资源的并发访问。您可以通过设置initial-permits属性以声明方式配置它,如下所示。
<semaphore name="semaphore">
<initial-permits>3</initial-permits>
</semaphore>
如果在创建信号量时缺少许可证,则可以将此属性的值设置为负数。 |
如果执行上述SemaphoreMember类5次,输出将类似于以下内容:
At iteration: 0, Active Threads: 1
At iteration: 1, Active Threads: 2
At iteration: 2, Active Threads: 3
At iteration: 3, Active Threads: 3
At iteration: 4, Active Threads: 3
如您所见,并发线程的最大数量等于或小于3。如果删除信号量获取/释放语句SemaphoreMember,您将看到并发使用数量没有限制。
Hazelcast还提供备份支持ISemaphore。当成员发生故障时,您可以让另一个成员接收带有许可信息的信号量(当成员发生故障时会自动释放许可)。为了实现这一点,配置与性能的同步或异步备份backup-count和async-backup-count(默认情况下,同步备份已经启用)。
以下是示例信号量配置。
声明:
<semaphore name="semaphore">
<backup-count>1</backup-count>
<async-backup-count>0</async-backup-count>
<initial-permits>3</initial-permits>
<quorum-ref>quorumname</quorum-ref>
</semaphore>
程序化:
Config config = new Config();
SemaphoreConfig semaphoreConfig = config.getSemaphoreConfig();
semaphoreConfig.setName( "semaphore" ).setBackupCount( "1" )
.setInitialPermits( "3" )
.setQuorumName( "quorumname" );
信号量配置具有以下元素。
- initial-permits:并发访问受限的线程计数。例如,如果将其设置为“3”,则对对象的并发访问限制为3个线程。
- backup-count:同步备份的数量。
- async-backup-count:异步备份的数量。
- quorum-ref:您希望此信号量使用的仲裁配置的名称。请参阅下面的ISemaphore分裂脑保护部分。
如果高性能比不丢失许可信息更重要,则可以通过设置backup-count为0 来禁用备份。 |
可以将ISemaphore配置为在应用其操作之前检查可用成员的最小数量(请参阅裂脑保护)。这是一项检查,以避免在网络分区期间对群集的所有部分执行成功的队列操作。
以下是现在支持裂脑保护检查的方法列表。该列表按仲裁类型分组。
- WRITE,READ_WRITE:
- acquire
- drainPermits
- init
- reducePermits
- release
- tryAcquire
- READ,READ_WRITE:
- availablePermits
配置裂脑保护
可以使用该方法以编程方式配置ISemaphore的Split-Brain保护setQuorumName(),或以声明方式使用该元素quorum-ref。以下是声明性配置示例:
<semaphore name="default">
...
<quorum-ref>quorumname</quorum-ref>
...
</semaphore>
值quorum-ref应该是您在quorum元素下配置的仲裁配置名称,如“ 裂脑保护”部分中所述。
IAtomicLong如果你需要处理很长时间,这是非常有用的,但在某些情况下你需要处理一个引用。这就是为什么Hazelcast还支持IAtomicReference分布式版本的原因java.util.concurrent.atomic.AtomicReference。
这是一个IAtomicReference示例。
public class Member {
public static void main(String[] args) {
Config config = new Config();
HazelcastInstance hz = Hazelcast.newHazelcastInstance(config);
IAtomicReference<String> ref = hz.getAtomicReference("reference");
ref.set("foo");
System.out.println(ref.get());
System.exit(0);
}
}
执行上面的示例时,您将看到以下输出。
foo
就像IAtomicLong,IAtomicReference有接受“功能”作为参数的方法,如alter,alterAndGet,getAndAlter和apply。使用这些方法有两大优势:
- 从性能的角度来看,最好将函数发送到数据然后将数据发送到函数。通常,该功能比数据小很多,因此通过线路发送更便宜。此功能只需要传输一次到目标机器,数据需要传输两次。
- 您不需要处理并发控制。如果要执行加载,转换,存储,则可能会遇到数据争用,因为另一个线程可能已更新您要覆盖的值。
以下是使用IAtomicReference时需要了解的一些注意事项。
- IAtomicReference基于字节内容而不是对象引用。如果使用该compareAndSet方法,请不要更改为原始值,因为其序列化内容将不同。同样重要的是要知道,如果您依赖Java序列化,有时(特别是使用散列图),相同的对象可能会导致不同的二进制内容。
- IAtomicReference 将始终有一个同步备份。
- 返回对象的所有方法都将返回私有副本。您可以修改私有副本,但世界其他地方将不受您的更改的影响。如果您希望这些更改对于世界其他地方可见,则需要将更改写回到IAtomicReference; 但要小心引入数据竞争。
- a的“内存格式” IAtomicReference是binary。接收方不需要具有类定义,除非需要在另一侧进行反序列化,例如,因为执行了诸如“alter”的方法。这种反序列化是为每个需要拥有对象而不是二进制内容的调用完成的,因此请注意需要反序列化的昂贵对象图。
- 如果您有一个包含许多字段或对象图的对象,并且您只需要计算某些信息或需要字段的子集,则可以使用该apply方法。使用该apply方法,整个对象不需要通过线路发送; 仅发送相关信息。
可以将IAtomicReference配置为在应用其操作之前检查可用成员的最小数量(请参阅裂脑保护)。这是为了避免在网络分区期间对群集的所有部分执行成功操作的检查。
以下是现在支持裂脑保护检查的方法列表。该列表按仲裁类型分组。
- WRITE,READ_WRITE:
- alter
- alterAndGet
- alterAndGetAsync
- alterAsync
- apply
- applyAsync
- clear
- clearAsync
- compareAndSet
- compareAndSetAsync
- getAndAlter
- getAndAlterAsync
- getAndSet
- getAndSetAsync
- set
- setAndGet
- setAsync
- READ,READ_WRITE:
- contains
- containsAsync
- get
- getAsync
- isNull
- isNullAsync
配置裂脑保护
可以使用该方法以编程方式配置IAtomicReference的Split-Brain保护setQuorumName(),或以声明方式使用该元素quorum-ref。以下是声明性配置示例:
<atomic-reference name="default">
...
<quorum-ref>quorumname</quorum-ref>
...
</atomic-reference>
值quorum-ref应该是您在quorum元素下配置的仲裁配置名称,如“ 裂脑保护”部分中所述。
Hazelcast ICountDownLatch是分布式实现的java.util.concurrent.CountDownLatch。
CountDownLatch被认为是并发活动的守门人。它使线程能够等待其他线程完成其操作。
以下代码示例描述了其机制ICountDownLatch。假设存在领导者流程并且有跟随者流程将等待领导者完成。这是领导者:
public class Leader {
public static void main( String[] args ) throws Exception {
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
ICountDownLatch latch = hazelcastInstance.getCountDownLatch( "countDownLatch" );
System.out.println( "Starting" );
latch.trySetCount( 1 );
Thread.sleep( 30000 );
latch.countDown();
System.out.println( "Leader finished" );
latch.destroy();
}
}
由于只需要一个步骤作为样本完成,上面的代码用1初始化锁存器。然后,代码休眠一段时间来模拟一个进程并开始倒计时。最后,它清除了闩锁。让我们写一个关注者:
public class Follower {
public static void main( String[] args ) throws Exception {
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
ICountDownLatch latch = hazelcastInstance.getCountDownLatch( "countDownLatch" );
System.out.println( "Waiting" );
boolean success = latch.await( 10, TimeUnit.SECONDS );
System.out.println( "Complete: " + success );
}
}
上面的跟随者类首先检索ICountDownLatch然后调用该await方法以使线程能够监听锁存器。该方法await具有超时值作为参数。当countDown方法失败时,这很有用。要查看ICountDownLatch实际操作,请首先启动领导者,然后启动一个或多个粉丝。您将看到关注者将等到领导者完成。
在分布式环境中,倒计时集群成员可能会关闭。在这种情况下,Hazelcast会立即自动通知所有听众。应该验证故障之前的当前进程的状态,并且应该确定“现在如何继续”,例如,重新启动所有进程操作,继续第一个失败的进程操作,并抛出异常。
虽然这ICountDownLatch是一个非常有用的同步辅助工具,但您可能不会每天使用它。与Java的实现不同,Hazelcast的ICountDownLatch计数可以在倒计时结束后重置,但不能在活动计数期间重置。
ICountDownLatch有1个同步备份,没有异步备份。其备份计数不可配置。此外,在活动计数期间无法重新设置计数,应在倒计时结束后重新设置计数。 |
可以将ICountDownLatch配置为在应用ICountDownLatch操作之前检查可用成员的最小数量(请参阅裂脑保护)。这是一项检查,以避免在网络分区期间对群集的所有部分执行成功的队列操作。
以下是现在支持裂脑保护检查的方法列表。该列表按仲裁类型分组。
- WRITE,READ_WRITE:
- countDown
- trySetCount
- READ,READ_WRITE:
- await
- getCount
配置裂脑保护
可以使用该方法以编程方式配置ICountDownLatch的Split-Brain保护setQuorumName(),或以声明方式使用该元素quorum-ref。以下是声明性配置示例:
<count-down-latch name="default">
...
<quorum-ref>quorumname</quorum-ref>
...
</count-down-latch>
值quorum-ref应该是您在quorum元素下配置的仲裁配置名称,如“ 裂脑保护”部分中所述。
无冲突复制数据类型(CRDT)是一种分布式数据结构,通过放松一致性约束来实现高可用性。对于相同的数据可能存在多个副本,并且可以在没有协调的情况下同时修改这些副本。这意味着在更新CRDT数据结构时可以实现高吞吐量和低延迟。另一方面,所有更新都是异步复制的。然后,每个副本最终将接收在其他副本上进行的更新,如果没有进行新的更新,则可以在一段时间之后返回相同状态(收敛)的所有副本。
Hazelcast提供了一个轻量级CRDT PN计数器(正 - 负计数器)实现,其中每个Hazelcast实例可以递增和递减计数器值,并且这些更新将传播到所有副本。只有Hazelcast成员可以存储计数器的状态,这意味着在Hazelcast成员上执行的计数器方法调用通常是本地的(取决于配置的副本计数)。如果没有成员失败,则保证每个副本最终都能看到计数器的最终值。计数器的状态会随着每次更新而收敛,并且可以相互通信的所有CRDT副本最终将具有相同的状态。
使用PN计数器,您可以获得分布式计数器,递增和递减它,并使用RYW(读写)和单调读取来查询其值。该实现借用了大多数方法,AtomicLong这些方法在大多数情况下应该是熟悉的,并且在现有代码中可以轻松地互换。
PN计数器的一些例子是:
- 计算“喜欢”或“+1”的数量
- 计算登录用户的数量
- 计算页面点击次数/浏览量
怎么运行的
计数器支持添加和减去值以及检索当前计数器值。此计数器的每个副本都可以在本地执行操作,而无需与其他副本协调,从而提高可用性。计数器保证每当两个成员收到相同的更新集(可能是不同的顺序)时,它们的状态是相同的,并且任何冲突的更新都会自动合并。如果没有对共享状态进行新的更新,则可以通信的所有成员最终将具有相同的数据。
在CRDT副本上调用时,计数器的更新将在本地应用。CRDT副本可以是任何不是客户端或精简成员的 Hazelcast实例。您可以使用replica-count配置元素配置群集中的副本数。
从非副本实例调用更新时,调用是远程的。这可能导致不确定状态 - 可能会应用更新但尚未收到响应。在这种情况下,TargetDisconnectedException将从客户端MemberLeftException调用时或从成员调用时通知调用者。
读写方法提供单调读取和RYW(读写)保证。这些保证是会话保证,这意味着如果没有可以访问先前观察到的状态的副本,则会丢失会话保证并且方法调用将抛出ConsistencyLostException。这并不意味着更新丢失。所有更新都是某些副本的一部分,最终将反映在所有其他副本的状态中。此异常仅表示您无法观察自己的写入,因为包含您的更新的所有副本当前都无法访问。收到你的后ConsistencyLostException,您可以等待一个足够的最新副本可以访问,在这种情况下会话可以继续,或者您可以通过调用方法`reset()重置会话。如果已调用此方法,则会在下次调用CRDT副本时启动新会话。
CRDT状态完全保留在非精简(数据)成员上。如果没有,并且在lite成员上调用此处的方法,则它们会失败NoDataMemberInClusterException。 |
以下是示例代码。
final HazelcastInstance instance = Hazelcast.newHazelcastInstance();
final PNCounter counter = instance.getPNCounter("counter");
counter.addAndGet(5);
final long value = counter.get();
此代码段创建PN计数器的实例,将其递增5并检索该值。
以下是声明性配置代码段示例:
<hazelcast>
<pn-counter name="default">
<replica-count>10<replica-count>
<statistics-enabled>true</statistics-enabled>
</pn-counter>
</hazelcast>
- name:PN计数器的名称。
- replica-count:将保留此PN计数器的状态的副本数。此数字适用于静止状态,如果当前有成员资格更改或群集正在合并,则状态可能会暂时保留在更多副本上。其默认值为Integer.MAX_VALUE。通常,将状态保持在更多副本上意味着更多Hazelcast成员将能够在本地执行更新,但这也意味着PN计数器状态将保留在更多副本上,增加网络流量,降低复制状态收敛的速度和增加每个副本上保留的PN计数器状态的大小。
- statistics-enabled:指定是否为PN计数器启用统计信息收集。如果设置为false,则无法在实施中收集统计信息,并且Hazelcast管理中心也不会显示它们。它的默认值是true。
以下是Java配置的等效片段:
PNCounterConfig pnCounterConfig = new PNCounterConfig()
.setReplicaCount(10)
.setStatisticsEnabled(true);
Config hazelcastConfig = new Config()
.addPNCounterConfig(pnCounterConfig);
配置复制机制仅适用于高级用例 - 通常默认配置在大多数情况下都能正常工作。 |
在某些情况下,您可能希望为所有CRDT实现配置复制机制。CRDT状态以轮次形式复制(周期是可配置的),并且在每一轮中,状态被复制到配置的成员数量。一般来说,您可以以更多网络流量为代价来提高副本收敛的速度,或者以更快的副本收敛为代价来减少网络流量。Hazelcast实现了基于状态的复制机制 - 已更改的CRDT的CRDT状态在每个复制回合中完全复制到其他副本。
<hazelcast>
<crdt-replication>
<max-concurrent-replication-targets>1</max-concurrent-replication-targets>
<replication-period-millis>1000</replication-period-millis>
</crdt-replication>
</hazelcast>
- max-concurrent-replication-targets:我们在一个时间段内复制CRDT状态的最大目标成员数。较高的计数将导致状态以更快的速度传播而牺牲类似突发的行为 - 对CRDT的一次更新将导致在短时间间隔内复制消息的数量突然爆发。默认值为1,这意味着每个副本将在每个复制回合中将状态复制到另一个副本。
- replication-period-millis:CRDT的两次复制之间的时间段以毫秒为单位。较低的值将增加更改以其类似突发行为的速度传播给其他集群成员的速度 - 更少的更新将在一个复制消息中一起批处理,一个CRDT的更新可能会导致突发的复制消息突发很短的时间间隔。该值必须是正的非空整数。默认值为1000 ms,这意味着每1秒复制一次更改的CRDT状态。
以下是Java配置的等效片段:
final CRDTReplicationConfig crdtReplicationConfig = new CRDTReplicationConfig()
.setMaxConcurrentReplicationTargets(1)
.setReplicationPeriodMillis(1000);
Config hazelcastConfig = new Config()
.setCRDTReplicationConfig(crdtReplicationConfig);
Hazelcast IdGenerator用于生成群集范围的唯一标识符。生成的标识符是0到0之间的长类型原始值Long.MAX_VALUE。
功能已弃用。即使启用了裂脑保护(在检测到裂脑的短窗口期间),实施也可以在网络分裂的情况下产生重复的ID。请使用 FlakeIdGenerator进行不受此问题影响的替代实现。另请参阅本节末尾的“迁移指南 ”。 |
ID生成几乎以速度发生AtomicLong.incrementAndGet()。为每个集群成员分配一组10,000个标识符。在后台,此分配以IAtomicLong增加10,000进行。集群成员生成ID(分配完成)后,IdGenerator递增本地计数器。如果集群成员使用组中的所有ID,则将获得另外10,000个ID。这样,只需要一次网络流量,这意味着在内存中而不是在网络上生成9,999个标识符。这很快。
让我们编写一个示例标识符生成器。
public class IdGeneratorExample {
public static void main( String[] args ) throws Exception {
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
IdGenerator idGen = hazelcastInstance.getIdGenerator( "newId" );
while (true) {
Long id = idGen.newId();
System.err.println( "Id: " + id );
Thread.sleep( 1000 );
}
}
}
让我们运行上面的代码两次。输出将类似于以下内容。
Members [1] {
Member [127.0.0.1]:5701 this
}
Id: 1
Id: 2
Id: 3
Members [2] {
Member [127.0.0.1]:5701
Member [127.0.0.1]:5702 this
}
Id: 10001
Id: 10002
Id: 10003
您可以看到生成的ID是唯一的并且向上计数。如果您看到重复的标识符,则表示您的实例无法形成群集。
生成的ID在群集的生命周期中是唯一的。如果重新启动整个群集,ID将再次从0开始,或者您可以使用init()IdGenerator 的方法初始化为值。 | |
IdGenerator有一个同步备份,没有异步备份。其备份计数不可配置。 |
Flake ID生成器提供类似的功能,在网络分割期间提供更多安全保证。两个生成器是完全不同的实现,但两种类型的生成器生成大致有序的ID。因此,为了确保生成的ID的唯一性,我们可以强制Flake ID生成器至少在旧生成器结束的位置启动。这可能是这种情况,因为与旧发电机的值相比,Flake发电机的值非常大。无论如何,这是您需要采取的步骤:
- 确保Hazelcast群集和所有客户端的版本至少为3.10。
- 如果旧的当前ID IdGenerator高于ID FlakeIdGenerator,则需要配置ID偏移量。有关详细信息,请参阅FlakeIdMigrationSample。
- 更换所有通话HazelcastInstance.getIdGenerator()使用HazelcastInstance.getFlakeIdGenerator()。如果使用Spring配置,请替换<id-generator>为<flake-id-generator>
Hazelcast Flake ID Generator用于生成群集范围的唯一标识符。生成的标识符是long原始值,并且是k顺序的(大致有序)。ID的范围是0到Long.MAX_VALUE。
ID包含时间戳组件和节点ID组件,该组件在成员加入群集时分配。这允许ID被排序且唯一,而成员之间没有任何协调,这使得发生器即使在裂脑情况下也是安全的(对于这种情况的限制,请参见下面的节点ID分配部分)。
时间戳组件自1.1.2018,0:00 UTC以来以毫秒为单位,具有41位。这将发电机的使用寿命限制在不到70年(直到~2088)。序列组件是6位。如果在单毫秒内请求超过64个ID,则ID将优雅地溢出到下一毫秒,并且在这种情况下保证唯一性。该实现不允许溢出超过15秒,如果以更高的速率请求ID,则呼叫将被阻止。但请注意,客户端能够生成更快的速度,因为每个调用都会转到另一个(随机)成员,而64个ID / ms限制适用于单个成员。
如果成员具有有效的节点ID,则成员上的操作始终是本地的,否则它是远程的。在客户端上,newId()方法转到随机成员并获取一批ID,然后在有限时间内在本地返回。可以为每个客户端和成员配置预取大小和有效时间。
让我们编写一个示例标识符生成器。
public class FlakeIdGeneratorSample {
public static void main(String[] args) {
HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance();
ClientConfig clientConfig = new ClientConfig()
.addFlakeIdGeneratorConfig(new FlakeIdGeneratorConfig("idGenerator")
.setPrefetchCount(10));
HazelcastInstance client = HazelcastClient.newHazelcastClient(clientConfig);
FlakeIdGenerator idGenerator = client.getFlakeIdGenerator("idGenerator");
for (int i = 0; i < 10000; i++) {
sleepSeconds(1);
System.out.printf("Id: %s\n", idGenerator.newId());
}
}
}
Flake ID需要为每个成员分配唯一的节点ID,从该点成员可以生成唯一ID而无需任何协调。Hazelcast使用成员列表版本,从成员加入群集时开始,作为唯一的节点ID。
连接算法专门用于确保成员列表连接版本对于群集中的每个成员都是唯一的。这确保了即使在网络拆分期间ID也是唯一的,但有一点需要注意:在网络拆分期间,最多允许一个成员加入集群。如果两个成员加入不同的子集群,则它们可能会获得相同的节点ID。这将在群集恢复时解决,但在此之前,它们可以生成重复的ID。
ID的节点ID组件具有16位。成员列表加入版本高于2 ^ 16的成员将无法生成ID,但通过转发给其他成员将保留功能。只要集群中至少有一个成员的连接版本小于2 ^ 16,就可以在任何成员或客户端上生成ID。解决方法是重新启动集群:将重置节点ID组件并再次从零开始分配。由于时间戳组件,将保留重新启动后的唯一性。
复制映射是一种分布式键值数据结构,其中数据被复制到集群中的所有成员。它为所有成员提供了完整的条目复制,以实现高速访问。以下是它的特点:
- 如果群集中有复制映射,则客户端可以与任何集群成员进行通信。
- 所有集群成员都能够执行写入操作。
- 它支持接口的所有方法java.util.Map。
- 它支持在启动新成员时自动初始填充。
- 它提供了条目访问,写入和更新的统计信息,以便您可以使用Hazelcast管理中心对其进行监控。
- 加入群集的新成员会从现有成员中提取所有数据。
- 您可以使用侦听器侦听输入事件。请参阅在复制映射上使用EntryListener。
复制映射不对数据进行分区(它不会将数据传播到不同的集群成员); 相反,它将数据复制到所有成员。所有其他数据结构都在设计中进行分区。
复制会导致更高的内存消耗。但是,复制映射具有更快的读写访问权限,因为所有成员都可以使用数据。
写入可以在本地/远程成员上进行,以便提供写入顺序,最终被复制到所有其他成员。
复制映射适用于对象,目录数据或幂等可计算数据(例如HTML页面)。它完全实现了java.util.Map接口,但java.util.concurrent.ConcurrentMap由于没有写入或读取的原子保证,因此缺少方法。
如果从虚拟客户端使用复制映射并且此虚拟客户端连接到lite成员,则无法注册/取消注册条目侦听器。 | |||
您不能使用lite成员中的Replicated Map。com.hazelcast.replicatedmap.ReplicatedMapCantBeCreatedOnLiteMemberException如果com.hazelcast.core.HazelcastInstance.getReplicatedMap(name)在lite成员上调用, 则抛出A. | |||
以下是复制映射代码的示例。HazelcastInstance的getReplicatedMap方法获取复制映射,而复制映射的put方法创建映射条目。
public class FillMapMember {
public static void main(String[] args) {
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
Map<String, Customer> customers = hazelcastInstance.getReplicatedMap("customers");
customers.put( "1", new Customer( "Joe", "Smith" ) );
customers.put( "2", new Customer( "Ali", "Selam" ) );
customers.put( "3", new Customer( "Avi", "Noyan" ) );
Collection<Customer> colCustomers = customers.values();
for ( Customer customer : colCustomers ) {
// process customer
}
}
}
HazelcastInstance.getReplicatedMap()返回com.hazelcast.core.ReplicatedMap,如上所述,扩展java.util.Map接口。
该com.hazelcast.core.ReplicatedMap接口具有一些其他方法,用于注册条目侦听器或按预期顺序检索值。
如果您有大型集群或非常多的更新,则复制映射可能无法按预期线性扩展,因为它必须将更新操作复制到集群中的所有成员。
由于更新的复制是以异步方式执行的,因此我们建议您在系统发生较多更新时启用反压。请参阅背压部分以了解如何启用它。
复制映射具有反熵系统,如果某些成员缺少复制更新,则会将值汇聚到公共值。
复制映射不保证最终的一致性,因为有些边缘情况无法提供一致性。
复制映射使用Hazelcast的内部分区系统,以便同时序列化在同一个密钥上发生的更新。这通过将相同密钥的更新发送到群集中的同一Hazelcast成员来实现。
由于复制的异步性质,Hazelcast成员可能会在写入过程中向其调用者发送“写入完成”响应后成功将“写入”操作复制到其他成员之前死亡。在这种情况下,Hazelcast的内部分区系统将促使分区的一个副本作为主要副本。新的主分区将没有最新的“写入”,因为死亡成员无法成功复制更新。(这将使系统处于这样一种状态,即调用者是唯一具有更新而其余群集没有更新的状态。)在这种情况下,即使反熵系统也无法收敛该值,因为真的来源更新丢失了信息。
除了上述场景之外,复制映射的行为类似于最终一致的系统,具有读写和单调读取一致性。
配置复制映射时,应考虑几个技术设计决策。
初始供应
如果新成员加入群集,则有两种方法可以处理执行的初始配置,以将所有现有值复制到新成员。每个都涉及如何配置异步填充。
首先,您可以将async fill配置为true,在填充操作正在进行时不会阻止读取。这样,您可以立即访问新成员,但需要时间才能最终访问所有值。尚未复制的值返回为不存在(null)。
其次,您可以配置同步初始填充(通过将异步填充配置为false),这将阻止对映射的每次读取或写入访问,直到填充操作完成。请谨慎使用,因为它可能会阻止您的应用程序运行。
可以通过编程或声明方式配置复制映射。
声明性配置:
您可以在Hazelcast配置文件中声明复制映射配置hazelcast.xml。请参阅以下示例。
<replicatedmap name="default">
<in-memory-format>BINARY</in-memory-format>
<async-fillup>true</async-fillup>
<statistics-enabled>true</statistics-enabled>
<entry-listeners>
<entry-listener include-value="true">
com.hazelcast.examples.EntryListener
</entry-listener>
</entry-listeners>
<quorum-ref>quorumname</quorum-ref>
</replicatedmap>
- in-memory-format:内部存储格式。请参阅内存格式部分。默认值为OBJECT。
- async-fillup:指定在初始复制完成之前,复制映射是否可用于读取。默认值为true。如果设置为false(即同步初始填充),则在复制映射尚未就绪时不会抛出异常,但null可以看到值,直到初始复制完成。
- statistics-enabled:如果设置为true,则收集缓存命中和未命中等统计信息。默认值为true。
- entry-listener:实现的完整规范类名EntryListener。
- entry-listener#include-value:指定事件是否包含值。有时钥匙足以对事件作出反应。在这些情况下,将此值设置为false将保存反序列化循环。默认值为true。
- entry-listener#local:不用于复制映射,因为侦听器始终是本地的。
- quorum-ref:您希望此复制映射使用的仲裁配置的名称。请参阅“ 复制地图的裂脑保护”部分。
程序化配置:
您可以以编程方式配置复制映射,就像Hazelcast中的所有其他数据结构一样。在实例化时,必须预先创建配置HazelcastInstance。以下代码段中显示了如何使用编程方法配置复制映射的基本示例。
Config config = new Config();
ReplicatedMapConfig replicatedMapConfig =
config.getReplicatedMapConfig( "default" );
replicatedMapConfig.setInMemoryFormat( InMemoryFormat.BINARY )
.setQuorumName( "quorumname" );
通过将标记名称转换为getter或setter名称,可以使用编程配置获得可以使用声明性配置配置的所有属性。
目前,in-memory-format复制映射可以使用两个值。
- OBJECT(默认值):数据将以反序列化的形式存储。此配置是默认选择,因为数据复制主要用于高速访问。请注意,更改没有a的值Map.put()不会反映在其他成员上,但在更改成员上可见,以便以后进行值访问。
- BINARY:数据以序列化二进制格式存储,并且必须在每个请求中反序列化。此选项提供更高的封装,因为只要新更改的对象未Map.put()再次显式进入映射,就会始终丢弃对值的更改。
一个com.hazelcast.core.EntryListener在复制地图服务使用的目的,因为它会在Hazelcast其他数据结构相同。您可以使用它来对添加,更新和删除操作做出反应。复制的地图尚不支持驱逐。
与其他数据结构相比,复制映射行为的根本区别在于EntryListener仅反映本地数据的更改。由于复制是异步的,因此只有在本地成员上完成操作时才会触发所有侦听器事件。事件可以在不同成员的不同时间触发。
以下是在复制映射上使用EntryListener的代码示例。
该HazelcastInstance’s `getReplicatedMap方法获取复制映射(客户),该ReplicatedMap’s `addEntryListener方法将一个条目侦听器添加到复制映射。然后,该ReplicatedMap’s `put方法添加一个Replicated Map条目并对其进行更新。该方法remove删除条目。
public class ListeningMember {
public static void main(String[] args) {
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
ReplicatedMap<String, Customer> customers = hazelcastInstance.getReplicatedMap( "customers" );
customers.addEntryListener( new EntryListener<String, Customer>() {
@Override
public void entryAdded( EntryEvent<String, Customer> event ) {
log( "Entry added: " + event );
}
@Override
public void entryUpdated( EntryEvent<String, Customer> event ) {
log( "Entry updated: " + event );
}
@Override
public void entryRemoved( EntryEvent<String, Customer> event ) {
log( "Entry removed: " + event );
}
@Override
public void entryEvicted( EntryEvent<String, Customer> event ) {
// Currently not supported, will never fire
}
});
customers.put( "1", new Customer( "Joe", "Smith" ) ); // add event
customers.put( "1", new Customer( "Ali", "Selam" ) ); // update event
customers.remove( "1" ); // remove event
}
}
可以将复制映射配置为在应用其操作之前检查可用成员的最小数量(请参阅裂脑保护)。这是一项检查,以避免在网络分区期间对群集的所有部分执行成功的队列操作。
以下是现在支持裂脑保护检查的方法列表。该列表按仲裁类型分组。
- WRITE,READ_WRITE:
- clear
- put
- putAll
- remove
- READ,READ_WRITE:
- containsKey
- containsValue
- entrySet
- get
- isEmpty
- keySet
- size
- values
配置裂脑保护
可以使用该方法以编程方式配置复制映射的裂脑保护setQuorumName(),或以声明方式使用该元素quorum-ref。以下是声明性配置示例:
<replicatedmap name="default">
...
<quorum-ref>quorumname</quorum-ref>
...
</replicatedmap>
值quorum-ref应该是您在quorum元素下配置的仲裁配置名称,如“ 裂脑保护”部分中所述。
Hazelcast的基数估计服务是一种数据结构,它实现了Flajolet的HyperLogLog算法,用于估算理论上庞大的数据集中唯一对象的基数。Hazelcast提供的实现包括Google版本的算法的改进,即HyperLogLog ++。
基数估算器服务不提供任何配置其属性的方法,而是使用一些经过良好测试的默认值。
- P:Precision - 14,使用索引的16 LSB哈希值。
- M:2 ^ P = 16384(16K)寄存器
- P':稀疏精度 - 25
- Durability:每个估算器的备份数量,默认值为2
重要的是要理解这个数据结构不是100%准确,它用于提供估计。错误率通常是1.04/sqrt(M)我们的实施中对于高百分位数约为0.81%的结果。 |
尽管源数据集或流中的元素大小,但此数据结构的内存消耗接近16K。
使用基数估算器有两个阶段。
- 将对象添加到估计器的实例,例如,用于IP estimator.add("0.0.0.0.")。首先序列化提供的对象,然后使用字节数组为该对象生成哈希。
对象必须以Hazelcast能够理解的形式进行序列化。 |
- 到目前为止计算集合的估计值estimator.estimate()。
有关其API的更多信息,请参阅基数估算器Javadoc。
以下是示例代码。
HazelcastInstance hz = Hazelcast.newHazelcastInstance();
CardinalityEstimator visitorsEstimator = hz.getCardinalityEstimator("visitors");
InputStreamReader isr = new InputStreamReader(Member.class.getResourceAsStream("visitors.txt"));
BufferedReader br = new BufferedReader(isr);
try {
String visitor = br.readLine();
while (visitor != null) {
visitorsEstimator.add(visitor);
visitor = br.readLine();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
closeResource(br);
closeResource(isr);
}
System.out.printf("Estimated unique visitors seen so far: %d%n", visitorsEstimator.estimate());
Hazelcast.shutdownAll();
可以将基数估算器配置为在应用其操作之前检查可用成员的最小数量(请参阅裂脑保护)。这是一项检查,以避免在网络分区期间对群集的所有部分执行成功的队列操作。
以下是现在支持裂脑保护检查的方法列表。该列表按仲裁类型分组。
- WRITE,READ_WRITE:
- add
- addAsync
- READ,READ_WRITE:
- estimate
- estimateAsync
配置裂脑保护
基数估计器的分裂脑保护可以使用该方法以编程方式配置setQuorumName(),或以声明方式使用该元素quorum-ref。以下是声明性配置示例:
<cardinality-estimator name="default">
...
<quorum-ref>quorumname</quorum-ref>
...
</cardinality-estimator>
值quorum-ref应该是您在quorum元素下配置的仲裁配置名称,如“ 裂脑保护”部分中所述。
配置合并策略
在从Split-Brain综合症中恢复时,小型群集中的基数估算器会根据配置的合并策略合并到更大的群集中。当估计器合并到群集中时,群集中可能已存在具有相同名称的估计器。因此,合并政策通过不同的开箱即用策略解决了这些冲突。它可以使用该方法以编程方式配置,也可以setMergePolicyConfig()使用该元素以声明方式配置merge-policy。以下是声明性配置示例:
<cardinality-estimator name="default">
...
<merge-policy>HyperLogLogMergePolicy</merge-policy>
...
</cardinality-estimator>
可以使用开箱即用的合并策略:
- DiscardMergePolicy:来自较小群集的估算器将被丢弃。
- HyperLogLogMergePolicy:Estimator将使用HyperLogLog的算法合并与现有的合并。这是默认策略。
- PassThroughMergePolicy:来自较小群集的估算器获胜。
- PutIfAbsentMergePolicy:如果群集中不存在,则较小群集中的估算器将获胜。
事件日志是一种分布式数据结构,用于在地图或缓存上存储变异操作的历史记录。修改其内容的地图或缓存上的每个操作(例如put,remove或未使用公共API触发的计划任务)将创建将存储在事件日志中的事件。该事件将存储事件类型以及条目的键,旧值和更新值(如果适用)。作为用户,您只能通过使用映射和缓存方法或配置到期和逐出来间接附加到日志。通过从事件日志中读取,您可以在任何时间点重新创建映射或缓存的状态。
目前,事件日志没有公开用于阅读Hazelcast IMDG中的事件日志的公共API。事件日志可用于将事件数据流式传输到Hazelcast Jet,因此它应与Hazelcast Jet结合使用。因此,我们将介绍如何配置它,而不是如何从IMDG中使用它。如果您启用并配置事件日志,您可能只能通过私有API访问它,并且您很可能不会获得任何好处,但日志将保留事件并消耗堆空间。 |
事件日志具有固定容量和到期时间。在内部,它被构造为一个环形缓冲区(由ringbuffer项目分区)并与它共享许多相似之处。
使用逐出和过期配置IMap可能会导致事件日志在同一分区的不同副本上包含不同的事件。如果从事件日志中读取并且分区所有者已终止,则可能会遇到问题。然后,备份副本将提升到分区所有者,但事件日志将包含不同的事件。事件计数应该保持不变,但您之前认为被驱逐和过期的条目现在可以“活着”,反之亦然。
这是因为逐出和过期随机选择要逐出/过期的条目。分区副本之间不协调该条目。在这些情况下,事件日志分歧并且不会在任何未来点收敛,但仍然不一致,内部记录存储的内容在副本之间不一致。您可能会说特定副本上的事件日志与该副本上的记录存储同步,但副本之间的事件日志和记录存储不同步。
默认情况下,事件日志配置有capacity10000个项目。这会为每个分区创建一个数组,大致是容量的大小除以分区数。因此,如果配置的容量为10000且分区数为271,我们将创建271个大小为36(10000/271)的阵列。如果time-to-live配置,则还会创建一个long数组,用于存储每个项的到期时间。事件日志的单个数组会保留仅与该分区中的映射条目相关的事件。在很多情况下,您可能想要改变这一点capacity编号可以更好地满足您的需求。由于容量在分区之间共享,请记住不要将其设置为对您来说太低的值。将容量设置为低于分区计数的数字将在初始化事件日志时导致错误。
下面是一个事件日志的声明性配置示例,其中包含capacity5000个用于映射的项目,以及10000个用于缓存的项目:
<event-journal enabled="true">
<mapName>myMap</mapName>
<capacity>5000</capacity>
<time-to-live-seconds>20</time-to-live-seconds>
</event-journal>
<event-journal enabled="true">
<cacheName>myCache</cacheName>
<capacity>10000</capacity>
<time-to-live-seconds>0</time-to-live-seconds>
</event-journal>
您还可以以编程方式配置事件日志。以下是上述声明性配置的编程版本:
EventJournalConfig myMapJournalConfig = new EventJournalConfig()
.setMapName("myMap")
.setEnabled(true)
.setCapacity(5000)
.setTimeToLiveSeconds(20);
EventJournalConfig myCacheJournalConfig = new EventJournalConfig()
.setMapName("myCache")
.setEnabled(true)
.setCapacity(10000)
.setTimeToLiveSeconds(0);
Config config = new Config();
config.addEventJournalConfig(myMapJournalConfig);
config.addEventJournalConfig(myCacheJournalConfig);
在mapName和cacheName属性定义此事件日志配置适用于地图或缓存。default这样做时,您可以使用模式匹配和关键字。例如,通过使用mapName中journaled*,该杂志的配置将适用于他们的名字开始与“轴颈”并没有匹配(例如,其他杂志配置的所有地图,如果你有一个确切的名称匹配更具体的配置日记)。如果指定mapName或cacheNameas default,则日记配置将应用于所有没有任何其他日志配置的映射和高速缓存。这意味着可能所有地图和/或缓存都将具有单个事件日志配置。
事件日志是分区数据结构。分区由事件键完成。因此,具有特定键的映射和高速缓存条目与该键的事件位于同一位置,并将相应地进行迁移。此外,事件日志的备份计数等于其包含事件的映射或缓存的备份计数。备份副本上的事件将使用映射或缓存备份操作创建,并且在将事件附加到事件日志时不会引入其他网络流量。
您可以time-to-live在几秒钟内配置Hazelcast事件日志。使用此设置,您可以控制项目在过期之前保留在事件日志中的时间。默认情况下,time-to-live设置为0,这意味着除非项目被覆盖,否则它将无限期地保留在日志中。每当新事件附加到事件日志或正在读取事件日志时,都会检查现有日记事件的到期时间。如果没有读取或写入期刊,期刊可能会无限期地保留过期的项目。
在下面的示例中,事件日志配置time-to-live为180秒:
<event-journal enabled="true">
<cacheName>myCache</cacheName>
<capacity>10000</capacity>
<time-to-live-seconds>180</time-to-live-seconds>
</event-journal>