Comparing Apache Ignite In-Memory Cache Performance With Hazelcast In-Memory Cache and Java Native Hashmap
This article compares different options for the in-memory maps and their performances in order for an application to move away from traditional RDBMS.
Join the DZone community and get the full member experience.
Join For FreeOverview
This article compares different options for the in-memory maps and their performances in order for an application to move away from traditional RDBMS tables for frequently accessed data. In this case, for the sake of demonstration, I have taken 2 million dummy physician records that reside in the database table and migrated them to in-memory maps. The migration will enable the application to quickly lookup in the map and vet the physician rather than querying the database table for vetting.
You may also like: Hazelcast With Spring Boot on Kubernetes
Source code for the Java clients to create these read-only distributed maps has been added to this article as well. All the Java clients used would create an in-memory map to save the physician objects with the physician's NPI as the key in the map. I have created an XML to export all the 2 million physicians for this purpose and below is the sample of that XML.
xxxxxxxxxx
<physicianProfiles xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<physicianProfile>
<nameList>
<name>
<firstName>FirstName1</firstName>
<lastName>LastName1</lastName>
<alternateFirstName>AltFirstName1</alternateFirstName>
<alternateLastName>AltLastName1</alternateLastName>
</name>
<name>
<firstName>AltFirstName2</firstName>
<lastName>AltFirstName2</lastName>
</name>
<name>
<firstName>AltFirstName3</firstName>
<lastName>AltFirstName3</lastName>
</name>
</nameList>
<licenseList>
<license>
<licenseState>State</licenseState>
<licenseNumber>LCNS1234</licenseNumber>
<licenseExpirationCYnInd>
<licenseExpirationCYList>
<licenseExpiration ></licenseExpiration>
</licenseExpirationCYList>
</licenseExpirationCYnInd>
</license>
<license>
<licenseState>State</licenseState>
<licenseNumber>LCNS5678</licenseNumber>
<licenseExpirationCYnInd>
<licenseExpirationCYList>
<licenseExpiration ></licenseExpiration>
</licenseExpirationCYList>
</licenseExpirationCYnInd>
</license>
<license>
<licenseState>State</licenseState>
<licenseNumber>LCNS23123</licenseNumber>
<licenseExpirationCYnInd>
<licenseExpirationCYList>
<licenseExpiration ></licenseExpiration>
</licenseExpirationCYList>
</licenseExpirationCYnInd>
</license>
</licenseList>
<profileId>12231211</profileId>
<npi>3000000001</npi>
</physicianProfile>
</physicianProfiles>
All the 2 million physician records have been loaded to native Java HashMap, Apache Ignite's Distributed Cache and Hazlecast's Distributed Cache to measured and compare the key performance metrics like the memory and CPU utilization along with load and read times.
The first option I have tried is native Java HashMap to load all the 2 million physician records. Below is the code to parse through the XML file and create a HashMap at the application startup.
Domain Class Used
xxxxxxxxxx
package prototype.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
public class PhysicianProfile implements Serializable {
private static final long serialVersionUID = 3056557315467894990L;
private String npi;
private String profileId;
private ListMultimap<String, String> nameMultiMap = LinkedListMultimap.create();
private List<String> licenseList = new ArrayList<String>();
private ListMultimap<String, String> licenseExpirationMultiMap;
public String getNpi() {
return npi;
}
public void setNpi(String npi) {
this.npi = npi;
}
public String getProfileId() {
return profileId;
}
public void setProfileId(String profileId) {
this.profileId = profileId;
}
public ListMultimap<String, String> getNameMultiMap() {
return nameMultiMap;
}
public void setNameMultiMap(ListMultimap<String, String> nameMultiMap) {
this.nameMultiMap = nameMultiMap;
}
public List<String> getLicenseList() {
return licenseList;
}
public void setLicenseList(List<String> licenseList) {
this.licenseList = licenseList;
}
public ListMultimap<String, String> getLicenseExpirationMultiMap() {
if(null != licenseExpirationMultiMap){
return licenseExpirationMultiMap;
}else{
return LinkedListMultimap.create();
}
}
public void setLicenseExpirationMultiMap(ListMultimap<String, String> licenseExpirationMultiMap) {
this.licenseExpirationMultiMap = licenseExpirationMultiMap;
}
}
Java HashMap Loader
x
package prototype.physician.cache;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.log4j.Logger;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import prototype.domain.PhysicianProfile;
public class PhysicianProfileXMLUtil extends DefaultHandler {
static private Logger logger = Logger.getLogger(PhysicianProfileXMLUtil.class);
private StopWatch stopWatch = new StopWatch();
private String npi = null;
private String profileId = null;
private String firstName = null;
private String lastName = null;
private String alternateFirstName = null;
private String alternateLastName = null;
private PhysicianProfile physicianProfile = null;
private String licenseState = null;
private String licenseNumber = null;
private static final long MEGABYTE = 1024L * 1024L;
/**
* @param bytes
* @return
*/
public static long bytesToMegabytes(long bytes) {
return bytes / MEGABYTE;
}
public static HashMap<String, PhysicianProfile> inMemoryPhysicianMap = new HashMap<String, PhysicianProfile>();
private StringBuilder textBuilder;
private boolean isTextField;
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
*/
public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException {
switch (qName) {
case "physicianProfiles":
stopWatch.start();
break;
case "physicianProfile":
physicianProfile = new PhysicianProfile();
break;
case "firstName":
isTextField = true;
textBuilder = new StringBuilder();
break;
case "lastName":
isTextField = true;
textBuilder = new StringBuilder();
break;
case "alternateFirstName":
isTextField = true;
textBuilder = new StringBuilder();
break;
case "alternateLastName":
isTextField = true;
textBuilder = new StringBuilder();
break;
case "npi":
isTextField = true;
textBuilder = new StringBuilder();
break;
case "profileId":
isTextField = true;
textBuilder = new StringBuilder();
break;
case "licenseState":
isTextField = true;
textBuilder = new StringBuilder();
break;
case "licenseNumber":
isTextField = true;
textBuilder = new StringBuilder();
break;
default:
break;
}
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
*/
public void endElement(String uri, String localName, String qName) throws SAXException {
switch (qName) {
case "physicianProfiles":
stopWatch.stop();
System.out.println("Time Taken to complete :: "+stopWatch.getTime());
System.out.println("In-Memory List Size :: "+inMemoryPhysicianMap.size());
break;
case "physicianProfile":
physicianProfile.setNpi(npi.trim());
physicianProfile.setProfileId(profileId.trim());
if(null != npi){
inMemoryPhysicianMap.put(npi, physicianProfile);
}
clearAttributes();
break;
case "name":
if(StringUtils.isNotBlank(firstName)){
physicianProfile.getNameMultiMap().put(firstName, lastName);
}
if(StringUtils.isNotBlank(alternateFirstName)){
physicianProfile.getNameMultiMap().put(alternateFirstName, alternateLastName);
}
clearNameAttributes();
break;
case "firstName":
firstName = this.textBuilder.toString().toUpperCase();
this.textBuilder = null;
isTextField = false;
break;
case "lastName":
lastName = this.textBuilder.toString().toUpperCase();
this.textBuilder = null;
isTextField = false;
break;
case "alternateFirstName":
alternateFirstName = this.textBuilder.toString().toUpperCase();
this.textBuilder = null;
isTextField = false;
break;
case "alternateLastName":
alternateLastName = this.textBuilder.toString().toUpperCase();
this.textBuilder = null;
isTextField = false;
break;
case "npi":
npi = this.textBuilder.toString();
this.textBuilder = null;
isTextField = false;
break;
case "profileId":
profileId = this.textBuilder.toString();
this.textBuilder = null;
isTextField = false;
break;
case "licenseState":
licenseState = this.textBuilder.toString();
this.textBuilder = null;
isTextField = false;
break;
case "licenseNumber":
licenseNumber = this.textBuilder.toString();
this.textBuilder = null;
isTextField = false;
break;
case "license":
if(StringUtils.isNotBlank(licenseState) && StringUtils.isNotBlank(licenseNumber)
&& !physicianProfile.getLicenseList().contains(licenseState.toUpperCase()+"-"+licenseNumber.toUpperCase())){
physicianProfile.getLicenseList().add(licenseState.toUpperCase()+"-"+licenseNumber.toUpperCase());
}
clearLicenseAttributes();
break;
default:
break;
}
}
/**
*
*/
private void clearLicenseAttributes() {
licenseState = null;
licenseNumber = null;
}
/**
*
*/
private void clearNameAttributes() {
firstName = null;
lastName = null;
alternateFirstName = null;
alternateLastName = null;
}
/**
*
*/
private void clearAttributes() {
npi = null;
profileId = null;
clearNameAttributes();
clearLicenseAttributes();
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
*/
public void characters(char[] chars, int start, int length) throws SAXException {
if(isTextField) {
textBuilder.append(chars, start, length);
}
}
/**
* @param args
* @throws SAXException
* @throws ParserConfigurationException
* @throws IOException
*/
public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
DefaultHandler handler = new PhysicianProfileXMLUtil();
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setValidating(false);
SAXParser parser = factory.newSAXParser();
FileInputStream fin=new FileInputStream("C:\\testfile.xml");
BufferedInputStream bin=new BufferedInputStream(fin);
parser.parse(bin, handler);
StopWatch stopWatch = new StopWatch();
stopWatch.start();
//get random profile
String profileId = PhysicianProfileXMLUtil.inMemoryPhysicianMap.get("3001999993").getProfileId();
stopWatch.stop();
System.out.println("PhysicianProfileXMLUtil.inMemoryPhysicianMap.get(\"3001999993\").getProfileId() takes " +
stopWatch.getNanoTime() + " nano seconds");
System.out.println(profileId);
stopWatch.reset();
stopWatch.start();
//get name map from random profile
String nameMap = PhysicianProfileXMLUtil.inMemoryPhysicianMap.get("3000999993").getNameMultiMap().asMap().toString();
stopWatch.stop();
System.out.println("PhysicianProfileXMLUtil.inMemoryPhysicianMap.get(\"3000999993\").getNameMultiMap().asMap().toString() " +
stopWatch.getNanoTime() + " nano seconds");
System.out.println(nameMap);
stopWatch.reset();
stopWatch.start();
//get name list from random profile
List<String> nameList = PhysicianProfileXMLUtil.inMemoryPhysicianMap.get("3000099993").getNameMultiMap().get("AltFirstName2");
stopWatch.stop();
System.out.println("PhysicianProfileXMLUtil.inMemoryPhysicianMap.get(\"3000099993\").getNameMultiMap().get(\"AltFirstName2\") takes " +
stopWatch.getNanoTime() + " nano seconds");
System.out.println(nameList);
stopWatch.reset();
stopWatch.start();
boolean flag = PhysicianProfileXMLUtil.inMemoryPhysicianMap.get("3000009993").getLicenseList().contains("STATE-LCNS5678");
stopWatch.stop();
System.out.println("PhysicianProfileXMLUtil.inMemoryPhysicianMap.get(\"3000009993\").getLicenseList().contains(\"STATE-LCNS5678\") takes " +
stopWatch.getNanoTime() + " nano seconds");
System.out.println(flag);
stopWatch.reset();
stopWatch.start();
flag = PhysicianProfileXMLUtil.inMemoryPhysicianMap.get("3000000993").getLicenseList().contains("STATE-LCNS23123");
stopWatch.stop();
System.out.println("PhysicianProfileXMLUtil.inMemoryPhysicianMap.get(\"3000000993\").getLicenseList().contains(\"STATE-LCNS23123\") takes " +
stopWatch.getNanoTime() + " nano seconds");
System.out.println(flag);
// Get the Java runtime
Runtime runtime = Runtime.getRuntime();
System.out.println(bytesToMegabytes(runtime.totalMemory()));
// Run the garbage collector
runtime.gc();
System.out.println(bytesToMegabytes(runtime.freeMemory()));
// Calculate the used memory
long memory = runtime.totalMemory() - runtime.freeMemory();
System.out.println("Used memory is bytes: " + memory);
System.out.println("Used memory is megabytes: " + bytesToMegabytes(memory));
}
}
The second option used is Apache Ignite's Key-Value store. Below is the code for parsing the physician XML and load it to the Ignite's data grid.
Apache Ignite Data Grid Loader
In order to use Apache Ignite's cache, I have replaced below line of code in the Java class
xxxxxxxxxx
public static HashMap<String, PhysicianProfile> inMemoryPhysicianMap = new HashMap<String, PhysicianProfile>();
with below code
xxxxxxxxxx
public static Ignite ignite = Ignition.start("ignite.xml");
public static IgniteCache<String, PhysicianProfile> inMemoryPhysicianMap = ignite.getOrCreateCache(CACHE_NAME);
HazelCast Map Loader
For HazelCast Map, I have replaced the below line of code
xxxxxxxxxx
public static HashMap<String, PhysicianProfile> inMemoryPhysicianMap = new
HashMap<String, PhysicianProfile>();
with below code
x
public static HazelcastInstance hz = Hazelcast.newHazelcastInstance();
public static IMap<String, PhysicianProfile> inMemoryPhysicianMap = hz.getMap(CACHE_NAME);
Results
All the loader classes were run on a machine with Windows 10 64-bit Operating System using Amazon Corretto JDK 11. The computer has 16GB ram with Intel Core i7-6820HQ 2.70GHz Processor. Both Apache Ignite and Hazelcast nodes were created with out-of-the-box configuration. I have run all three programs 10 times and taken an average of the results. Below are the results of the three maps when installed as a single node.
Java Native HashMap | Apache Ignite Cache | Hazelcast Map | |
Time consumed to load 2 million records | 114.525 Seconds | 135.695 Seconds | 301.015 Seconds |
Average read time from Map | 10194 Nanoseconds = 0.010194 Milliseconds | 3432403 Nanoseconds = 3.432403 Milliseconds | 7979049 Nanoseconds = 7.979049 Milliseconds |
Memory consumed to store 2 million physician profiles | 2530 MB | 24 MB | 1448 MB |
CPU Utilization Graphs
Java Native HashMap
Apache Ignite
Hazelcast
Final Verdict
Although Java Native Hashmap read times are 300 times faster when compared to Apache Ignite, in a clustered environment that has resource constraints it is almost impossible to use it due to its resource utilization. The application will have issues when more records needed to be loaded into the Java Native Hashmap.
For limited data in a non-clustered environment, Hashmap will be an ideal solution. Whereas in a clustered environment, data will need to be redundantly loaded to all the JVMs, which will not be an ideal way of using the memory across the JVMs. This is not the case with Apache Ignite and Hazelcast as they can be easily deployed in network topologies in order for the application to access the map across the network.
Java Native HashMap has used around 2.5 GB of memory whereas Apache Ignite has only used 24 MB while Hazelcast has used 1448 MB to store all the 2 million physician profiles. CPU utilization of Java Native Hashmap loader kept increasing by the number of records inserted while Apache Ignite and Hazelcast loaders CPU utilization are almost flat. Hazelcast was far behind Apache Ignite in Loading, Reading times and in resource utilization.
This makes Apache Ignite a clear winner for Distributed Map use cases.
Further Reading
Opinions expressed by DZone contributors are their own.
Comments