Java development 2.0: Вторая волна разработки Java-приложений: Шардинг средствами Hibernate Shards

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

Шардинг (разделение одной базы данных на несколько баз-шардов по какому-то признаку) – не универсальное решение, но тем не менее это один из подходов, позволяющих реляционным системам работать с большими объемами данных. Для некоторых проектов шардинг - единственное решение, позволяющее сохранить высокую надежность СУРБД без потери производительности системы и снижения возможностей масштабирования данных. В этой серии статей, входящей в группу публикаций о второй волне разработки Java-приложений, вы узнаете, в каких случаях имеет смысл использовать шардинг, а в каких – нет, и сможете изучить возможности шардинга на примере простого приложения, рассчитанного на обработку терабайтов данных.

Эндрю Гловер, президент компании, Stelligent Incorporated

Эндрю ГловерЭндрю Гловер является президентом компании Stelligent Incorporated , которая помогает другим фирмам решать проблемы качества программного обеспечения. Для этого используются эффективные стратегии тестирования и технологии непрерывной интеграции, которые позволяют коллективам разработчиков постоянно контролировать качество кода, начиная с ранних стадий разработки. Просмотрите блог Энди , там можно найти список его публикаций.



27.02.2012

Попытки хранить терабайты информации в общих таблицах реляционных баз данных обычно приводят к значительным потерям производительности. Индексирование данных в таких случаях не только очевидным образом влечет за собой значительные затраты на чтение данных, но и порождает большие затраты на запись. Хотя хранилища данных типа NoSQL и рассчитаны на эффективную работу с большими массивами данных (вспомните о Google Bigtable), NoSQL гарантированно не поддерживает реляционные подходы к обработке и хранению данных. Таким образом, для разработчиков, предпочитающих ACID-ность и жестко фиксированные структуры реляционных баз данных, или для проектов, реализация которых требует этих качеств, шардинг является заманчивой альтернативой.

Шардинг, одна из разновидностей разбиения баз данных на разделы, не принадлежит к числу технологий, реализованных на уровне самих баз данных; эта техника работает на уровне приложений. Среди существующих в настоящее время различных вариантов шардинга наиболее популярным в мире Java™-технологий является Hibernate Shards. Этот оригинальный проект позволяет вам работать с разделенными базами данных практически без потерь (что значит «практически без потерь», я объясню чуть позже), используя объекты POJO, отображенные на логические базы данных. При использовании Hibernate Shards вам не нужно беспокоиться об отображении POJO на шарды: отображение делается так же, как при работе с обычной реляционной базой данных с помощью Hibernate. Всю организацию низкоуровневого шардинга Hibernate Shards берет на себя.

В предыдущих статьях этой серии для демонстрации различных технологий хранения данных я использовал простой домен, представляющий собой аналогию соревнований и участвующих в них бегунов. В статье этого месяца я вновь воспользуюсь этим примером для рассмотрения стратегии шардинга и реализации ее с помощью проекта Hibernate Shards. Обращаю ваше внимание на то, что основные проблемы шардинга никак не связаны с его реализацией в Hibernate. На самом деле код для использования Hibernate Shards – одна из самых простых составляющих нашего примера. Главная задача при использовании шардинга состоит в том, чтобы правильно выбрать, что и как следует разделять.

Об этой серии

Специфика современных разработок на языке Java коренным образом отличается от методов, характерных для начального этапа развития Java-технологий. Использование развитых открытых инфраструктур и предоставляемых в аренду надежных сред развертывания позволяет быстро и с минимальными затратами собирать, тестировать, запускать и поддерживать Java-приложения. В предлагаемой серии статей Эндрю Гловер рассматривает ряд технологий и инструментов, лежащих в основе новой парадигмы Java-разработки.

Шардинг: краткий обзор

Разбиение (partitioning) баз данных - это реляционный по своей сути процесс распределения записей таблицы на группы по какому-либо логическому признаку. Так, например, вы можете разбить гигантскую таблицу с именем foo по временному признаку таким образом, что все данные, собранные до августа 2010 года включительно попадут в раздел А, а все данные, полученные позже, - в раздел B. Разбиение позволяет ускорить операции чтения и записи, поскольку они будут выполняться на меньших объемах данных, хранящихся в отдельных разделах.

Однако возможности разбиения на разделы не всегда доступны (например, разбиение не поддерживается в MySQL версии ниже 5.1 ), и, кроме того, цена такого разбиения для коммерческих систем может оказаться непозволительно высокой. Более того, большинство реализаций разбиения рассчитаны на хранение разделенных данных на одной и той же физической машине. Таким образом, даже после разбиения базы данных проблема нехватки ресурсов по-прежнему остается актуальной. Разбиение не решает проблему надежности, вернее, отсутствия таковой в вашей аппаратной среде. В итоге ведущие специалисты пришли к выводу о необходимости поиска новых возможностей масштабирования.

Шардинг по своей сути есть разбиение непосредственно базы данных: вместо того чтобы разделять записи в таблице по какому-либо логическому признаку, разделяется сама база данных (как правило, по нескольким физическим машинам). Таким образом, шардинг разделяет на отдельные массивы данных не таблицы, а сами базы данных.

Каноническим примером шардинга является разбиение глобальной базы данных заказчика по регионам: шард А содержит данные клиентов в США, шард В – данные клиентов в Азии, шард С – в Европе и так далее. Сами шарды располагаются на разных машинах, причем каждый шард содержит полную информацию по клиенту (например, историю заказов или предпочитаемые товары).

Преимущество шардинга как подхода к разбиению данных состоит в том, что он позволяет сократить объемы хранимой информации: на каждом шарде таблицы содержат меньше данных, чем в глобальной базе, в результате ускоряется процесс чтения и записи, а, следовательно, увеличивается производительность базы данных. Кроме того, шардинг значительно увеличивает надежность системы: в случае внезапного «падения» одного из шардов остальные продолжат функционировать без потери данных. Поскольку разделение осуществляется на уровне приложения, шардинг можно использовать и для тех СУБД, которые не поддерживают стандартное разбиение на разделы. Стоимость реализации шардинга, как правило, ниже затрат на разбиение на разделы.


Шардинг и стратегия

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

Первичные ключи
В результате шардинга вы получаете фактически несколько баз данных, каждая из которых работает в автономном режиме без прямой взаимосвязи с остальными базами данных. В результате, если вы полагаетесь на генерацию рядов данных средствами СУБД (например, в случае автоматической генерации значений первичного ключа), то рано или поздно в глобальной базе данных появятся дублирующие значения. Безусловно, существуют возможности координации значений ряда в распределенной базе данных, но подобная координация значительно повысит сложность системы. Самый безопасный способ избежать дубликатов – это генерировать уникальные ключи средствами вашего приложения (которому все равно придется выполнять дополнительные функции по управлению разделенными базами данных).

Кросс-шардовые запросы
Большинство реализаций шардинга (в том числе и Hibernate Shards) не поддерживает кросс-шардинговые запросы. Это означает, что для операций с наборами данных из разных шардов вам потребуются дополнительные действия (интересно, что Amazon SimpleDB тоже не поддерживает кросс-доменные запросы). Так, например, если данные по клиентам в США хранятся в шарде с именем Shard 1, то все связанные с ними данные тоже должны храниться в этом шарде. Если вы поместите часть данных в Shard 2, это значительно усложнит процесс обработки, и, скорее всего, приведет к падению производительности системы. Проблема кросс-шардовых запросов косвенно связана с проблемой уникальности ключей: если вам все-таки потребуется такой запрос, следует заранее позаботиться о таком методе генерации ключей, который позволит вам избежать появления дубликатов.

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

Избегайте преждевременного шардинга

Шардинг относится к категории «последних средств». Так же как и преждевременная оптимизация, преждевременный шардинг, выполненный в расчете на предположительный рост объема данных в будущем, может привести к катастрофическим последствиям. Успешные реализации шардинга базируются на измерении роста объема данных во времени с максимально точной экстраполяцией полученной статистики на будущее. После шардинга обратное перемещение данных становится экстраординарно сложной задачей.

Пример стратегии

Поскольку шардинг ограничивает вас рамками линейной модели данных (т.е. вы не можете объединять данные из разных шардов без дополнительных операций), для начала вам следует четко определить, какую логическую единицу вы будете использовать в качестве основы для разделения данных по шардам. Такое решение может быть принято исходя из первичного узла домена. В случае систем электронной коммерции первичным узлом, как правило, является клиент или заказ. Если вы выбираете в качестве основы разделения клиентов, то все данные, относящиеся к конкретному клиенту, должны храниться на соответствующем шарде – остается только выбрать, на каком именно.

Если логической единицей разбиения является клиент, то в качестве признака разбиения вы можете использовать его географическое положение (Европа, Азия, Америка) или какое-либо другое свойство – выбор за вами. Однако при выборе стратегии шардинга постарайтесь определить признак разделения таким образом, чтобы данные между шардами распределялись равномерно. Основная идея шардинга состоит в том, чтобы разделить большой набор данных на меньшие множества, так что если среди клиентов конкретного домена электронной коммерции преобладают пользователи из Европы, а американских клиентов немного, то в этом случае шардинг по географическому положению клиентов большого смысла не имеет.


Шардинг на примере приложения для хранения данных о соревнованиях

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

Обратите внимание: принимая подобные решения, я тем самым, соглашаюсь на определенный компромисс: что будет, если один и тот же бегун принимает участие в нескольких соревнованиях, которые хранятся в разных шардах? Hibernate Shards, как и большинство других реализаций шардинга, не поддерживает кросс-шардовых запросов. Что ж, мне остается только смириться с этим незначительным неудобством и хранить одних и тех же бегунов в различных шардах — это означает, что для каждого спортсмена будет создана новая запись в том шарде, куда попадает длина соответствующего забега.

Для упрощения я рассмотрю случай с двумя шардами: в первом будут храниться забеги с длиной дистанции менее 10 миль, а во втором – забеги с длиной дистанции более 10 миль.


Реализация Hibernate Shards

Реализация Hibernate Shards без особых проблем работает со всеми существующими проектами Hibernate. Единственное неудобство состоит в том, что Hibernate Shards требует от вас некоторой дополнительной информации и определенных действий, а именно шардам требуется задать стратегию доступа, стратегию отбора и стратегию-определение. Эти интерфейсы вы должны задавать самостоятельно, однако в некоторых случая вы можете воспользоваться готовыми стратегиями по умолчанию. В следующих секциях мы подробно рассмотрим каждую из стратегий.

ShardAccessStrategy

При выполнении запросов Hibernate Shards требуется механизм, определяющий, какой из шардов должен быть опрошен в первую очередь, какой – во вторую, и так далее. Hibernate Shards при этом совсем не обязательно знать, какие именно данные ищет каждый конкретный запрос (это задача Hibernate Core и соответствующей базы данных), тем не менее эта реализация шардинга понимает, что запрос, возможно, должен быть выполнен на нескольких шардах. Для выполнения запросов Hibernate Shards включает в себя два готовых логических блока: один выполняет запрос последовательно на нескольких шардах (опрашивая по очереди каждый шард), а другой реализует стратегию параллельного доступа, которая представляет собой многопоточную модель для одновременного опроса всех шардов.

Опять-таки для упрощения рассматриваемого примера я воспользуюсь последовательной стратегией, которая, вполне ожидаемо, называется SequentialShardAccessStrategy. Чуть позже мы настроим конфигурацию этой стратегии.

ShardSelectionStrategy

При создании нового объекта (т.е. при добавлении нового элемента Race или Runner в Hibernate) Hibernate Shards необходимо знать, в каком шарде надо сохранить данные объекта. Соответственно вам нужно задать этот интерфейс и добавить код, определяющий логику шардинга. Если вы хотите воспользоваться готовой реализацией по умолчанию, вы можете остановиться на стратегии RoundRobinShardSelectionStrategy, которая записывает данные в шарды по очереди, выбирая каждый раз следующий шард.

В приложении для хранения данных о соревнованиях нам нужен механизм, определяющий шард объекта исходя из длины дистанции. Поэтому мне придется реализовать свою собственную стратегию выбора ShardSelectionStrategy и определить простую логику, определяющую шард исходя из свойства длины дистанции объекта Race в методе selectShardIdForNewObject (сам объект Race мы рассмотрим чуть позже).

Во время работы приложения при вызове метода сохранения данных доменных объектов в глубинах ядра Hibernate будет использоваться определенная мной логика шардинга.

Листинг 1. Простая стратегия выбора нужного шарда
import org.hibernate.shards.ShardId;
import org.hibernate.shards.strategy.selection.ShardSelectionStrategy;

public class RacerShardSelectionStrategy implements ShardSelectionStrategy {

 public ShardId selectShardIdForNewObject(Object obj) {
  if (obj instanceof Race) {
   Race rce = (Race) obj;
   return this.determineShardId(rce.getDistance());
  } else if (obj instanceof Runner) {
   Runner runnr = (Runner) obj;
   if (runnr.getRaces().isEmpty()) {
    throw new IllegalArgumentException("runners must have at least one race");
   } else {
    double dist = 0.0;
    for (Race rce : runnr.getRaces()) {
     dist = rce.getDistance();
     break;
    }
    return this.determineShardId(dist);
   }
  } else {
   throw new IllegalArgumentException("a non-shardable object is being created"); 
 }
}

 private ShardId determineShardId(double distance){
  if (distance > 10.0) {
   return new ShardId(1);
  } else {
   return new ShardId(0);
  }
 }
}

Как показано в листинге 1, при сохранении объекта класса Race для этого объекта сначала определяется длина дистанции, а потом в зависимости от длины дистанции выбирается соответствующий шард. В нашем случае их всего два: Shard 0 и Shard 1. В Shard 1 попадают соревнования с длиной дистанции больше 10 миль, а в Shard 0 – все остальные.

При сохранении объектов класса Runner или других классов (отличных от Race) ситуация требует более сложного анализа. В наш пример я включил проверку трех условий:

  • Объект класса Runner не может быть сохранен без привязки к соответствующему объекту класса Race.
  • Если объект Runner связан с несколькими объектами Race, то такой объект Runner будет сохранен в шарде, соответствующем первому из связанных с ним объектов Race (это правило несет в себе несколько негативных последствий, с которыми мы столкнемся в будущем)
  • При попытке сохранения доменных объектов других классов будет выдаваться ошибка.

Теперь можно перевести дух, так как самая тяжелая часть работы уже выполнена. Определенная нами логика может оказаться недостаточно гибкой по мере роста приложения для хранения данных о соревнованиях, но для примера по изучению свойств шардинга она вполне подходит.

ShardResolutionStrategy

Для поиска объекта по заданному ключу Hibernate Shards требуется механизм для определения, в какой очередности вести поиск по шардам. Этот механизм определяется с помощью интерфейса SharedResolutionStrategy.

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

При использовании ShardedUUIDGenerator в качестве средства генерации первичных ключей (а именно это я и собираюсь сделать в нашем примере) вы можете воспользоваться готовой реализацией ShardResolutionStrategy, которая называется AllShardsShardResolutionStrategy и определяет порядок поиска объекта исходя из его идентификатора.

После конфигурирования трех основных интерфейсов, необходимых для корректной работы Hibernate Shards, мы можем перейти к следующему этапу внедрения шардинга в наше приложение, а именно к запуску SessionFactory.


Конфигурирование Hibernate Shards

SessionFactory – это один из основных объектов интерфейса Hibernate. Все основные функции Hibernate используют SessionFactory, так как именно этот маленький объект отвечает за конфигурацию приложения Hibernate, в частности, определяет загрузку файлов отображения и настроек. Объект SessionFactory понадобится вам и в том случае, если вы используете аннотации или почтенные файлы Hibernate .hbm для указания объектов, которые требуют постоянного хранения, и места, где эти объекты должны храниться.

Таким образом, для работы с Hibernate Shards вам потребуется объект SessionFactory расширенного типа, который используется для конфигурирования нескольких баз данных. Этот расширенный тип носит соответствующее название ShardedSessionFactory и, естественно, относится к типу SessionFactory. При создании экземпляра ShardedSessionFactory вы должны указать три предварительно заданных типа реализации интерфейсов шардинга (ShardAccessStrategy, ShardSelectionStrategy и ShardResolutionStrategy). Кроме того, вам нужно будет указать все файлы отображения, необходимые для ваших объектов POJO (в случае использования аннотированных объектов Hibernate POJO требования будут несколько другими). Наконец, вам потребуется экземпляр ShardedSessionFactory с несколькими конфигурационными файлами, по одному на каждый используемый шард.

Создание программной конфигурации Hibernate

Я определил тип ShardedSessionFactoryBuilder, который содержит единственный метод createSessionFactory для создания экземпляра SessionFactory нужной мне конфигурации. Впоследствии я свяжу все воедино с помощью Spring (кто в наши дни не использует IoC-контайнеры?). В листинге 2 показана основная функция ShardedSessionFactoryBuilder - а именно создание конфигурации Hibernate:

Листинг 2. Создание программной конфигурации Hibernate
private Configuration getPrototypeConfig(String hibernateFile, List<String> 
  resourceFiles) {
 Configuration config = new Configuration().configure(hibernateFile);
 for (String res : resourceFiles) {
  configs.addResource(res);
 }
 return config;
}

В листинге 2 показано, как на основе конфигурационного файла Hibernate создается простой экземпляр класса Configuration. Этот файл содержит информацию об используемых базах данных, имя пользователя, пароль и т.д., а также данные о необходимых файлах ресурсов, например, файлах hbm для объектов POJO. При использовании шардинга вам потребуется хранить конфигурационные данные для нескольких баз данных. Hibernate Shards позволяет хранить всю необходимую информацию в одном файле hibernate.cfg.xml (т.е. вам потребуется по одному такому файлу для каждого шарда – см. листинг 4).

Далее в листинге 3 я соберу все конфигурационные параметры шарда в список List:

Листинг 3. Список конфигурационных параметров шарда
List<ShardConfiguration> shardConfigs = new ArrayList<ShardConfiguration>();
for (String hibconfig : this.hibernateConfigurations) {
 shardConfigs.add(buildShardConfig(hibconfig));
}

Конфигурация Spring

В листинге 3 ссылка hibernateConfigurations указывает на список List строк Strings, каждая из которых определяет конфигурационный файл Hibernate. Этот список автоматически будет подгружен в Spring. Соответствующий фрагмент конфигурационного файла Spring приведен в листинге 4:

Листинг 4. Фрагмент конфигурационного файла Spring
<bean id="shardedSessionFactoryBuilder" 
  class="org.disco.racer.shardsupport.ShardedSessionFactoryBuilder">
    <property name="resourceConfigurations">
        <list>
            <value>racer.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateConfigurations">
        <list>
            <value>shard0.hibernate.cfg.xml</value>
            <value>shard1.hibernate.cfg.xml</value>
        </list>
    </property>
</bean>

Как показано в листинге 4, конфигурация привязана к экземпляру ShardedSessionFactoryBuilder, включающему в себя один файл отображения объектов POJO и два файла конфигурации шардов. Листинг 5 демонстрирует фрагмент файла POJO:

Листинг 5. Отображение POJO объектов Race
<class name="org.disco.racer.domain.Race" table="race"dynamic-update="true"
   dynamic-insert="true">

 <id name="id" column="RACE_ID" unsaved-value="-1">
  <generator class="org.hibernate.shards.id.ShardedUUIDGenerator"/>
 </id>

 <set name="participants" cascade="save-update" inverse="false" table="race_participants"
    lazy="false">
  <key column="race_id"/>
  <many-to-many column="runner_id" class="org.disco.racer.domain.Runner"/>
 </set>

 <set name="results" inverse="true" table="race_results" lazy="false">
  <key column="race_id"/>
  <one-to-many class="org.disco.racer.domain.Result"/>
 </set>

 <property name="name" column="NAME" type="string"/>
 <property name="distance" column="DISTANCE" type="double"/>
 <property name="date" column="DATE" type="date"/>
 <property name="description" column="DESCRIPTION" type="string"/>
</class>

Обратите внимание: единственный уникальный аспект в файле отображения POJO (см. листинг 5) — это класс генератора уникальных ключей ShardedUUIDGenerator, который, как вы помните, добавляет данные идентификатора шарда к UUID соответствующего объекта. Это единственный момент, связанный с шардингом, который присутствует в моем файле отображения POJO объектов.

Конфигурационные файлы шардов

В листинге 6 приведен конфигурационный файл шарда Shard 0. Конфигурация шарда Shard 1 совпадает с конфигурацией Shard 0 во всем, за исключением ID и параметров подключения.

Листинг 6. Пример конфигурационного файла шарда Hibernate Shards
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory name="HibernateSessionFactory0">
        <property name="dialect">org.hibernate.dialect.HSQLDialect</property>
        <property name="connection.driver_class">org.hsqldb.jdbcDriver</property>
        <property name="connection.url">
            jdbc:hsqldb:file:/.../db01/db01
        </property>
        <property name="connection.username">SA</property>
        <property name="connection.password"></property>
        <property name="hibernate.connection.shard_id">0</property>
        <property name="hibernate.shard.enable_cross_shard_relationship_checks">true
        </property>
    </session-factory>
</hibernate-configuration>

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

Наконец, ShardedSessionFactoryBuilder собирает воедино все свойства и настройки, создавая экземпляр ShardStrategyFactory и добавляя к нему три типа интерфейсов (включая RacerShardSelectionStrategy, приведенный в листинге 1) – см. листинг 7:

Листинг 7. Создание ShardStrategyFactory
private ShardStrategyFactory buildShardStrategyFactory() {
 ShardStrategyFactory shardStrategyFactory = new ShardStrategyFactory() {
  public ShardStrategy newShardStrategy(List<ShardId> shardIds) {
   ShardSelectionStrategy pss = new RacerShardSelectionStrategy();
   ShardResolutionStrategy prs = new AllShardsShardResolutionStrategy(shardIds);
   ShardAccessStrategy pas = new SequentialShardAccessStrategy();
   return new ShardStrategyImpl(pss, prs, pas);
  }
 };
 return shardStrategyFactory;
}

Наконец, воспользуемся волшебным методом createSessionFactory, который в нашем случае создаст экземпляр ShardedSessionFactory (см. листинг 8):

Листинг 8. Создание ShardedSessionFactory
public SessionFactory createSessionFactory() {
 Configuration prototypeConfig = this.getPrototypeConfig
  (this.hibernateConfigurations.get(0), this.resourceConfigurations);

 List<ShardConfiguration> shardConfigs = new ArrayList<ShardConfiguration>();
 for (String hibconfig : this.hibernateConfigurations) {
  shardConfigs.add(buildShardConfig(hibconfig));
 }

 ShardStrategyFactory shardStrategyFactory = buildShardStrategyFactory();
 ShardedConfiguration shardedConfig = new ShardedConfiguration(
  prototypeConfig, shardConfigs,shardStrategyFactory);
 return shardedConfig.buildShardedSessionFactory();
}

Привязка доменных объектов к Spring

Переведите дух, мы уже близки к финишу. Пока что мы создали класс для конфигурации ShardedSessionFactory, который на самом деле является реализацией типа Hibernate SessionFactory. ShardedSessionFactory выполнит все операции, связанные с реализацией шардинга. Используя стратегию выбора шардов, определенную нами в листинге 1, он будет сохранять и считывать данные из двух сконфигурированных нами шардов (конфигурация Shard 0 показана в (листинге 6, конфигурация Shard 1 практически идентична ей).

Все, что мне осталось сделать, - это привязать к Spring мои доменные объекты, которым, в силу использования Hibernate, для корректной работы требуется экземпляр SessionFactory. Для создания экземпляра SessionFactory я воспользуюсь классом ShardedSessionFactoryBuilder, как показано в листинге 9:

Листинг 9. Привязка POJO к Spring
<bean id="mySessionFactory"
 factory-bean="shardedSessionFactoryBuilder"
 factory-method="createSessionFactory">
</bean>

<bean id="race_dao" class="org.disco.racer.domain.RaceDAOImpl">
 <property name="sessionFactory">
  <ref bean="mySessionFactory"/>
 </property>
</bean>

Как показано в листинге 9, сначала я создаю в среде Spring «фабричный» bean-компонент и метод; таким образом, мой тип RaceDAOImpl обладает свойством sessionFactory типа sessionFactory. Соответственно ссылка на элемент mySessionFactory вызывает метод createSessionFactory компонента ShardedSessionFactoryBuilder, который мы определили в листинге 4. В результате создается нужный мне экземпляр SessionFactory.

Как только я обращаюсь к Spring (а я использую эту среду в качестве гигантской фабрики для получения объектов с предварительно заданной конфигурацией), чтобы получить новый экземпляр объекта Race, все указанные в конфигурации настройки вступают в действие. Тип RaceDAOImpl использует шаблоны Hibernate для хранения и извлечения данных. Мой тип Race включает в себя экземпляр RaceDAOImpl, которому он предоставляет выполнение всех взаимодействий с хранилищем данных. Весьма удобно, не правда ли?

Обратите внимание, мои DAO привязаны к Hibernate Shards через конфигурацию, а не посредством кода. Конфигурация (см. листинг 5) привязывает их к схеме генерации UUID с учетом шардинга, что, в свою очередь, позволит мне повторно использовать доменные объекты существующей реализации Hibernate при необходимости использования шардинга.


Шардинг: тест-драйв с easyb

Следующее, что мне нужно сделать, - это убедиться в том, что мой шардинг работает. У меня есть две базы данных, и в качестве признака разбиения данных я использую длину дистанции, так что если я создам забег на марафонскую дистанцию (длина которой определенно больше 10 миль), то соответствующий ему объект Race должен попасть в Shard 1. Забеги с меньшей длиной дистанции, например 5 км (3.1 мили), должны попасть в Shard 0. После создания каждого объекта Race я буду проверять, попала ли соответствующая запись в нужную базу данных.

В листинге 10 я создаю марафонский забег и проверяю, что соответствующая запись действительно хранится в Shard 1, а не в Shard 0. Для того, чтобы сделать этот процесс более интересным (и более простым), я воспользуюсь easyb – BDD-инфраструктурой, основанной на Groovy и поддерживающей проверки естественных языков. Инфраструктура easyb так же легко работает и с Java-кодом. Даже не имея никакого представления о Groovy или easyb, вы легко прочтете код, приведенный в листинге 10, и убедитесь, что все работает так, как должно (кстати, я принимал участие в создании easyb, и на сайте developerWorks есть мои статьи на эту тему):

Листинг 10. Фрагмент истории easyb для проверки правильности работы шардинга
scenario "races greater than 10.0 miles should be in shard 1 or db02", {
  given "a newly created race that is over 10.0 miles", {
    new Race("Leesburg Marathon", new Date(), 26.2,
            "Race the beautiful streets of Leesburg!").create()
  }
  then "everything should work fine w/respect to Hibernate", {
    rce = Race.findByName("Leesburg Marathon")
    rce.distance.shouldBe 26.2
  }
  and "the race should be stored in shard 1 or db02", {
    sql = Sql.newInstance(db02url, name, psswrd, driver)
    sql.eachRow("select race_id, distance, name from race where name=?", 
      ["Leesburg Marathon"]) { row ->
      row.distance.shouldBe 26.2
    }
    sql.close()
  }
  and "the race should NOT be stored in shard 0 or db01", {
    sql = Sql.newInstance(db01url, name, psswrd, driver)
    sql.eachRow("select race_id, distance, name from race where name=?", 
      ["Leesburg Marathon"]) { row ->
      fail "shard 0 contains a marathon!"
    }
    sql.close()
  }
}

Безусловно, на этом моя работа не заканчивается. Я должен создать забег на короткую дистанцию и убедиться, что он попадет в Shard 0, а не в Shard 1. Этот пример вы найдете в разделе Загрузка данной статьи.


Шардинг: за и против

Использование шардинга позволяет значительно ускорить процесс чтения и записи данных приложением, особенно в тех случаях, когда ваше приложение должно обрабатывать значительные объемы данных – порядка терабайтов - либо предназначено для области применения с неограниченным ростом данных, как Google или Facebook.

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

Своевременно и правильно используемый шардинг – ключ к эффективному масштабированию и высокой скорости обработки данных для СУРБД. Особенно эффективно использование шардинга в организациях, привязанных к реляционным инфраструктурам и не имеющих возможности постоянно обновлять аппаратное обеспечение для обеспечения нужного уровня масштабируемости хранилищ данных.


Загрузка

ОписаниеИмяРазмер
Примеры кода, используемые в статьеj-javadev2-11.zip15 KБ

Ресурсы

Научиться

Получить продукты и технологии

  • Hibernate Shards: реализация горизонтального разделения баз данных Hibernate Core, разработанная для инкапсуляции и упрощения схем данных
  • easyb: благодаря использованию четко специфицированного предметно-ориентированного языка, инфраструктура easyb позволяет создавать эффективную и вместе с тем удобную для чтения документацию.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=795905
ArticleTitle=Java development 2.0: Вторая волна разработки Java-приложений: Шардинг средствами Hibernate Shards
publish-date=02272012