Введение
В программировании часто перед нами встают задачи, которые мы можем решить несколькими путями: найти и использовать уже готовые решения, или же решать задачу самостоятельно. Хоть и написано множество спецификаций и их реализаций, они не всегда дают нам то, что требуется в конкретном случае. Вот и мне в очередной раз пришлось столкнуться с подобной ситуацией.
Задача состояла в хранении объектов в файле в формате 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, трудоемкость операций чтения и записи растет прямо пропорционально росту количества объектов. Для того чтобы повысить производительность хранилища планируется реализовать фрагментацию и построение файла индекса. Фрагментация подразумевает под собой разбиение одного файла на фрагменты, содержащие ограниченное количество объектов, а индекс в данном случае будет содержать ссылки с указанием файла фрагмента, в котором сохранен объект.
Также в планы входит реализация восстановления целостного состояния хранилища после сбоя при принятии изменений транзакции.
Возможно, что в новой реализации хранилища появятся триггеры, которые будут вызываться при изменении состояния хранимых объектов. Т.е. при добавлении, изменении или удалении объектов.
Автор: Бесчастный Евгений
ссылка на оригинал статьи http://habrahabr.ru/post/185890/
Добавить комментарий