Введение
В программировании часто перед нами встают задачи, которые мы можем решить несколькими путями: найти и использовать уже готовые решения, или же решать задачу самостоятельно. Хоть и написано множество спецификаций и их реализаций, они не всегда дают нам то, что требуется в конкретном случае. Вот и мне в очередной раз пришлось столкнуться с подобной ситуацией.
Задача состояла в хранении объектов в файле в формате xml. Ничего казалось бы сложного, если бы не несколько «но». Объектов много, имеют они древовидную структуру и над ними постоянно выполняются операции добавления, изменения и удаления в разных потоках. Как вы понимаете постоянные запись и чтение большого xml файла довольно трудоемкая задача. Тем более если с одними и теми же данными работают несколько потоков. Так собственно и родилась идея написать много-файловое хранилище объектов в формате xml.
В этой статье я не буду рассматривать саму реализацию. Приведу лишь основные идеи и как использовать эту реализацию. Если вы хотите углубиться, то можете скачать посмотреть исходные коды.
Исходники доступны по ссылке: xdstore-1.3
Исходные тексты немного отличаются от приведенных в этой статье. В них были глубже проработаны исключительные ситуации, а именно, — для каждой операции, включая чтение, выбрасывается свое исключение. Также в последней версии реализована фрагментация.
Основная идея разработки
Главная идея заключается в том, чтобы объекты хранить не в одном файле, а в некотором множестве. При этом предоставить возможность настраивать политики хранения для каждого требуемого класса. Для класса можно установить одну из следующих политик:
- ParentObjectFile – объекты класса будут сохраняться в файле объекта владельца как дочерние элементы, эта политика применяется по умолчанию;
- SingleObjectFile – каждому объекту класса предоставляется отдельный файл, а в файле объекта владельца будет сохранена лишь ссылка на этот объект (в дальнейшем буду просто называть ее объектной ссылкой); все файлы каждого объекта будут сохраняться в отдельной папке внутри хранилища;
- ClassObjectsFile – все объекты этого класса будут храниться в отдельном файле, а в файлах объектов владельцев будут сохранены лишь объектные ссылки.
Под понятием объектной ссылки понимается объект указанного класса, у которого проставлено одно поле – идентификатор. В xml файле вместо полных данных этого объекта сохраняется лишь имя класса и идентификатор, чтобы в дальнейшем по этой ссылке можно было получить все данные. Загрузка таких объектов подобна поздней инициализации в hibernate.
Сохраняемые объекты должны быть реализованы как JavaBeans с методами get(is) и set для сохраняемых полей.
Одна интересная задача
Чтобы лучше понять ситуацию, в которую мы попадаем при попытке реализовать такое хранилище, необходимо правильно поставить задачу. В терминах БД звучит она следующим образом: в таблице базы данных имеются две строки, одновременно начинаются две транзакции, каждая из которых модифицирует обе строки, затем завершается коммитом первая транзакция и начинается третья, которая также модифицирует эти две строки.
Нас интересует поведение в подобной ситуации, т.е. что произойдет с данными в каждой из транзакций. В текущей реализации библиотеки поведение будет следующим:
1) Поскольку данные были модифицированы первой транзакцией, то вторая транзакция получит отказ на изменение данных в виде исключения. Объясняется это тем, что первая и вторая транзакции начались в одно время и скорее всего работали с одинаковыми копиями, и чтобы не потерять изменения первой транзакции второй необходимо отказать.
2) А вот данные третьей транзакции будут приняты, поскольку она началась после коммита первой транзакции и работает с обновленными данными.
Поскольку это довольно простая реализация, то при решении поставленной задачи не использовались блокировки записей чтобы избежать deadlock-ов и необходимости отката транзакций по таймауту. В этом случае выбрасывается исключение, по которому транзакция должна быть откачена.
Начало использования
Сама цель данной разработки получить простую и гибкую библиотеку, позволяющую сохранять объекты в формате xml. Поэтому получившийся интерфейс довольно прост, а требования предъявляемые к сохраняемым объектам сведены к минимуму. Основное требование для каждого сохраняемого объекта заключается в необходимости реализовать простой интерфейс IXmlDataStoreIdentifiable. Выглядит он следующим образом:
public interface IXmlDataStoreIdentifiable {
String getId();
void setId(String id);
}
Как вы можете видеть, необходимо лишь реализовать два метода работы с идентификатором объекта. Это необходимое условие обусловлено тем, что при некоторых политиках сохраняются лишь ссылки на объекты, по которым в дальнейшем может потребоваться восстановить (загрузить) все свойства. Ссылка в xml файле выглядит следующим образом:
<reference class="org.flib.xdstore.entities.XdGalaxy" id="cc74e3f2"/>
При загрузке этой ссылки будет создан объект указанного класса и у него проставлено свойство идентификатора. Остальные поля будут проинициализированы по умолчанию, т. е. они не будут загружены.
Рассмотрим теперь простой пример настройки хранилища для хранения объектов следующих классов: XdUniverse и XdGalaxy. Для начала определим их классы.
package org.flib.xdstore.entities;
import java.util.Collection;
import org.flib.xdstore.IXmlDataStoreIdentifiable;
public class XdUniverse implements IXmlDataStoreIdentifiable {
private String id;
private Collection<XdGalaxy> galaxies;
@Override
public String getId() {
return id;
}
@Override
public void setId(final String id) {
this.id = id;
}
public Collection<XdGalaxy> getGalaxies() {
return galaxies;
}
public void setGalaxies(Collection<XdGalaxy> galaxies) {
this.galaxies = galaxies;
}
public void addGalaxy(XdGalaxy galaxy) {
galaxies.add(galaxy);
}
public XdGalaxy removeGalaxy() {
final Iterator<XdGalaxy> it = galaxies.iterator();
XdGalaxy galaxy = null;
if(it.hasNext()) {
galaxy = it.next();
it.remove();
}
return galaxy;
}
}
И простенький класс XdGalaxy.
package org.flib.xdstore.entities;
import org.flib.xdstore.IXmlDataStoreIdentifiable;
public class XdGalaxy implements IXmlDataStoreIdentifiable {
private String id;
@Override
public String getId() {
return id;
}
@Override
public void setId(String id) {
this.id = id;
}
}
Теперь можно рассмотреть настройку хранилища для указанных сущностей.
final XmlDataStore store = new XmlDataStore("./teststore");
store.setStorePolicy(XdUniverse.class, XmlDataStorePolicy.ClassObjectsFile);
store.setStorePolicy(XdGalaxy.class, XmlDataStorePolicy.ClassObjectsFile);
Сейчас мы выбрали настройки, что все объекты каждого из классов будут храниться в своем файле, т.е. для каждого класса один файл. Можно использовать другие настройки и, например, не указывать политику для класса XdGalaxy, — тогда его объекты будут сохраняться вместе с объектами класса XdUniverse.
В результате для наших настроек после записи объектов мы получим два файла: XdUniverse.xml и XdGalaxy.xml.
<?xml version="1.0" encoding="UTF-8"?>
<objects>
<object isNull="false" class="org.flib.xdstore.entities.XdUniverse" id="002df141">
<collection name="Galaxies" class="java.util.ArrayList">
<reference class="org.flib.xdstore.entities.XdGalaxy" id="cc74e3f2"/>
<reference class="org.flib.xdstore.entities.XdGalaxy" id="ca519d20"/>
</collection>
<object name="Id" isNull="false" class="java.lang.String" value="002df141"/>
</object>
</objects>
Как видно из примера в этом файле хранятся ссылки на объекты из второго файла XdGalaxy.xml, приведенного ниже.
<?xml version="1.0" encoding="UTF-8"?>
<objects>
<object isNull="false" class="org.flib.xdstore.entities.XdGalaxy" id="cc74e3f2">
<object name="Id" isNull="false" class="java.lang.String" value="cc74e3f2"/>
</object>
<object isNull="false" class="org.flib.xdstore.entities.XdGalaxy" id="ca519d20">
<object name="Id" isNull="false" class="java.lang.String" value="ca519d20"/>
</object>
</objects>
Таким образом мы получили двух файловое хранилище для наших объектов. Если нам не требуются объекты класса XdGalaxy, то мы можем загрузить лишь объекты класса XdUniverse и работать с ними. Если же нам потребуются объекты класса XdGalaxy, то нам достаточно загрузить их по уже загруженным ссылкам.
В случае, если мы поставим политику хранения объектов SingleObjectFile, в корневом каталоге хранилища будет создана папка, в которую и будут сохраняться файлы объектов.
Сохранение и загрузка объектов
Рассмотрим интерфейс класса XmlDataStore, касающийся операций сохранения объектов. Он довольно прост и позволяет нам сохранять объекты без указания политик, поскольку они уже проставлены при инициализации хранилища.
public class XmlDataStore {
public XmlDataStoreTransaction beginTransaction();
public void commitTransaction(final XmlDataStoreTransaction transaction);
public void rollbackTransaction(final XmlDataStoreTransaction transaction);
public <T extends IXmlDataStoreIdentifiable> boolean saveRoot(final T root) throws XmlDataStoreException
public <T extends IXmlDataStoreIdentifiable> boolean saveObject(final T object) throws XmlDataStoreException
public <T extends IXmlDataStoreIdentifiable> boolean saveObjects(final Collection<T> objects)
throws XmlDataStoreException
}
Хранилище разрабатывалось для многопоточного использования и в ходе работы может быть задействовано несколько ресурсных объектов, поэтому оно использует механизм транзакций и предоставляет соответствующие методы. Принятие и откат транзакции могут быть вызваны также через методы объекта самой транзакции.
Сохранение корневых объектов и дочерних объектов немного отличаются, поэтому методы работы над корневыми объектами выделены в отдельную группу. Отличие заключается в том, что при политике SingleObjectFile для каждого корневого объекта будет выделен отдельный файл и в дополнение для них всех создан дополнительный файл, в котором будут храниться ссылки. Это позволяет разом загрузить все корневые объекты.
Теперь рассмотрим операцию сохранения.
final XmlDataStore store = initStore("./teststore");
final XdUniverse universe = generateUniverse();
final XmlDataStoreTransaction tx = store.beginTransaction();
try {
store.saveRoot(universe);
store.saveObjects(universe.getGalaxies());
tx.commit();
} catch (XmlDataStoreException e) {
tx.rollback();
}
Из примера видно, что сохранить объекты довольно просто. Отметим лишь тот момент, что поскольку объекты класса XdGalaxy сохраняются в отдельном файле, нам необходимо явно выполнить операцию их сохранения. Их можно также сохранять по отдельности используя другой метод, описанный выше. Сама запись объектов в файл происходит при выполнении операции принятия транзакции и до тех пор пока она не вызвана все операции производятся с кэшем.
Рассмотрим теперь часть интерфейса, относящуюся к загрузке объектов из хранилища.
public class XmlDataStore {
public <T extends IXmlDataStoreIdentifiable> Map<String, T> loadRoots(final Class<T> cl)
throws XmlDataStoreException
public <T extends IXmlDataStoreIdentifiable> T loadRoot(final Class<T> cl, final String id)
throws XmlDataStoreException
public <T extends IXmlDataStoreIdentifiable> boolean loadObject(final T reference) throws XmlDataStoreException
public <T extends IXmlDataStoreIdentifiable> T loadObject(Class<T> cl, final String id)
throws XmlDataStoreException
public <T extends IXmlDataStoreIdentifiable> boolean loadObjects(final Collection<T> references)
throws XmlDataStoreException
}
Как видно, хранилище позволяет нам загрузить разом все корни указанного класса или же запросить один корень указанного класса по идентификатору. Также можно загрузить объекты любого класса по ссылке или идентификатору. В нашем случае загрузка всех сохраненных данных будет выглядеть следующим образом.
final XmlDataStore store = initStore("./teststore");
final XmlDataStoreTransaction tx = store.beginTransaction();
try {
final Map<String, XdUniverse> roots = store.loadRoots(XdUniverse.class);
for (final XdUniverse root : roots.values()) {
final Collection<XdGalaxy> galaxies = root.getGalaxies();
store.loadObjects(galaxies);
}
tx.commit();
} catch(XmlDataStoreException e) {
tx.rollback();
}
Из примера видно, что сначала загружаются все корни, а затем для каждого корня по объектным ссылкам загружаются все дочерние объекты.
Обновление и удаление объектов
Методы обновления (изменения) и удаления объектов представлены ниже.
public class XmlDataStore {
public <T extends IXmlDataStoreIdentifiable> boolean updateRoot(final T root) throws XmlDataStoreException
public <T extends IXmlDataStoreIdentifiable> boolean deleteRoot(final T root) throws XmlDataStoreException
public <T extends IXmlDataStoreIdentifiable> boolean deleteRoot(final Class<T> cl, final String id)
throws XmlDataStoreException
public <T extends IXmlDataStoreIdentifiable> boolean updateObject(final T object) throws XmlDataStoreException
public <T extends IXmlDataStoreIdentifiable> boolean deleteObject(final T reference) throws XmlDataStoreException
public <T extends IXmlDataStoreIdentifiable> boolean deleteObjects(final Collection<T> references)
throws XmlDataStoreException
}
Следует отметить, что все зависимые объекты, которые хранятся в отдельных от владельца файлах, должны быть явно обновлены или удалены. Например, в нашем случае при удалении объекта класса XdGalaxy из объекта XdUniverse необходимо обновить объект XdUniverse и дополнительно явно удалить XdGalaxy.
final XmlDataStore store = initStore("./teststore");
final XmlDataStoreTransaction tx = store.beginTransaction();
try {
final Map<String, XdUniverse> roots = store.loadRoots(XdUniverse.class);
for (final XdUniverse root : roots.values()) {
final Collection<XdGalaxy> galaxies = root.getGalaxies();
store.loadObjects(galaxies);
}
if(roots.size() > 0) {
final XdUniverse universe = roots.values().iterator().next();
final XdGalaxy galaxy = universe.removeGalaxy();
if(galaxy != null) {
store.updateRoot(universe);
store.deleteObject(galaxy);
}
}
tx.commit();
} catch(XmlDataStoreException e) {
tx.rollback();
}
В случае добавления объекта код выглядит следующим образом.
final XmlDataStore store = initStore("./teststore");
final XmlDataStoreTransaction tx = store.beginTransaction();
try {
final Map<String, XdUniverse> roots = store.loadRoots(XdUniverse.class);
for (final XdUniverse root : roots.values()) {
final Collection<XdGalaxy> galaxies = root.getGalaxies();
store.loadObjects(galaxies);
}
if(roots.size() > 0) {
final XdUniverse universe = roots.values().iterator().next();
final XdGalaxy galaxy = initGalaxy(); // initialization XdGalaxy
universe.addGalaxy(galaxy);
store.updateRoot(universe);
store.saveObject(galaxy);
}
tx.commit();
} catch(XmlDataStoreException e) {
tx.rollback();
}
Если же политика сохранения ParentObjectFile, то для дочерних объектов нет необходимости явно выполнять операции удаления и сохранения, поскольку после обновления объекта владельца необходимая операция будет выполнена автоматически.
Полная очистка нашего хранилища будет выглядеть следующим образом:
final XmlDataStore store = initStore(storedir);
final XmlDataStoreTransaction tx = store.beginTransaction();
try {
final Map<String, XdUniverse> roots = store.loadRoots(XdUniverse.class);
for (final XdUniverse root : roots.values()) {
final Collection<XdGalaxy> galaxies = root.getGalaxies();
store.deleteObjects(galaxies);
store.deleteRoot(root);
}
tx.commit();
} catch(XmlDataStoreException e) {
tx.rollback();
}
Из примера видно, что нам даже не потребовалось загружать объекты класса XdGalaxy перед удалением. Мы просто передали коллекцию объектных ссылок. Это возможно поскольку объектная ссылка хранит идентификатор объекта.
Немного о реализации
Для повышения производительности работы хранилища используется неотключаемое кэширование. Т.е. при работе с любым ресурсным объектом (файлом) все хранимые в нем объекты загружаются и кэшируются при первой транзакции. Все остальные транзакции работают с уже кэшированными данными. Данные кэша сбрасываются, когда завершается последняя транзакция, которая работает с этим ресурсным объектом. Все изменения регистрируются в кэше и не сбрасываются на диск до тех пор, пока не происходит принятие транзакции.
Поскольку в ходе выполнения транзакции может быть затронуто неопределенное количество ресурсных объектов, то операция принятия изменений транзакции выполняется над всеми поочередно. Если при этом процессе происходит какая-либо ошибка, то целостность хранилища данных нарушается и выбрасывается исключение типа XmlDataStoreRuntimeException. В текущей реализации восстановление целостного состояния хранилища не реализовано. Это один из существенных недостатков текущей версии.
Планы по развитию
В текущей реализации при большом количестве объектов определенного класса и политике хранения ClassObjectsFile, трудоемкость операций чтения и записи растет прямо пропорционально росту количества объектов. Для того чтобы повысить производительность хранилища планируется реализовать фрагментацию и построение файла индекса. Фрагментация подразумевает под собой разбиение одного файла на фрагменты, содержащие ограниченное количество объектов, а индекс в данном случае будет содержать ссылки с указанием файла фрагмента, в котором сохранен объект.
Также в планы входит реализация восстановления целостного состояния хранилища после сбоя при принятии изменений транзакции.
Возможно, что в новой реализации хранилища появятся триггеры, которые будут вызываться при изменении состояния хранимых объектов. Т.е. при добавлении, изменении или удалении объектов.
Автор: Бесчастный Евгений
Автор: EwgenyB