Как настроить Hibernate для чтения / записи в разные источники данных? - PullRequest
13 голосов
/ 08 декабря 2010

Используя Spring и Hibernate, я хочу записать в одну главную базу данных MySQL и прочитать еще одного реплицированного ведомого устройства в облачном веб-приложении Java.

Я не могу найти решение, прозрачное для кода приложения. Я действительно не хочу менять свои DAO для управления различными SessionFactories, так как это выглядит очень грязно и сочетает код с конкретной серверной архитектурой.

Есть ли способ сообщить Hibernate об автоматической маршрутизации запросов CREATE / UPDATE к одному источнику данных и SELECT к другому? Я не хочу делать ни одного шардинга или чего-либо другого в зависимости от типа объекта - просто перенаправляйте разные типы запросов к разным источникам данных.

Ответы [ 5 ]

18 голосов
/ 16 октября 2015

Пример можно найти здесь: https://github.com/afedulov/routing-data-source.

enter image description here

Spring предоставляет вариант DataSource, называемый AbstractRoutingDatasource.Он может использоваться вместо стандартных реализаций DataSource и позволяет механизму определять, какой конкретный DataSource использовать для каждой операции во время выполнения.Все, что вам нужно сделать, это расширить его и обеспечить реализацию абстрактного determineCurrentLookupKey метода.Это место для реализации вашей пользовательской логики для определения конкретного источника данных.Возвращенный объект служит ключом поиска.Обычно это String или en Enum, используемый в качестве квалификатора в конфигурации Spring (подробности будут приведены ниже).

package website.fedulov.routing.RoutingDataSource

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder.getDbType();
    }
}

Вам может быть интересно, что это за объект DbContextHolder и как он узнает, какой идентификатор DataSource возвращать?Имейте в виду, что метод determineCurrentLookupKey будет вызываться всякий раз, когда TransactionsManager запрашивает соединение.Важно помнить, что каждая транзакция «связана» с отдельным потоком.Точнее, TransactionsManager связывает Connection с текущим потоком.Поэтому, чтобы отправлять разные транзакции в разные целевые источники данных, мы должны убедиться, что каждый поток может надежно определить, какой источник данных предназначен для его использования.Это делает естественным использование переменных ThreadLocal для привязки конкретного источника данных к потоку и, следовательно, к транзакции.Вот как это делается:

public enum DbType {
   MASTER,
   REPLICA1,
}

public class DbContextHolder {

   private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<DbType>();

   public static void setDbType(DbType dbType) {
       if(dbType == null){
           throw new NullPointerException();
       }
      contextHolder.set(dbType);
   }

   public static DbType getDbType() {
      return (DbType) contextHolder.get();
   }

   public static void clearDbType() {
      contextHolder.remove();
   }
}

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

  ....
<bean id="dataSource" class="website.fedulov.routing.RoutingDataSource">
 <property name="targetDataSources">
   <map key-type="com.sabienzia.routing.DbType">
     <entry key="MASTER" value-ref="dataSourceMaster"/>
     <entry key="REPLICA1" value-ref="dataSourceReplica"/>
   </map>
 </property>
 <property name="defaultTargetDataSource" ref="dataSourceMaster"/>
</bean>

<bean id="dataSourceMaster" class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="${db.master.url}"/>
  <property name="username" value="${db.username}"/>
  <property name="password" value="${db.password}"/>
</bean>
<bean id="dataSourceReplica" class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="${db.replica.url}"/>
  <property name="username" value="${db.username}"/>
  <property name="password" value="${db.password}"/>
</bean>

В этот момент вы можете обнаружить, что делаете что-то вроде этого:

@Service
public class BookService {

  private final BookRepository bookRepository;
  private final Mapper               mapper;

  @Inject
  public BookService(BookRepository bookRepository, Mapper mapper) {
    this.bookRepository = bookRepository;
    this.mapper = mapper;
  }

  @Transactional(readOnly = true)
  public Page<BookDTO> getBooks(Pageable p) {
    DbContextHolder.setDbType(DbType.REPLICA1);   // <----- set ThreadLocal DataSource lookup key
                                                  // all connection from here will go to REPLICA1
    Page<Book> booksPage = callActionRepo.findAll(p);
    List<BookDTO> pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class);
    DbContextHolder.clearDbType();               // <----- clear ThreadLocal setting
    return new PageImpl<BookDTO>(pContent, p, callActionsPage.getTotalElements());
  }

  ...//other methods

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

... Или так?Прежде всего, эти статические вызовы методов магического DbContextHolder действительно торчат.Похоже, они не принадлежат бизнес-логике.И они не делают.Они не только не сообщают о цели, но и кажутся хрупкими и подверженными ошибкам (как насчет того, чтобы забыть очистить dbType).А что, если между setDbType и cleanDbType возникает исключение?Мы не можем просто игнорировать это.Мы должны быть абсолютно уверены в том, что мы сбрасываем dbType, в противном случае поток, возвращаемый в ThreadPool, может находиться в «неисправном» состоянии, пытаясь записать реплику при следующем вызове.Итак, нам нужно это:

  @Transactional(readOnly = true)
  public Page<BookDTO> getBooks(Pageable p) {
    try{
      DbContextHolder.setDbType(DbType.REPLICA1);   // <----- set ThreadLocal DataSource lookup key
                                                    // all connection from here will go to REPLICA1
      Page<Book> booksPage = callActionRepo.findAll(p);
      List<BookDTO> pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class);
       DbContextHolder.clearDbType();               // <----- clear ThreadLocal setting
    } catch (Exception e){
      throw new RuntimeException(e);
    } finally {
       DbContextHolder.clearDbType();               // <----- make sure ThreadLocal setting is cleared         
    }
    return new PageImpl<BookDTO>(pContent, p, callActionsPage.getTotalElements());
  }

Yikes >_<!Это определенно не похоже на то, что я хотел бы добавить в каждый метод только для чтения.Можем ли мы сделать лучше?Конечно!Этот паттерн «сделай что-нибудь в начале метода, затем сделай что-нибудь в конце» должен звучать как колокол.Аспекты на помощь!

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

5 голосов
/ 08 декабря 2010

Я не думаю, что решение о том, что SELECT должен идти в одну БД (одну ведомую), а CREATE / UPDATES должно идти в другую (ведущую), является очень хорошим решением. Причины:

  • репликация не происходит мгновенно, поэтому вы можете СОЗДАТЬ что-либо в главной БД и, как часть той же операции, ВЫБРАТЬ это из ведомого устройства и заметить, что данные еще не достигли подчиненного устройства.
  • если один из подчиненных не работает, вам не следует запрещать запись данных в ведущее устройство, поскольку, как только ведомое устройство возвращается в рабочее состояние, его состояние синхронизируется с ведущим. В вашем случае, однако, ваши операции записи зависят как от ведущего, так и от ведомого.
  • Как бы вы тогда определили транзакционность, если фактически используете 2 дБ?

Я бы посоветовал использовать главную базу данных для всех потоков WRITE со всеми инструкциями, которые могут им потребоваться (будь то SELECT, UPDATE или INSERTS). Затем приложение, работающее с потоками только для чтения, может читать из ведомой БД.

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

3 голосов
/ 08 декабря 2010

Вы можете создать 2 фабрики сессий и иметь BaseDao, обертывающий 2 фабрики (или 2 hibernateTemplates, если вы их используете), и использовать методы get с фабрикой и методы saveOrUpdate с другими

1 голос
/ 18 ноября 2016

Попробуйте следующим образом: https://github.com/kwon37xi/replication-datasource

Работает хорошо и очень легко реализуется без каких-либо дополнительных аннотаций или кода.Требуется только @Transactional(readOnly=true|false).

Я использую это решение с Hibernate (JPA), Spring JDBC Template, iBatis.

0 голосов
/ 22 июля 2017

Вы можете использовать DDAL для реализации записи базы данных master и чтения базы данных slave в DefaultDDRDataSource без изменения Daos, и более того, DDAL обеспечил баланс загрузки для баз данных mulit-slave. Это не зависит от весны или зимней спячки. Есть демонстрационный проект, чтобы показать, как его использовать: https://github.com/hellojavaer/ddal-demos, а demo1 - это именно то, что вы описали в сцене.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...