обзор
В этой статье сравниваются различные варианты карт в памяти и их производительности, чтобы приложение отошло от традиционных таблиц СУБД для часто используемых данных. В этом случае для демонстрации я взял 2 миллиона фиктивных записей врачей, которые находятся в таблице базы данных, и перенес их на карты в памяти. Миграция позволит приложению быстро искать на карте и проверять врача, а не запрашивать таблицу базы данных для проверки.
Вам также может понравиться: Hazelcast с весенней загрузкой на Kubernetes
Исходный код для клиентов Java для создания этих распределенных карт только для чтения также был добавлен в эту статью. Все используемые клиенты Java будут создавать карту в памяти для сохранения объектов врача с NPI врача в качестве ключа на карте. Я создал XML для экспорта всех 2 миллионов врачей для этой цели, и ниже приведен пример этого XML.
XML
xxxxxxxxxx
1
2
<physicianProfiles xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3
<physicianProfile>
4
<nameList>
5
<name>
6
<firstName>FirstName1</firstName>
7
<lastName>LastName1</lastName>
8
<alternateFirstName>AltFirstName1</alternateFirstName>
9
<alternateLastName>AltLastName1</alternateLastName>
10
</name>
11
<name>
12
<firstName>AltFirstName2</firstName>
13
<lastName>AltFirstName2</lastName>
14
</name>
15
<name>
16
<firstName>AltFirstName3</firstName>
17
<lastName>AltFirstName3</lastName>
18
</name>
19
</nameList>
20
<licenseList>
21
<license>
22
<licenseState>State</licenseState>
23
<licenseNumber>LCNS1234</licenseNumber>
24
<licenseExpirationCYnInd>
25
<licenseExpirationCYList>
26
<licenseExpiration ></licenseExpiration>
27
</licenseExpirationCYList>
28
</licenseExpirationCYnInd>
29
</license>
30
<license>
31
<licenseState>State</licenseState>
32
<licenseNumber>LCNS5678</licenseNumber>
33
<licenseExpirationCYnInd>
34
<licenseExpirationCYList>
35
<licenseExpiration ></licenseExpiration>
36
</licenseExpirationCYList>
37
</licenseExpirationCYnInd>
38
</license>
39
<license>
40
<licenseState>State</licenseState>
41
<licenseNumber>LCNS23123</licenseNumber>
42
<licenseExpirationCYnInd>
43
<licenseExpirationCYList>
44
<licenseExpiration ></licenseExpiration>
45
</licenseExpirationCYList>
46
</licenseExpirationCYnInd>
47
</license>
48
</licenseList>
49
<profileId>12231211</profileId>
50
<npi>3000000001</npi>
51
</physicianProfile>
52
</physicianProfiles>
Все 2 миллиона записей врачей были загружены в собственный Java HashMap , распределенный кэш Apache Ignite и распределенный кэш Hazlecast для измерения и сравнения ключевых показателей производительности, таких как использование памяти и ЦП, а также времени загрузки и чтения.
Первый вариант, который я попробовал — это встроенный Java HashMap для загрузки всех 2 миллионов записей о врачах. Ниже приведен код для анализа XML-файла и создания HashMap при запуске приложения.
Используемый класс домена
Джава
xxxxxxxxxx
1
package prototype.domain;
2
import java.io.Serializable;
4
import java.util.ArrayList;
5
import java.util.List;
6
import com.google.common.collect.LinkedListMultimap;
8
import com.google.common.collect.ListMultimap;
9
public class PhysicianProfile implements Serializable {
11
private static final long serialVersionUID = 3056557315467894990L;
13
private String npi;
14
private String profileId;
15
private ListMultimap<String, String> nameMultiMap = LinkedListMultimap.create();
16
private List<String> licenseList = new ArrayList<String>();
17
private ListMultimap<String, String> licenseExpirationMultiMap;
18
public String getNpi() {
20
return npi;
21
}
22
public void setNpi(String npi) {
23
this.npi = npi;
24
}
25
public String getProfileId() {
26
return profileId;
27
}
28
public void setProfileId(String profileId) {
29
this.profileId = profileId;
30
}
31
public ListMultimap<String, String> getNameMultiMap() {
32
return nameMultiMap;
33
}
34
public void setNameMultiMap(ListMultimap<String, String> nameMultiMap) {
35
this.nameMultiMap = nameMultiMap;
36
}
37
public List<String> getLicenseList() {
38
return licenseList;
39
}
40
public void setLicenseList(List<String> licenseList) {
41
this.licenseList = licenseList;
42
}
43
public ListMultimap<String, String> getLicenseExpirationMultiMap() {
44
if(null != licenseExpirationMultiMap){
45
return licenseExpirationMultiMap;
46
}else{
47
return LinkedListMultimap.create();
48
}
49
}
50
public void setLicenseExpirationMultiMap(ListMultimap<String, String> licenseExpirationMultiMap) {
51
this.licenseExpirationMultiMap = licenseExpirationMultiMap;
52
}
53
}
55
Java HashMap Loader
x
1
package prototype.physician.cache;
2
import java.io.BufferedInputStream;
4
import java.io.FileInputStream;
5
import java.io.IOException;
6
import java.util.HashMap;
7
import java.util.List;
8
import javax.xml.parsers.ParserConfigurationException;
10
import javax.xml.parsers.SAXParser;
11
import javax.xml.parsers.SAXParserFactory;
12
import org.apache.commons.lang3.StringUtils;
14
import org.apache.commons.lang3.time.StopWatch;
15
import org.apache.log4j.Logger;
16
import org.xml.sax.Attributes;
17
import org.xml.sax.SAXException;
18
import org.xml.sax.helpers.DefaultHandler;
19
import prototype.domain.PhysicianProfile;
21
public class PhysicianProfileXMLUtil extends DefaultHandler {
23
24
static private Logger logger = Logger.getLogger(PhysicianProfileXMLUtil.class);
25
26
private StopWatch stopWatch = new StopWatch();
27
private String npi = null;
28
private String profileId = null;
29
private String firstName = null;
30
private String lastName = null;
31
private String alternateFirstName = null;
32
private String alternateLastName = null;
33
private PhysicianProfile physicianProfile = null;
34
private String licenseState = null;
35
private String licenseNumber = null;
36
37
private static final long MEGABYTE = 1024L * 1024L;
38
/**
40
* @param bytes
41
* @return
42
*/
43
public static long bytesToMegabytes(long bytes) {
44
return bytes / MEGABYTE;
45
}
46
47
public static HashMap<String, PhysicianProfile> inMemoryPhysicianMap = new HashMap<String, PhysicianProfile>();
48
private StringBuilder textBuilder;
49
private boolean isTextField;
50
/* (non-Javadoc)
51
* @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
52
*/
53
54
public void startElement(String uri, String localName, String qName,
55
Attributes attributes) throws SAXException {
56
switch (qName) {
57
case "physicianProfiles":
58
stopWatch.start();
59
break;
60
case "physicianProfile":
61
physicianProfile = new PhysicianProfile();
62
break;
63
case "firstName":
64
isTextField = true;
65
textBuilder = new StringBuilder();
66
break;
67
case "lastName":
68
isTextField = true;
69
textBuilder = new StringBuilder();
70
break;
71
case "alternateFirstName":
72
isTextField = true;
73
textBuilder = new StringBuilder();
74
break;
75
case "alternateLastName":
76
isTextField = true;
77
textBuilder = new StringBuilder();
78
break;
79
case "npi":
80
isTextField = true;
81
textBuilder = new StringBuilder();
82
break;
83
case "profileId":
84
isTextField = true;
85
textBuilder = new StringBuilder();
86
break;
87
case "licenseState":
88
isTextField = true;
89
textBuilder = new StringBuilder();
90
break;
91
case "licenseNumber":
92
isTextField = true;
93
textBuilder = new StringBuilder();
94
break;
95
default:
96
break;
97
}
98
99
}
100
101
/* (non-Javadoc)
102
* @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
103
*/
104
105
public void endElement(String uri, String localName, String qName) throws SAXException {
106
switch (qName) {
107
case "physicianProfiles":
108
stopWatch.stop();
109
System.out.println("Time Taken to complete :: "+stopWatch.getTime());
110
System.out.println("In-Memory List Size :: "+inMemoryPhysicianMap.size());
111
break;
112
case "physicianProfile":
113
physicianProfile.setNpi(npi.trim());
114
physicianProfile.setProfileId(profileId.trim());
115
if(null != npi){
116
inMemoryPhysicianMap.put(npi, physicianProfile);
117
}
118
clearAttributes();
119
break;
120
case "name":
121
if(StringUtils.isNotBlank(firstName)){
122
physicianProfile.getNameMultiMap().put(firstName, lastName);
123
}
124
if(StringUtils.isNotBlank(alternateFirstName)){
125
physicianProfile.getNameMultiMap().put(alternateFirstName, alternateLastName);
126
}
127
clearNameAttributes();
128
break;
129
case "firstName":
130
firstName = this.textBuilder.toString().toUpperCase();
131
this.textBuilder = null;
132
isTextField = false;
133
break;
134
case "lastName":
135
lastName = this.textBuilder.toString().toUpperCase();
136
this.textBuilder = null;
137
isTextField = false;
138
break;
139
case "alternateFirstName":
140
alternateFirstName = this.textBuilder.toString().toUpperCase();
141
this.textBuilder = null;
142
isTextField = false;
143
break;
144
case "alternateLastName":
145
alternateLastName = this.textBuilder.toString().toUpperCase();
146
this.textBuilder = null;
147
isTextField = false;
148
break;
149
case "npi":
150
npi = this.textBuilder.toString();
151
this.textBuilder = null;
152
isTextField = false;
153
break;
154
case "profileId":
155
profileId = this.textBuilder.toString();
156
this.textBuilder = null;
157
isTextField = false;
158
break;
159
case "licenseState":
160
licenseState = this.textBuilder.toString();
161
this.textBuilder = null;
162
isTextField = false;
163
break;
164
case "licenseNumber":
165
licenseNumber = this.textBuilder.toString();
166
this.textBuilder = null;
167
isTextField = false;
168
break;
169
case "license":
170
if(StringUtils.isNotBlank(licenseState) && StringUtils.isNotBlank(licenseNumber)
171
&& !physicianProfile.getLicenseList().contains(licenseState.toUpperCase()+"-"+licenseNumber.toUpperCase())){
172
physicianProfile.getLicenseList().add(licenseState.toUpperCase()+"-"+licenseNumber.toUpperCase());
173
}
174
clearLicenseAttributes();
175
break;
176
default:
177
break;
178
179
}
180
}
181
182
/**
183
*
184
*/
185
private void clearLicenseAttributes() {
186
licenseState = null;
187
licenseNumber = null;
188
}
189
/**
191
*
192
*/
193
private void clearNameAttributes() {
194
firstName = null;
195
lastName = null;
196
alternateFirstName = null;
197
alternateLastName = null;
198
}
199
/**
201
*
202
*/
203
private void clearAttributes() {
204
npi = null;
205
profileId = null;
206
clearNameAttributes();
207
clearLicenseAttributes();
208
}
209
/* (non-Javadoc)
211
* @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
212
*/
213
214
public void characters(char[] chars, int start, int length) throws SAXException {
215
if(isTextField) {
216
textBuilder.append(chars, start, length);
217
}
218
}
219
220
221
222
/**
223
* @param args
224
* @throws SAXException
225
* @throws ParserConfigurationException
226
* @throws IOException
227
*/
228
public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
229
DefaultHandler handler = new PhysicianProfileXMLUtil();
230
231
SAXParserFactory factory = SAXParserFactory.newInstance();
232
233
factory.setValidating(false);
234
235
SAXParser parser = factory.newSAXParser();
236
237
FileInputStream fin=new FileInputStream("C:\\testfile.xml");
238
239
BufferedInputStream bin=new BufferedInputStream(fin);
240
241
parser.parse(bin, handler);
242
StopWatch stopWatch = new StopWatch();
243
stopWatch.start();
244
//get random profile
245
String profileId = PhysicianProfileXMLUtil.inMemoryPhysicianMap.get("3001999993").getProfileId();
246
stopWatch.stop();
247
System.out.println("PhysicianProfileXMLUtil.inMemoryPhysicianMap.get(\"3001999993\").getProfileId() takes " +
248
stopWatch.getNanoTime() + " nano seconds");
249
System.out.println(profileId);
250
stopWatch.reset();
251
252
stopWatch.start();
253
//get name map from random profile
254
String nameMap = PhysicianProfileXMLUtil.inMemoryPhysicianMap.get("3000999993").getNameMultiMap().asMap().toString();
255
stopWatch.stop();
256
System.out.println("PhysicianProfileXMLUtil.inMemoryPhysicianMap.get(\"3000999993\").getNameMultiMap().asMap().toString() " +
257
stopWatch.getNanoTime() + " nano seconds");
258
System.out.println(nameMap);
259
stopWatch.reset();
260
261
stopWatch.start();
262
//get name list from random profile
263
List<String> nameList = PhysicianProfileXMLUtil.inMemoryPhysicianMap.get("3000099993").getNameMultiMap().get("AltFirstName2");
264
stopWatch.stop();
265
System.out.println("PhysicianProfileXMLUtil.inMemoryPhysicianMap.get(\"3000099993\").getNameMultiMap().get(\"AltFirstName2\") takes " +
266
stopWatch.getNanoTime() + " nano seconds");
267
System.out.println(nameList);
268
stopWatch.reset();
269
270
stopWatch.start();
271
boolean flag = PhysicianProfileXMLUtil.inMemoryPhysicianMap.get("3000009993").getLicenseList().contains("STATE-LCNS5678");
272
stopWatch.stop();
273
System.out.println("PhysicianProfileXMLUtil.inMemoryPhysicianMap.get(\"3000009993\").getLicenseList().contains(\"STATE-LCNS5678\") takes " +
274
stopWatch.getNanoTime() + " nano seconds");
275
System.out.println(flag);
276
stopWatch.reset();
277
278
stopWatch.start();
279
flag = PhysicianProfileXMLUtil.inMemoryPhysicianMap.get("3000000993").getLicenseList().contains("STATE-LCNS23123");
280
stopWatch.stop();
281
System.out.println("PhysicianProfileXMLUtil.inMemoryPhysicianMap.get(\"3000000993\").getLicenseList().contains(\"STATE-LCNS23123\") takes " +
282
stopWatch.getNanoTime() + " nano seconds");
283
System.out.println(flag);
284
285
// Get the Java runtime
286
Runtime runtime = Runtime.getRuntime();
287
System.out.println(bytesToMegabytes(runtime.totalMemory()));
288
// Run the garbage collector
289
runtime.gc();
290
System.out.println(bytesToMegabytes(runtime.freeMemory()));
291
// Calculate the used memory
292
long memory = runtime.totalMemory() - runtime.freeMemory();
293
System.out.println("Used memory is bytes: " + memory);
294
System.out.println("Used memory is megabytes: " + bytesToMegabytes(memory));
295
296
}
297
}
299
Второй используемый вариант - хранилище ключей-значений Apache Ignite . Ниже приведен код для синтаксического анализа XML-файла врача и его загрузки в таблицу данных Ignite.
Apache Ignite Data Grid Loader
Чтобы использовать кэш Apache Ignite, я заменил строку кода ниже в классе Java
xxxxxxxxxx
1
public static HashMap<String, PhysicianProfile> inMemoryPhysicianMap = new HashMap<String, PhysicianProfile>();
2
с кодом ниже
xxxxxxxxxx
1
public static Ignite ignite = Ignition.start("ignite.xml");
2
public static IgniteCache<String, PhysicianProfile> inMemoryPhysicianMap = ignite.getOrCreateCache(CACHE_NAME);
HazelCast Map Loader
Для HazelCast Map я заменил приведенную ниже строку кода
XML
xxxxxxxxxx
1
public static HashMap<String, PhysicianProfile> inMemoryPhysicianMap = new
2
HashMap<String, PhysicianProfile>();
с кодом ниже
Джава
x
1
public static HazelcastInstance hz = Hazelcast.newHazelcastInstance();
2
public static IMap<String, PhysicianProfile> inMemoryPhysicianMap = hz.getMap(CACHE_NAME);
Полученные результаты
Все классы загрузчиков запускались на компьютере с 64-битной операционной системой Windows 10 с использованием Amazon Corretto JDK 11. Компьютер имеет 16 ГБ ОЗУ с процессором Intel Core i7-6820HQ 2,70 ГГц. Оба узла Apache Ignite и Hazelcast были созданы с готовой конфигурацией. Я выполнил все три программы 10 раз и взял среднее значение результатов. Ниже приведены результаты трех карт при установке в качестве одного узла.
Java Native HashMap | Apache Ignite Cache | Карта Hazelcast | |
Время, затраченное на загрузку 2 миллионов записей | 114,525 секунд | 135,695 секунды | 301.015 секунды |
Среднее время чтения с карты | 10194 наносекунды = 0,010194 миллисекунды | 3432403 наносекунды = 3,432403 миллисекунды | 7979049 наносекунд = 7,979049 миллисекунд |
Память, используемая для хранения 2 миллионов профилей врачей | 2530 МБ | 24 МБ | 1448 МБ |
Графики использования ЦП
Java Native HashMap
Apache Ignite
Hazelcast
Окончательный вердикт
Хотя время чтения Java Native Hashmap в 300 раз быстрее по сравнению с Apache Ignite, в кластерной среде с ограничениями по ресурсам его практически невозможно использовать из-за использования ресурсов. Приложение будет иметь проблемы, когда в Java Native Hashmap потребуется загрузить больше записей.
Для ограниченных данных в некластеризованной среде Hashmap будет идеальным решением. Принимая во внимание, что в кластерной среде данные должны быть избыточно загружены во все JVM, что не будет идеальным способом использования памяти через JVM. Это не относится к Apache Ignite и Hazelcast, так как они могут быть легко развернуты в сетевых топологиях, чтобы приложение могло получить доступ к карте по сети.
Java Native HashMap использовал около 2,5 ГБ памяти, тогда как Apache Ignite использовал только 24 МБ, в то время как Hazelcast использовал 1448 МБ для хранения всех 2 миллионов профилей врачей. Загрузка ЦП загрузчика Java Native Hashmap продолжала увеличиваться за счет количества вставленных записей, в то время как загрузчики Apache Ignite и Hazelcast практически не загружались. Hazelcast сильно отставал от Apache Ignite по загрузке, чтению и использованию ресурсов.
Это делает Apache Ignite явным победителем в случаях использования распределенной карты.
Дальнейшее чтение
Hazelcast для Go Getters, часть 1