Статьи

Java Best Practices — высокопроизводительная сериализация

Продолжая серию статей о предлагаемых методах при работе с языком программирования Java, мы собираемся обсудить и продемонстрировать, как использовать объектную сериализацию для высокопроизводительных приложений.

Все обсуждаемые темы основаны на сценариях использования, полученных в результате разработки критически важных, высокопроизводительных производственных систем для телекоммуникационной отрасли.

Перед прочтением каждого раздела этой статьи настоятельно рекомендуется ознакомиться с соответствующей документацией по Java API для получения подробной информации и примеров кода.

Все тесты выполняются на Sony Vaio со следующими характеристиками:

  • Система: openSUSE 11.1 (x86_64)
  • Процессор (ЦП): Процессор Intel® Core ™ 2 Duo T6670 с частотой 2,20 ГГц
  • Скорость процессора: 1200,00 МГц
  • Общий объем памяти (ОЗУ): 2,8 ГБ
  • Java: OpenJDK 1.6.0_0 64-битная

Применяется следующая тестовая конфигурация:

  • Параллельный рабочий потоков: 200
  • Тест повторений на одного работника Тема: 1000
  • Всего тестовых прогонов: 100

Высокая производительность сериализации

Сериализация — это процесс преобразования объекта в поток байтов. Этот поток затем может быть отправлен через сокет, сохранен в файле и / или базе данных или просто обработан как есть. В этой статье мы не собираемся представлять подробное описание механизма сериализации, существует множество статей, которые предоставляют такую ​​информацию. Здесь будет обсуждаться наше предложение использовать сериализацию для достижения высоких результатов.

Три основные проблемы с производительностью при сериализации:

  • Сериализация — это рекурсивный алгоритм. Начиная с одного объекта, все объекты, которые могут быть получены из этого объекта с помощью следующих переменных экземпляра, также сериализуются. Поведение по умолчанию может легко привести к ненужным издержкам сериализации
  • И сериализация, и десериализация требуют, чтобы механизм сериализации обнаружил информацию об экземпляре, который он сериализует. Используя механизм сериализации по умолчанию, будем использовать отражение, чтобы обнаружить все значения полей. Более того, если вы явно не установите атрибут класса «serialVersionUID», механизм сериализации должен его вычислить. Это включает в себя прохождение всех полей и методов для генерации хеша. Вышеупомянутая процедура может быть довольно медленной
  • Используя механизм сериализации по умолчанию, вся информация описания класса сериализации включается в поток, например:
    • Описание всех сериализуемых суперклассов
    • Описание самого класса
    • Данные экземпляра, связанные с конкретным экземпляром класса

Чтобы решить вышеупомянутые проблемы с производительностью, вы можете использовать Externalization. Основное различие между этими двумя методами состоит в том, что Serialization записывает описания классов всех сериализуемых суперклассов вместе с информацией, связанной с экземпляром, когда рассматривается как экземпляр каждого отдельного суперкласса. Экстернализация, с другой стороны, записывает идентичность класса (имя класса и соответствующий атрибут класса «serialVersionUID») вместе со структурой суперкласса и всей информацией об иерархии классов. Другими словами, он хранит все метаданные, но записывает только информацию локального экземпляра. Короче говоря, Externalization устраняет почти все отражающие вызовы, используемые механизмом сериализации, и дает вам полный контроль над алгоритмами маршалинга и демаршаллинга, что приводит к значительному повышению производительности.

Конечно, эффективность экстернализации имеет свою цену. Механизм сериализации по умолчанию адаптируется к изменениям приложения благодаря тому, что метаданные автоматически извлекаются из определений классов. С другой стороны, экстернализация не очень гибка и требует от вас переписывания кода маршалинга и демаршаллинга всякий раз, когда вы меняете определения классов.

Далее следует короткая демонстрация того, как использовать Externalization для высокопроизводительных приложений. Мы начнем с предоставления объекта «Сотрудник» для выполнения операций сериализации и десериализации. Будут использованы два варианта объекта «Сотрудник». Один из них подходит для стандартных операций сериализации, а другой изменен, чтобы его можно было выводить из системы.

Ниже приведен первый вариант объекта «Сотрудник»:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
package com.javacodegeeks.test;
 
import java.io.Serializable;
import java.util.Date;
import java.util.List;
 
public class Employee implements Serializable {
 
 private static final long serialVersionUID = 3657773293974543890L;
  
 private String firstName;
 private String lastName;
 private String socialSecurityNumber;
 private String department;
 private String position;
 private Date hireDate;
 private Double salary;
 private Employee supervisor;
 private List<string> phoneNumbers;
  
 public Employee() {
 }
  
 public Employee(String firstName, String lastName,
   String socialSecurityNumber, String department, String position,
   Date hireDate, Double salary) {
  this.firstName = firstName;
  this.lastName = lastName;
  this.socialSecurityNumber = socialSecurityNumber;
  this.department = department;
  this.position = position;
  this.hireDate = hireDate;
  this.salary = salary;
 }
 
 public String getFirstName() {
  return firstName;
 }
 
 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }
 
 public String getLastName() {
  return lastName;
 }
 
 public void setLastName(String lastName) {
  this.lastName = lastName;
 }
 
 public String getSocialSecurityNumber() {
  return socialSecurityNumber;
 }
 
 public void setSocialSecurityNumber(String socialSecurityNumber) {
  this.socialSecurityNumber = socialSecurityNumber;
 }
 
 public String getDepartment() {
  return department;
 }
 
 public void setDepartment(String department) {
  this.department = department;
 }
 
 public String getPosition() {
  return position;
 }
 
 public void setPosition(String position) {
  this.position = position;
 }
 
 public Date getHireDate() {
  return hireDate;
 }
 
 public void setHireDate(Date hireDate) {
  this.hireDate = hireDate;
 }
 
 public Double getSalary() {
  return salary;
 }
 
 public void setSalary(Double salary) {
  this.salary = salary;
 }
 
 public Employee getSupervisor() {
  return supervisor;
 }
 
 public void setSupervisor(Employee supervisor) {
  this.supervisor = supervisor;
 }
 
 public List<string> getPhoneNumbers() {
  return phoneNumbers;
 }
 
 public void setPhoneNumbers(List<string> phoneNumbers) {
  this.phoneNumbers = phoneNumbers;
 }
 
}

Что следует отметить здесь:

  • Мы предполагаем, что следующие поля являются обязательными:
    • «имя»
    • «фамилия»
    • «номер социального страхования»
    • «Отдел»
    • «позиция»
    • «Дата приема на работу»
    • «зарплата»

Ниже приводится вторая разновидность объекта «Сотрудник»:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
package com.javacodegeeks.test;
 
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
 
public class Employee implements Externalizable {
 
 private String firstName;
 private String lastName;
 private String socialSecurityNumber;
 private String department;
 private String position;
 private Date hireDate;
 private Double salary;
 private Employee supervisor;
 private List<string> phoneNumbers;
  
 public Employee() {
 }
  
 public Employee(String firstName, String lastName,
   String socialSecurityNumber, String department, String position,
   Date hireDate, Double salary) {
  this.firstName = firstName;
  this.lastName = lastName;
  this.socialSecurityNumber = socialSecurityNumber;
  this.department = department;
  this.position = position;
  this.hireDate = hireDate;
  this.salary = salary;
 }
 
 public String getFirstName() {
  return firstName;
 }
 
 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }
 
 public String getLastName() {
  return lastName;
 }
 
 public void setLastName(String lastName) {
  this.lastName = lastName;
 }
 
 public String getSocialSecurityNumber() {
  return socialSecurityNumber;
 }
 
 public void setSocialSecurityNumber(String socialSecurityNumber) {
  this.socialSecurityNumber = socialSecurityNumber;
 }
 
 public String getDepartment() {
  return department;
 }
 
 public void setDepartment(String department) {
  this.department = department;
 }
 
 public String getPosition() {
  return position;
 }
 
 public void setPosition(String position) {
  this.position = position;
 }
 
 public Date getHireDate() {
  return hireDate;
 }
 
 public void setHireDate(Date hireDate) {
  this.hireDate = hireDate;
 }
 
 public Double getSalary() {
  return salary;
 }
 
 public void setSalary(Double salary) {
  this.salary = salary;
 }
 
 public Employee getSupervisor() {
  return supervisor;
 }
 
 public void setSupervisor(Employee supervisor) {
  this.supervisor = supervisor;
 }
 
 public List<string> getPhoneNumbers() {
  return phoneNumbers;
 }
 
 public void setPhoneNumbers(List<string> phoneNumbers) {
  this.phoneNumbers = phoneNumbers;
 }
 
 public void readExternal(ObjectInput objectInput) throws IOException,
   ClassNotFoundException {
   
  this.firstName = objectInput.readUTF();
  this.lastName = objectInput.readUTF();
  this.socialSecurityNumber = objectInput.readUTF();
  this.department = objectInput.readUTF();
  this.position = objectInput.readUTF();
  this.hireDate = new Date(objectInput.readLong());
  this.salary = objectInput.readDouble();
   
  int attributeCount = objectInput.read();
 
  byte[] attributes = new byte[attributeCount];
 
  objectInput.readFully(attributes);
   
  for (int i = 0; i < attributeCount; i++) {
   byte attribute = attributes[i];
 
   switch (attribute) {
   case (byte) 0:
    this.supervisor = (Employee) objectInput.readObject();
    break;
   case (byte) 1:
    this.phoneNumbers = Arrays.asList(objectInput.readUTF().split(";"));
    break;
   }
  }
   
 }
 
 public void writeExternal(ObjectOutput objectOutput) throws IOException {
   
  objectOutput.writeUTF(firstName);
  objectOutput.writeUTF(lastName);
  objectOutput.writeUTF(socialSecurityNumber);
  objectOutput.writeUTF(department);
  objectOutput.writeUTF(position);
  objectOutput.writeLong(hireDate.getTime());
  objectOutput.writeDouble(salary);
   
  byte[] attributeFlags = new byte[2];
   
  int attributeCount = 0;
   
  if (supervisor != null) {
   attributeFlags[0] = (byte) 1;
   attributeCount++;
  }
  if (phoneNumbers != null && !phoneNumbers.isEmpty()) {
   attributeFlags[1] = (byte) 1;
   attributeCount++;
  }
   
  objectOutput.write(attributeCount);
   
  byte[] attributes = new byte[attributeCount];
 
  int j = attributeCount;
 
  for (int i = 0; i < 2; i++)
   if (attributeFlags[i] == (byte) 1) {
    j--;
    attributes[j] = (byte) i;
   }
 
  objectOutput.write(attributes);
   
  for (int i = 0; i < attributeCount; i++) {
   byte attribute = attributes[i];
 
   switch (attribute) {
   case (byte) 0:
    objectOutput.writeObject(supervisor);
    break;
   case (byte) 1:
    StringBuilder rowPhoneNumbers = new StringBuilder();
    for(int k = 0; k < phoneNumbers.size(); k++)
     rowPhoneNumbers.append(phoneNumbers.get(k) + ";");
    rowPhoneNumbers.deleteCharAt(rowPhoneNumbers.lastIndexOf(";"));
    objectOutput.writeUTF(rowPhoneNumbers.toString());
    break;
   }
  }
   
 }
}

Что следует отметить здесь:

  • Мы реализуем метод writeExternal для маршалинга объекта Employee. Все обязательные поля записываются в поток
  • Для поля hireDate мы записываем только количество миллисекунд, представленных этим объектом Date. Предполагая, что демаршаллер будет использовать тот же часовой пояс, что и маршаллер, значение в миллисекундах — это вся информация, которая нам необходима для правильной десериализации поля hireDate. Имейте в виду, что мы могли бы сериализовать весь объект hireDate, используя операцию objectOutput.writeObject (hireDate). В этом случае механизм сериализации по умолчанию сработает, что приведет к снижению скорости и увеличению размера результирующего потока.
  • Все необязательные поля («supervisor» и «phoneNumbers») записываются в поток только тогда, когда они имеют действительные (не нулевые) значения. Для реализации этой функциональности мы используем байтовые массивы attributeFlags и attribute. Каждая позиция массива attributeFlags представляет необязательное поле и содержит «маркер», указывающий, имеет ли конкретное поле значение. Мы проверяем каждое необязательное поле и заполняем байтовый массив attributeFlags соответствующими маркерами. Байтовый массив «attribute» указывает фактические необязательные поля, которые должны быть записаны в поток с помощью «position». Например, если необязательные поля «supervisor» и «phoneNumbers» имеют фактические значения, тогда байтовый массив attributeFlags должен быть [1,1], а байтовый массив attribute — [0,1]. В случае, если только необязательное поле «phoneNumbers» имеет ненулевое значение, байтовый массив «attributeFlags» должен быть [0,1], а байтовый массив «атрибуты» должен быть [1]. Используя вышеупомянутый алгоритм, мы можем достичь минимального размера для результирующего потока. Для правильной десериализации необязательных параметров объекта «Сотрудник» мы должны записать в steam только следующую информацию:
    • Общее количество необязательных параметров, которые будут записаны (он же размер атрибута байтового массива — для разбора демаршаллером)
    • Байтовый массив «атрибутов» (чтобы демаршаллер правильно назначал значения полей)
    • Фактические необязательные значения параметров
  • Для поля «phoneNumbers» мы создаем и записываем в поток строковое представление его содержимого. В качестве альтернативы мы могли бы сериализовать весь объект «phoneNumbers», используя операцию «objectOutput.writeObject (phoneNumbers)». В этом случае механизм сериализации по умолчанию сработает, что приведет к снижению скорости и увеличению размера результирующего потока.
  • Мы реализуем метод readExternal для демаршаллинга объекта Employee. Все обязательные поля записываются в поток. Для необязательных полей демаршаллер назначает соответствующие значения полей в соответствии с протоколом, описанным выше

Для процессов сериализации и десериализации мы использовали следующие четыре функции. Эти функции бывают двух видов. Первая пара подходит для сериализации и десериализации экземпляров объекта Externalizable, тогда как вторая пара подходит для сериализации и десериализации экземпляров объекта Serializable.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static byte[][] serializeObject(Externalizable object) throws Exception {
  ByteArrayOutputStream baos = null;
  ObjectOutputStream oos = null;
  byte[][] res = new byte[2][];
   
  try {
   baos = new ByteArrayOutputStream();
   oos = new ObjectOutputStream(baos);
    
   object.writeExternal(oos);
   oos.flush();
    
   res[0] = object.getClass().getName().getBytes();
   res[1] = baos.toByteArray();
   
  } catch (Exception ex) {
   throw ex;
  } finally {
   try {
    if(oos != null)
     oos.close();
   } catch (Exception e) {
    e.printStackTrace();
   }
  }
   
  return res;
 }
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static Externalizable deserializeObject(byte[][] rowObject) throws Exception {
  ObjectInputStream ois = null;
  String objectClassName = null;
  Externalizable res = null;
   
  try {
    
   objectClassName = new String(rowObject[0]);
   byte[] objectBytes = rowObject[1];
    
   ois = new ObjectInputStream(new ByteArrayInputStream(objectBytes));
    
   Class objectClass = Class.forName(objectClassName);
   res = (Externalizable) objectClass.newInstance();
   res.readExternal(ois);
   
  } catch (Exception ex) {
   throw ex;
  } finally {
   try {
    if(ois != null)
     ois.close();
   } catch (Exception e) {
    e.printStackTrace();
   }
    
  }
   
  return res;
   
 }
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static byte[] serializeObject(Serializable object) throws Exception {
  ByteArrayOutputStream baos = null;
  ObjectOutputStream oos = null;
  byte[] res = null;
   
  try {
   baos = new ByteArrayOutputStream();
   oos = new ObjectOutputStream(baos);
    
   oos.writeObject(object);
   oos.flush();
    
   res = baos.toByteArray();
   
  } catch (Exception ex) {
   throw ex;
  } finally {
   try {
    if(oos != null)
     oos.close();
   } catch (Exception e) {
    e.printStackTrace();
   }
  }
   
  return res;
 }
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static Serializable deserializeObject(byte[] rowObject) throws Exception {
  ObjectInputStream ois = null;
  Serializable res = null;
   
  try {
    
   ois = new ObjectInputStream(new ByteArrayInputStream(rowObject));
   res = (Serializable) ois.readObject();
   
  } catch (Exception ex) {
   throw ex;
  } finally {
   try {
    if(ois != null)
     ois.close();
   } catch (Exception e) {
    e.printStackTrace();
   }
    
  }
   
  return res;
   
 }

Ниже мы представляем таблицу сравнения производительности между двумя вышеупомянутыми подходами

Горизонтальная ось представляет количество тестовых прогонов, а вертикальная ось — среднее количество транзакций в секунду (TPS) для каждого тестового прогона. Таким образом, чем выше значения, тем лучше. Как вы можете видеть, используя подход Externalizable, вы можете добиться превосходного прироста производительности при сериализации и десериализации по сравнению с простым подходом Serializable.

Наконец, мы должны точно указать, что мы выполнили наши тесты, предоставляя значения для всех необязательных полей объекта «Сотрудник». Вы должны ожидать еще большего прироста производительности, если не используете все необязательные параметры для своих тестов, либо при сравнении между одним и тем же подходом и, что наиболее важно, при перекрестном сравнении между подходами Externalizable и Serializable.

Удачного кодирования!

Джастин

Статьи по Теме :