本文旨在解决Hazelcast Replicatedmap在使用InMemoryFormat.BINARY时遇到的Java.lang.ClassCastException,具体表现为String无法转换为com.hazelcast.internal.serialization.impl.HeapData。该异常源于ReplicatedMap声明的泛型类型与Hazelcast内部二进制数据存储格式不匹配。解决方案是调整ReplicatedMap的泛型参数,使其与Hazelcast的Data序列化单元保持一致,从而确保在内部操作(如指标收集)期间的数据处理一致性。
理解Hazelcast中的ClassCastException
在使用hazelcast的replicatedmap时,如果配置了inmemoryformat.binary,可能会遇到一个频繁出现的java.lang.classcastexception。典型的错误信息如下:
java.lang.ClassCastException: class java.lang.String cannot be cast to class com.hazelcast.internal.serialization.impl.HeapData
这个异常通常发生在Hazelcast内部的指标收集周期中,具体堆栈信息指向com.hazelcast.replicatedmap.impl.LocalReplicatedMapStats类的getLocalReplicatedMapStats方法,尤其是在尝试计算内存使用量时:
if (isBinary) { memoryUsage += ((HeapData) record.getValueInternal()).getHeapcost(); // <-- 异常发生在此处 }
这表明,当Hazelcast尝试获取ReplicatedMap中条目的内部值并将其视为HeapData(com.hazelcast.internal.serialization.impl.HeapData是com.hazelcast.internal.serialization.Data的一个实现)时,它意外地得到了一个String对象,导致类型转换失败。
Hazelcast的InMemoryFormat.BINARY与Data对象
Hazelcast提供了多种内存存储格式,其中InMemoryFormat.BINARY和InMemoryFormat.Object是两种主要的选择:
- InMemoryFormat.OBJECT: 数据以其原始的Java对象形式存储在内存中。每次读写操作通常涉及对象的序列化和反序列化,但对于用户代码而言,直接操作的是Java对象。
- InMemoryFormat.BINARY: 数据以其序列化后的二进制形式存储在内存中。这种格式通常更节省内存,并且在某些场景下能提高性能,因为它避免了不必要的反序列化。当使用此格式时,Hazelcast内部将对象转换为com.hazelcast.internal.serialization.Data的实例。
com.hazelcast.internal.serialization.Data是Hazelcast内部序列化机制的基本单元,它封装了对象的二进制表示。根据其Javadoc,Data存储了通过SerializationService.toData(Object)方法序列化后的对象的二进制形式。
根源分析:类型不匹配
尽管在配置中明确设置了replicatedMapConfig.setInMemoryFormat(InMemoryFormat.BINARY),但ReplicatedMap的实例却被声明为ReplicatedMap
ReplicatedMap<String, String> map = hz.getReplicatedMap("rogueUsers");
Hazelcast通常能够透明地处理用户代码与内部二进制格式之间的转换。例如,当调用map.put(“key”, “value”)时,Hazelcast会将”key”和”value”序列化为Data对象并存储;当调用map.get(“key”)时,它会从Data对象反序列化回String。
然而,在某些内部操作(如收集ReplicatedMap的统计信息)中,Hazelcast可能会直接访问存储在内存中的Data对象。当InMemoryFormat.BINARY被激活时,内部逻辑期望获取到的是Data类型的实例(或其子类如HeapData)。但由于ReplicatedMap被声明为ReplicatedMap
简而言之,虽然用户代码通常与string类型交互,但当InMemoryFormat.BINARY启用时,Hazelcast内部存储和部分内部操作是基于Data对象的。声明ReplicatedMap
解决方案
解决此ClassCastException的关键在于确保ReplicatedMap的泛型类型与InMemoryFormat.BINARY的内部存储格式保持一致。这意味着,当选择二进制格式存储时,ReplicatedMap的键和值类型应声明为com.hazelcast.internal.serialization.Data。
将ReplicatedMap的声明从ReplicatedMap
ReplicatedMap<Data, Data> map = hz.getReplicatedMap("rogueUsers");
修正后的配置示例
以下是修正后的Hazelcast配置方法:
import com.hazelcast.config.Config; import com.hazelcast.config.InMemoryFormat; import com.hazelcast.config.JoinConfig; import com.hazelcast.config.NetworkConfig; import com.hazelcast.config.ReplicatedMapConfig; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.internal.serialization.Data; // 引入Data类 import com.hazelcast.map.listener.EntryAddedListener; import com.hazelcast.map.listener.EntryRemovedListener; import com.hazelcast.map.listener.EntryUpdatedListener; import com.hazelcast.replicatedmap.ReplicatedMap; import com.hazelcast.core.EntryEvent; import java.io.Serializable; public class HazelcastConfigsetup { private static HazelcastInstance setupHazelcastConfig() { Config config = new Config(); config.setInstanceName("rogueUsers"); NetworkConfig network = config.getNetworkConfig(); network.setPort(5701).setPortCount(20); network.setPortAutoIncrement(true); JoinConfig join = network.getJoin(); join.getMulticastConfig().setEnabled(true); // join.getTcpIpConfig().setEnabled(true); // 如果使用TCP/IP,取消注释 HazelcastInstance hz = Hazelcast.getOrCreateHazelcastInstance(config); ReplicatedMapConfig replicatedMapConfig = config.getReplicatedMapConfig("rogueUsers"); replicatedMapConfig.setInMemoryFormat(InMemoryFormat.BINARY); replicatedMapConfig.setAsyncFillup(true); replicatedMapConfig.setStatisticsEnabled(true); replicatedMapConfig.setSplitBrainProtectionName("splitbrainprotection-name"); // 核心修改:将ReplicatedMap的泛型类型改为Data, Data ReplicatedMap<Data, Data> map = hz.getReplicatedMap("rogueUsers"); map.addEntryListener(new RogueEntryListener()); return hz; } // 示例EntryListener,需要根据实际业务逻辑调整 // 注意:如果ReplicatedMap的泛型改为Data,那么EntryListener中的EntryEvent也会是Data类型 // 在实际使用中,你需要手动对Data进行反序列化 static class RogueEntryListener implements EntryAddedListener, EntryUpdatedListener, EntryRemovedListener, Serializable { @Override public void entryAdded(EntryEvent event) { System.out.println("Entry Added: " + event.getKey() + " -> " + event.getValue()); // 示例:如何从Data对象获取原始值 // String keyStr = (String) event.getOldValue().getSerializationService().toObject(event.getKey()); // String valueStr = (String) event.getOldValue().getSerializationService().toObject(event.getValue()); } @Override public void entryRemoved(EntryEvent event) { System.out.println("Entry Removed: " + event.getKey() + " -> " + event.getOldValue()); } @Override public void entryUpdated(EntryEvent event) { System.out.println("Entry Updated: " + event.getKey() + " -> " + event.getOldValue() + " to " + event.getValue()); } } public static void main(String[] args) { HazelcastInstance hz = setupHazelcastConfig(); ReplicatedMap<Data, Data> map = hz.getReplicatedMap("rogueUsers"); // 示例:如何存入和取出数据 String originalKey = "user:123"; String originalValue = "status:active"; // 将String序列化为Data对象存入Map Data keyData = hz.getSerializationService().toData(originalKey); Data valueData = hz.getSerializationService().toData(originalValue); map.put(keyData, valueData); System.out.println("Put: " + originalKey + " -> " + originalValue); // 从Map中取出Data对象并反序列化为String Data retrievedValueData = map.get(keyData); if (retrievedValueData != null) { String retrievedValue = hz.getSerializationService().toObject(retrievedValueData); System.out.println("Get: " + originalKey + " -> " + retrievedValue); } hz.shutdown(); } }
重要注意事项和最佳实践
- 手动序列化/反序列化: 当ReplicatedMap的泛型类型被声明为Data, Data时,用户代码在向Map中存入或从Map中取出数据时,需要显式地进行序列化和反序列化操作。这可以通过HazelcastInstance.getSerializationService().toData(Object)和HazelcastInstance.getSerializationService().toObject(Data)方法完成。
- 性能与可用性权衡: InMemoryFormat.BINARY通常能提供更好的内存效率和吞吐量,因为它避免了不必要的对象反序列化开销。然而,它也增加了用户代码的复杂性,需要手动处理Data对象。InMemoryFormat.OBJECT则提供了更简单的API,直接操作Java对象,但可能消耗更多内存。选择哪种格式应根据具体的应用场景和性能需求来决定。
- 类加载器问题: 原始堆栈信息中提到了String is in module java.base of loader ‘bootstrap‘; com.hazelcast.internal.serialization.impl.HeapData is in unnamed module of loader org.apache.catalina.loader.ParallelWebappClassLoader。虽然ClassCastException有时确实与类加载器隔离性问题有关(即同一个类名被不同的类加载器加载),但在本例中,根本原因是String和HeapData是两种完全不同的类型,且内部逻辑期望HeapData但收到了String。解决方案直接针对类型不匹配,而非类加载器隔离性。然而,在复杂的Web应用(如tomcat部署)环境中,始终要注意类加载器的配置,以避免潜在的依赖冲突。
- 持续监控: 部署更改后,应密切监控Hazelcast的日志,特别是WARNING和SEVERE级别的消息,以确保没有新的异常或性能问题出现。
总结
当Hazelcast ReplicatedMap配置为InMemoryFormat.BINARY时,为了避免ClassCastException,关键在于确保ReplicatedMap的泛型类型与Hazelcast内部的Data存储格式保持一致。将ReplicatedMap