【问题标题】:How to setup Hibernate to read/write to different datasources?如何设置 Hibernate 以读取/写入不同的数据源?
【发布时间】:2011-05-22 03:07:51
【问题描述】:

使用 Spring 和 Hibernate,我想写入一个 MySQL 主数据库,并在基于云的 Java webapp 中从另一个复制的从属数据库中读取。

我找不到对应用程序代码透明的解决方案。我真的不想改变我的 DAO 来管理不同的 SessionFactories,因为这看起来真的很混乱,并且将代码与特定的服务器架构耦合在一起。

有什么方法可以告诉 Hibernate 自动将 CREATE/UPDATE 查询路由到一个数据源,然后将 SELECT 路由到另一个数据源?我不想根据对象类型进行任何分片或任何操作 - 只需将不同类型的查询路由到不同的数据源。

【问题讨论】:

  • 您是否在同一个 DAO/服务中同时获得了 UPDATE/CREATE 和 SELECT 查询?一种选择是将它们分开(使设置数据源更容易)
  • 嗯,这听起来是迄今为止我见过的最明智的选择。如果没有更“透明”的选项,我想我可能会试一试。谢谢!
  • 使用 MySQL 代理拆分读写操作如何?有人试过吗?

标签: java mysql hibernate spring architecture


【解决方案1】:

您可以使用DDAL在DefaultDDRDataSource中实现写入主库和读取从库,而无需修改您的Daos,而且DDAL为多从库提供负载平衡。它不依赖于spring或hibernate。有一个演示项目来展示如何使用它:https://github.com/hellojavaer/ddal-demos 和 demo1 正是你描述的场景。

【讨论】:

    【解决方案2】:

    试试这个方法:https://github.com/kwon37xi/replication-datasource

    它运行良好且非常容易实现,无需任何额外的注释或代码。它只需要@Transactional(readOnly=true|false)

    我一直在将此解决方案与 Hibernate(JPA)、Spring JDBC 模板、iBatis 一起使用。

    【讨论】:

    【解决方案3】:

    可以在此处找到示例:https://github.com/afedulov/routing-data-source

    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 标识符?请记住,只要 TransactionsManager 请求连接,就会调用 determineCurrentLookupKey 方法。重要的是要记住每个事务都与一个单独的线程“关联”。更准确地说,TransactionsManager 将 Connection 绑定到当前线程。因此,为了将不同的事务分派到不同的目标数据源,我们必须确保每个线程都能可靠地识别要使用哪个数据源。这使得使用 ThreadLocal 变量将特定的 DataSource 绑定到 Thread 并因此绑定到 Transaction 变得很自然。它是这样完成的:

    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();
       }
    }
    

    如您所见,您还可以使用枚举作为键,Spring 将根据名称正确解析它。关联的 DataSource 配置和键可能如下所示:

      ....
    <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
    

    现在我们可以控制将使用哪个 DataSource 并根据需要转发请求。看起来不错!

    ...或者是吗?首先,那些对神奇 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());
      }
    

    哎呀&gt;_&lt;!这绝对不像我想放入每个只读方法中的东西。我们能做得更好吗?当然!这种“在方法的开头做某事,然后在结尾做某事”的模式应该敲响了警钟。救援方面!

    不幸的是,这篇文章已经太长了,无法涵盖自定义方面的主题。您可以使用此link 跟进使用方面的详细信息。

    【讨论】:

    • 如果您希望通过 Java 而不是 XML 配置 RoutingDataSource。通过:baeldung.com/spring-abstract-routing-data-source
    • slave 接管 master 角色时,这将如何工作?应用程序是否必须不断检查数据源以确定哪个担任主角色?
    【解决方案4】:

    我不认为决定 SELECT 应该转到一个 DB(一个从属)而 CREATE/UPDATES 应该转到另一个(主)是一个非常好的决定。原因是:

    • 复制不是即时的,因此您可以在主数据库中创建一些内容,并作为同一操作的一部分,从从属数据库中选择它,并注意数据尚未到达从属数据库。
    • 如果其中一个slave宕机了,不应该阻止你在master中写入数据,因为一旦slave恢复了,它的状态就会和master同步。不过,在您的情况下,您的写入操作同时依赖于主服务器和从服务器。
    • 如果您实际上使用 2 个 db,您将如何定义事务性?

    我建议将主数据库用于所有 WRITE 流程,以及它们可能需要的所有指令(无论它们是 SELECT、UPDATE 还是 INSERTS)。然后,处理只读流的应用程序可以从从数据库中读取。

    我还建议使用单独的 DAO,每个 DAO 都有自己的方法,这样您就可以清楚地区分只读流程和写入/更新流程。

    【讨论】:

    • 感谢您的意见。 1) Hibernate 缓存不会处理这种情况吗?当我们进行水平扩展时,计划是使用 EHCache 和 Terracotta。 2)不确定我是否理解为什么写操作取决于从属设备的可用性? 3)如果写了一些东西并且没有返回错误,那对我来说已经足够了。
    • 2) 我在想,例如,一个 UPDATE 操作假定一个初始 SELECT 然后对从 SELECT 检索到的操作进行 UPDATE,因此您最终可能会在从属设备上执行初始 SELECT以及 master 上的 UPDATE,这可能会导致定义事务的困难以及复制仍在进行时的问题(slave 不包含 master 拥有的所有数据)。
    • 1) MySQL 复制是一个独立于 ORM 工具的过程,所以我认为这在这里可能没有帮助..
    • 1) 如果 Hibernate 正在执行写入,那么当下一次请求 SELECT 时,如果缓存仍然存在,那么它实际上不会命中数据库,它将使用它写入并存储的值在缓存中。使用 EHCache 和 Terracotta 应确保这在应用服务器实例之间保持一致。
    【解决方案5】:

    您可以创建 2 个会话工厂并使用 BaseDao 包装 2 个工厂(或者如果您使用它们,则使用 2 个 hibernateTemplates)并将 get 方法与 on factory 和 saveOrUpdate 方法与其他工厂一起使用

    【讨论】:

    猜你喜欢
    • 2017-09-05
    • 2014-01-04
    • 1970-01-01
    • 1970-01-01
    • 2019-05-18
    • 2011-05-23
    • 2011-08-24
    • 2013-06-14
    • 2022-12-04
    相关资源
    最近更新 更多