Hazelcast - IMap
java.util.concurrent.Map提供了一个接口,支持在单个JVM中存储键值对。 而 java.util.concurrent.ConcurrentMap 对此进行了扩展,以支持具有多个线程的单个 JVM 中的线程安全。
类似地,IMap 扩展了 ConcurrentHashMap 并提供了一个接口,使映射线程能够跨 JVM 安全。 它提供类似的功能:put、get 等。
IMap支持同步备份和异步备份。 同步备份可确保即使保存队列的 JVM 出现故障,所有元素都将被保留并可从备份中获取。
让我们看一个有用函数的示例。
创建和读取/写入
添加元素和读取元素。 让我们在两个 JVM 上执行以下代码。 一个是生产者代码,另一个是消费者代码。
示例
第一部分是生产者代码,它创建映射并向其中添加项目。
public static void main(String... args) throws IOException, InterruptedException { //initialize hazelcast instance HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance(); // create a map IMap<String, String> hzStock = hazelcast.getMap("stock"); hzStock.put("Mango", "4"); hzStock.put("Apple", "1"); hzStock.put("Banana", "7"); hzStock.put("Watermelon", "10"); Thread.sleep(5000); System.exit(0); }
第二部分是读取元素的消费者代码。
public static void main(String... args) throws IOException, InterruptedException { //initialize hazelcast instance HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance(); // create a map IMap<String, String> hzStock = hazelcast.getMap("stock"); for(Map.Entry<String, String> entry: hzStock.entrySet()){ System.out.println(entry.getKey() + ":" + entry.getValue()); } Thread.sleep(5000); System.exit(0); }
输出
消费者代码的输出 −
Mango:4 Apple:1 Banana:7 Watermelon:10
有用的方法
Sr.No | 函数名称 & 描述 |
---|---|
1 | put(K key, V value) 向映射添加元素 |
2 | remove(K key) 从映射中删除元素 |
3 | keySet() 返回映射中所有键的副本 |
4 | localKeySet() 返回本地分区中存在的所有密钥的副本 |
5 | values() 返回映射中所有值的副本 |
6 | size() 返回映射中元素的数量 |
7 | containsKey(K key) 如果密钥存在则返回 true |
8 | executeOnEnteries(EntryProcessor processor) 将处理器应用于映射的所有键并返回该应用程序的输出。 我们将在接下来的部分中查看相同的示例。 |
9 | addEntryListener(EntryListener listener, value) 通知订阅者映射中的元素被删除/添加/修改。 |
10 | addLocalEntryListener(EntryListener listener, value) 通知订阅者本地分区中的元素被删除/添加/修改 |
驱逐
默认情况下,Hazelcast 中的键无限期地保留在 IMap 中。 如果我们有一个非常大的键集,那么我们需要确保与不经常使用的键相比,频繁使用的键存储在 IMap 中,以获得更好的性能和高效的内存使用。
为此,可以通过remove()/evict()函数手动删除不经常使用的键。 但是,Hazelcast 还提供基于各种驱逐算法的自动驱逐键。
此策略可以通过 XML 或以编程方式设置。 让我们看一个相同的例子−
<map name="stock"> <max-size policy="FREE_HEAP_PERCENTAGE">30</max-size> <eviction-policy>LFU</eviction-policy> </map>
上面的配置中有两个属性。
Max-size − 用于向 Hazelcast 传达我们声明的映射"库存"最大尺寸已达到的限制的策略。
Eviction-policy − 一旦达到上述最大大小策略,使用什么算法来删除/逐出密钥。
以下是一些有用的 max_size 策略。
Sr.No | Max_Size 政策和说明 |
---|---|
1 | PER_NODE 映射的每个 JVM 的最大条目数,这是默认策略。 |
2 | FREE_HEAP JVM 中保留的最小可用堆内存(以 MB 为单位) |
3 | FREE_HEAP_PERCENTAGE JVM 中保留的最小可用堆内存(以百分比表示) |
4 | take() 返回队列头或等待元素可用 |
5 | USED_HEAP JVM 中允许使用的最大堆内存(以 MB 为单位) |
6 | USED_HEAP_PERCENTAGE JVM 中允许使用的最大堆内存(百分比) |
这里有一些有用的驱逐政策 −
Sr.No | 驱逐政策和描述 |
---|---|
1 | NONE 默认政策不会驱逐 |
2 | LFU 最不常用的将被驱逐 |
3 | LRU 最近最少使用的密钥将被驱逐 |
驱逐的另一个有用参数也是生存时间秒,即 TTL。 有了这个,我们可以要求 Hazelcast 删除任何早于 X 秒的密钥。 这确保了我们在达到最大大小策略之前主动删除旧密钥。
分区数据和高可用性
关于 IMap 需要注意的一个要点是,与其他集合不同,数据是跨 JVM 进行分区的。 所有数据不需要存储/存在于单个 JVM 上。 所有 JVM 仍然可以访问完整的数据。 这为 Hazelcast 提供了一种跨可用 JVM 线性扩展的方法,并且不受单个 JVM 内存的限制。
IMap 实例被分为多个分区。 默认情况下,映射分为 271 个分区。 这些分区分布在可用的 Hazelcast 成员中。 添加到映射中的每个条目都存储在单个分区中。
让我们在 2 个 JVM 上执行此代码。
public static void main(String... args) throws IOException, InterruptedException { //initialize hazelcast instance HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance(); // create a map IMap<String, String> hzStock = hazelcast.getMap("stock"); hzStock.put("Mango", "4"); hzStock.put("Apple", "1"); hzStock.put("Banana", "7"); hzStock.put("Watermelon", "10"); Thread.sleep(5000); // print the keys which are local to these instance hzStock.localKeySet().forEach(System.out::println); System.exit(0); }
输出
如以下输出所示,消费者 1 打印自己的分区,其中包含 2 个键 −
Mango Watermelon
消费者 2 拥有包含其他 2 个密钥的分区 −
Banana Apple
默认情况下,IMap有一个同步备份,这意味着即使一个节点/成员发生故障,数据也不会丢失。 有两种类型的备份。
同步 − 在键也备份到另一个节点/成员上之前,map.put(key, value) 不会成功。 同步备份会阻塞,从而影响 put 调用的性能。
异步 − 最终执行存储密钥的备份。 异步备份是非阻塞且快速的,但如果成员发生故障,它们不能保证数据的存在。
可以使用 XML 配置来配置该值。 例如,我们对缺货映射进行操作 −
<map name="stock"> <backup-count>1</backup-count> <async-backup-count>1<async-backup-count> </map>
哈希码和等于
示例
在基于 Java 的 HashMap 中,键比较是通过检查 hashCode() 和 equals() 方法的相等性来进行的。 例如,为了简单起见,车辆可能有序列号和型号。
public class Vehicle implements Serializable{ private static final long serialVersionUID = 1L; private int serialId; private String model; public Vehicle(int serialId, String model) { super(); this.serialId = serialId; this.model = model; } public int getId() { return serialId; } public String getModel() { return model; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + serialId; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Vehicle other = (Vehicle) obj; if (serialId != other.serialId) return false; return true; } }
当我们尝试使用上面的类作为 HashMap 和 IMap 的键时,我们会看到比较的差异。
public static void main(String... args) throws IOException, InterruptedException { // create a Java based hash map Map<Vehicle, String> vehicleOwner = new HashMap<>(); Vehicle v1 = new Vehicle(123, "Honda"); vehicleOwner.put(v1, "John"); Vehicle v2 = new Vehicle(123, null); System.out.println(vehicleOwner.containsKey(v2)); // create a hazelcast map HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance(); IMap<Vehicle, String> hzVehicleOwner = hazelcast.getMap("owner"); hzVehicleOwner.put(v1, "John"); System.out.println(hzVehicleOwner.containsKey(v2)); System.exit(0); }
现在,为什么 Hazelcast 给出的答案是 false?
Hazelcast 序列化密钥并将其存储为二进制格式的字节数组。 由于这些键是序列化的,因此无法基于 equals() 和 hashcode() 进行比较。
Hazelcast 需要进行序列化和反序列化,因为 get()、containsKey() 等函数可能会在不拥有 key 的节点上调用,因此需要远程调用。
序列化和反序列化是昂贵的操作,因此 Hazelcast 不使用 equals() 方法,而是比较字节数组。
这意味着 Vehicle 类的所有属性都应该匹配,而不仅仅是 id。 那么,让我们执行以下代码 −
示例
public static void main(String... args) throws IOException, InterruptedException { Vehicle v1 = new Vehicle(123, "Honda"); // create a hazelcast map HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance(); IMap<Vehicle, String> hzVehicleOwner = hazelcast.getMap("owner"); Vehicle v3 = new Vehicle(123, "Honda"); System.out.println(hzVehicleOwner.containsKey(v3)); System.exit(0); }
输出
上面代码的输出是 −
true
此输出意味着 Vehicle 的所有属性都应匹配相等。
EntryProcessor
EntryProcessor 是一个支持将代码发送到数据而不是将数据带入代码的构造。 它支持在拥有 IMap 键的节点上序列化、传输和执行函数,而不是将数据引入到发起函数执行的节点。
示例
让我们通过一个例子来理解这一点。 假设我们创建了一个车辆 IMap -> 所有者。 现在,我们想为所有者存储小写字母。 那么,我们该怎么做呢?
public static void main(String... args) throws IOException, InterruptedException { // create a hazelcast map HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance(); IMap<Vehicle, String> hzVehicleOwner = hazelcast.getMap("owner"); hzVehicleOwner.put(new Vehicle(123, "Honda"), "John"); hzVehicleOwner.put(new Vehicle(23, "Hyundai"), "Betty"); hzVehicleOwner.put(new Vehicle(103, "Mercedes"), "Jane"); for(Map.Entry<Vehicle, String> entry: hzVehicleOwner.entrySet()) hzVehicleOwner.put(entry.getKey(), entry.getValue().toLowerCase()); for(Map.Entry<Vehicle, String> entry: hzVehicleOwner.entrySet()) System.out.println(entry.getValue()); System.exit(0); }
输出
上面代码的输出是 −
john jane betty
虽然这段代码看起来很简单,但如果有大量的键,它在规模方面有一个主要缺点 −
处理将在单个/调用者节点上进行,而不是跨节点分布。
需要更多的时间和内存来获取调用方节点上的关键信息。
这就是 EntryProcessor 提供帮助的地方。 我们将转换为小写的函数发送给保存密钥的每个节点。 这使得处理并行并控制内存需求。
示例
public static void main(String... args) throws IOException, InterruptedException { // create a hazelcast map HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance(); IMap<Vehicle, String> hzVehicleOwner = hazelcast.getMap("owner"); hzVehicleOwner.put(new Vehicle(123, "Honda"), "John"); hzVehicleOwner.put(new Vehicle(23, "Hyundai"), "Betty"); hzVehicleOwner.put(new Vehicle(103, "Mercedes"), "Jane"); hzVehicleOwner.executeOnEntries(new OwnerToLowerCaseEntryProcessor()); for(Map.Entry<Vehicle, String> entry: hzVehicleOwner.entrySet()) System.out.println(entry.getValue()); System.exit(0); } static class OwnerToLowerCaseEntryProcessor extends AbstractEntryProcessor<Vehicle, String> { @Override public Object process(Map.Entry<Vehicle, String> entry) { String ownerName = entry.getValue(); entry.setValue(ownerName.toLowerCase()); return null; } }
输出
上述代码的输出是 −
john jane betty
hazelcast_data_structures.html