第16章 Spring数据访问之扩展篇(二)

第16章 Spring数据访问之扩展篇

16.2 数据访问中的多数据源

在实际的项目中,我们很少只使用一个数据库。出于灾难恢复或者负载均衡之类目的考虑,生产环境中通常都会存在多台数据库服务器,相应地,在应用程序对这些数据库进行数据访问的时候,我们通常会碰到一个比较常见的问题,即如何管理数据访问过程中涉及的多个数据源。

下面我们不妨从两个角度来阐述一下在应用程序中如何对多个数据源进行管理。

16.2.1 “主权独立”的多数据源

所谓“主权独立”是指系统中的每个数据源都对外独立承担公开数据库资源的职能,如图16-1所示。

image-20220516211654251

具体的应用场景如下。

  • 每个数据库所存储的数据性质不同,比如数据库A存储重要的交易信息,数据库B存储次要的系统管理信息等。如果要访问交易信息,那么通常可以明确指定使用对应数据库A的dataSourceA进行数据访问。如果要访问系统管理信息,则明确指定使用对应数据库B的dataSourceB,依此类推。
  • 每个数据库分别承担不同的数据访问请求形式,比如数据库A只允许更新操作不允许查询,数据库B只允许查询不允许更新等,这时,也是可以明确指定使用哪个dataSource进行数据访问。

当然,场景并非只有这些,但总的意图是相似的,那就是每个dataSource的职能对于使用它们的客户端来说足够明确,完全是各自独立使用

我参与开发的FX项目中就使用了这样的多数据源管理方式。在FX中,设置的MAIN数据库主要存储顾客或者银行与FXBroker之间的交易信息,设置的INFO数据库主要存储汇率以及系统履历之类的信息。通常情况下,交易以及汇率之间的信息存储,在逻辑上是可以分开进行的,所以现在,应用程序对应的Spring配置内容基本如下方代码清单所示。

<bean id="mainDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
	<property name="url" value="$(main.jdbcUrl)"/>
	<property name="driverClassName" value="$(main.driver)"/>
	<property name="username" value="$(main.username)"/>
	<property name="password" value="$main.password)"/>
	<!--其他属性设置-->
</bean>

<bean id="infoDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
	<property name="url" value="$(info.jdbcUrl)"/>
	<property name="driverClassName" va1ue="$finfo.driver)"/>
	<property name="username" value="$(info.username)"/>
	<property name="password" value="$info.password)"/>
	<!--其他属性设置-->
</bean>

<bean id="mainJdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
	<property name="dataSource" ref="mainDataSource"/>
</bean>

<bean id="infoJdbcTemp1ate" class="org.springframework.jdbc.core.UdbcTemplate">
	<property name="dataSource" ref="infoDataSource"/>
</bean>

<bean id="dataAccessResourceSupport" abstract="true">
	<property name="mainJdbcTemplate" ref="mainJdbcTemplate"/>
	<property name="infoJdbcTemplate" ref="infoJdbcTemplate"/>
</bean>

<bean id="someDaowithMainDS" class="...">
	<property name="mainJdbcTemplate" ref="mainJabcTemplate"/>
	<!--其他属性设置-->
</bean>

<bean id="someDaoWithInfoDS" class="...">
	<property name="infoJdbcTemplate" ref="infoJdbcTemplate"/>
	<!--其他属性设置-->
</bean>

<bean id="someDaoWithBothDS" class="..." parent="dataAccessResourceSupport">
	<!--其他属性设置-->
</bean>

这种情况下的多数据源管理是最简单的,也是比较容易管理的。所以,你在考虑下面将要介绍的这种更加动态、更加复杂的多数据源管理方式之前,请先对自己的数据访问场景做一个评估,看一下当前这种方式是否已经足够满足项目的数据访问需要,实在不行的话,再考虑后继方案,也就是在运行期间来决定到底使用多个数据源中的哪一个。

16.2.2 “合纵连横”的多数据源

社区中经常提到的多数据源互换即属于这种场景。之所以用“合纵连横”来形容这些数据源是因为,对于使用它们的数据访问类来说,这些数据源已经丧失了“独立自主”的地位,所有与数据访问类进行的交互需要通过**“盟主”进行,该盟主本质上也是一个DataSource,但它的职贵更加倾向于对“联盟”内的多个DataSource的职能进行协调和管理**,最终数据访问所需要的资源由“盟主”来决定要哪一个DataSource提供(见图16-2)。

image-20220516213745107

使用这种多数据源管理方式的具体场景如下所述。

(1)在系统中设置多台“地位相当”的数据库以实现多机热备,从而保证数据库的高可用性(HA,High Availability)。这时,如果某一台数据库挂掉,可以迅速切换到另一台数据库,而对于数据访问类来说,这样的切换是透明的。

(2)系统中存在的多台服务器也是"地位相当”的。不过,同一时间它们都处于活动(Active)状态,出于负载均衡等因素考虑,数据访问请求需要在这几台数据库服务器之间进行合理分配。这时,通过统一的一个DataSource来屏蔽这种请求分配的需求,从而解除数据访问类与具体DataSource的耦合。

扫描二维码关注公众号,回复: 14223539 查看本文章

(3)系统中存在的多台数据库服务器地位可能相当,也可能不相当,但数据访问类在系统启动时间无法明确到底应该使用哪个数据源进行数据访问,而必须在系统运行期间通过某种条件来判定到底应该使用哪个数据源。这时,我们也得使用这种“合纵连横”的方式向数据访问类公开一个统一的DataSource,由该DataSource来解除数据访问类与具体数据源之间的过紧耦合。

更多场景需要你根据具体的应用来判定。不过,并非所有的应用都要做这样的处理。如果能够保持简单,那么尽量保持简单,毕竟,我们提倡K.I.S.S.(Keep It Simple,Stupid)。

要实现这种“合纵连横”的多数据源管理方式,总的指导原则就是实现一个自定义的DataSource。让该DataSource来管理系统中存在的多个与具体数据库挂钩的数据源,数据访问类只跟这个自定义的DataSource打交道即可。在Spring 2.0.1发布之前,各个项目中可能存在多种针对这种情况的多数据源管理方式。Spring 2.0.1发布之后,引入了AbstractRoutingDataSource,使用该类可以实现普遍意义上的多数据源管理功能。

假设有三台数据库用来实现负载均衡,所有的数据访问请求最终需要平均的分配到这三台数据库服务器上,那么,可以通过继承AbstractRoutingDataSource来快速实现一个满足这样场景的原型(Prototype),见下方代码清单。

public class PrototypeLoadBalanceDataSource extends AbstractRoutingDataSource {
    
    
    private Lock lock = new ReentrantLock();
    private int counter = 0;
    private int dataSourceNumber = 3;

    @Override
    protected Object determineCurrentLookupKey() {
    
    
        lock.lock();
        try {
    
    
            counter++;
            int lookupKey = counter % getDataSourceNumber();
            return new Integer(lookupKey);
        } finally {
    
    
            lock.unlock();
        }
    }
    // ...
}

我们在介绍AbstractRoutingDataSource的时候说过,要继承该类,通常只需要给出determineCurrentLookupKey()方法的逻辑即可。下方代码清单是针对PrototypeLoadBalanceDataSource的配置。

<bean id="dataSource1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
	<property name="url" value=".."/>
	<property name="driverClassName" value=".."/>
	<property name="username" value=".."/>
	<property name="password" value=".."/>
	<!--其他属性配置-->
</bean>

<bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
	<property name="url" value=".."/>
	<property name="driverClassName" value=".."/>
	<property name="username" value=".."/>
	<property name="password" value=".."/>
	<!--其他属性配置-->
</bean>

<beanid="dataSource3" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
	<property name="url" value=".."/>
	<property name="driverClassName" value=".."/>
	<property name="username" value=".."/>
	<property name="password" value=".."/>
	<!--其他属性配置-->
</bean>

<util:map id="dataSources">
	<entry key="0" value-ref="dataSource1"/>
	<entry key="1" value-ref="dataSource2"/>
	<entry key="2" value-ref="dataSource3"/>
</util:map>

<bean id="dataSourceLookup" class="org.springframework.jdbc.datasource.lookup.MapDataSourceLookup">
	<constructor-arg>
		<ref bean="dataSources"/>
	</constructor-arg>
</bean>

<bean id="dataSource" class="..PrototypeLoadBalanceDataSource">
	<property name="defaultTargetDataSource" ref="dataSource1"/>
	<property name="targetDataSources" ref="dataSources"/>
	<property name="dataSourcelookup" ref="dataSourceLookup"/>
</bean>

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
	<property name="dataSource" ref="dataSource"/>
</bean>

<bean id="someDao" class="...">
	<property name="jdbcTemplate" ref="jdbcTemplate"/>
	<!--其他属性设置-->
</bean>

因为我们不想使用AbstractRoutingDataSource默认的键查找行为(根据指定的键通过JNDI进行查找),所以为PrototypeLoadBalanceDataSource重新设置了DataSourceLookup,转而使用MapDataSourceLookup。

提示:在PrototypeLoadBalanceDataSource中,我们直接将查找的键硬编码到了代码中。实际上,更多时候,我们会将键绑定到当前线程上,而在determineCurrentLookupKey()方法内直接从当前线程取得绑定的键返回即可。而这种情况下对键的更改也变得更加灵活多变,比如,我们可以在数据访问类内直接将要访问的数据源对应的查找键绑定到当前线程,也可以在系统的某个位置设置拦截器,当拦截到相应事件的时候,根据逻辑设置绑定到当前线程的查找键等。你可以先思考如果让你来实现这么一个AbstractRoutingDataSource,应该如何处理。不过,如果你已经迫不及待,那么可以直接转向Spring的事务管理章节中的扩展篇,我将在那里为你展示详细的实现过程。

有了AbstractRoutingDataSource之后,实现这种“合纵连横”的多数据源管理,将不再像最初看起来那么复杂和神秘。

16.2.3 结束语

因为以上两种多数据源的管理方式在实际的使用过程中可能还有一些变数,所以最后,还是应该提及如下两点注意事项。

(1)不管是“独立主权”的多数据源管理方式还是“合纵连横”的多数据源管理方式,单独使用任何一种都是有其特定的应用场景的。不过,这并不意味着二者是相互竞争甚至割裂的。如果必要,我们完全可以组合两种多数据源管理方式。

如果将“合纵连横”的多数据源作为一个整体放入“独立主权”的多数据源场景中,我们可以得到如图16-3所示的一幅画卷。

如果将“独立主权”的多个数据源先分别注入上一层对象,然后将上一层对象和数据源作为一个整体再并入“合纵连横”的多数据源场景,那么,我们又可以得出另一幅图景(如图16-4所示)。

image-20220516215936027 image-20220516215950792

当然,如果愿意,我们还可以根据情况采取进一步的组合措施。不过,在进行之前,还是需要先全面评估一下整体情况,看是否真的需要这么做。毕竟,复杂度的过多引入有些时候并非必要。

(2)在“主权独立”的多数据源场景中,我们是将独立的数据源注入给了JdbcTemplate,但这只是为了演示的目的,实际上,对于iBATIS和Hibernate来说,这样的场景也是类似的。

不过,在“合纵连横”的多数据源场景中,将JdbcTemplate的使用类推iBATIS,即sqlMapClientTemplate是可以的。但以同样的方式类推到Hibernate则有需要注意的地方。我们可以将“合纵连横”的多个数据源注入给Hibernate的SessionFactory(实际上是通过Spring的LocalSessionFactoryBean),然后HibernateTemplate直接引用这个SessionFactory即可。

但当开启了Hibernate的二级缓存的时候(与SessionFactory挂钩),这样的多个数据源直接注入SessionFactory并且可以动态查找替换的方式可能造成问题。如果二级缓存中有与当前使用的DataSource挂钩的内容,而这时切换到了下一个DataSource,那么二级缓存里的内容需要根据情况进行合理的处理,或者清空,或者通过某种方式来同步,否则在并发的情况下,难免出现问题。当然,如果可以忽略这样的数据冲突,那么可能也有不处理的理由。

如果不需要开启Hibernate的二级缓存,或者可以忽略二级缓存数据的不一致性,那么,采用“合纵连横”的多数据源直接注入SessionFactory的方式来实现多个数据源的管理是可以的。否则,可以将“独立主权”的多数据源管理方式并入“合纵连横”的多数据源管理方式,以SessionFactory一级替代DataSource一级,这也就是第二种组合场景所描绘的那样。这时,我们可以像Spring提供AbstractRoutingDataSource那样,提供一个AbstractRoutingSessionFactory,也可以自己实现一个SessionFactory来屏蔽多个具体的SessionFactory。总之方法和原则与多个数据源的处理方式是类似的,至于要采用设计模式还是AOP,那就看你了。

最后,祝各位在多数据源管理的道路上一帆风顺,即使不顺,那么应用以上的方式“排除万难去争取胜利”也不再是难事了吧?

16.3 Spring3.0展望

在Spring3.0将全面升级到Java5这一前提之下,我们不难想象,像JdbcTemplate这样的重量级选手绝对会被“重新塑造”一番。虽然SimpleJdbcTemplate之前已经有“担此重任”之势,但既然Spring3.0中的API将大修一把,SimpleJdbcTemplate可能还会保持,但只可能局限在某些场景下的使用。更多时候,泛型化之后的JdbcTemplate依然会是Spring数据访问层的主力战将。

虽然Spring的数据访问层提供了两种数据访问最佳实践方式,但是,基于操作对象的实践方式在实际使用过程中并非像预想的那样受欢迎,除了StoredProcedure确切地可以减轻存储过程调用的复杂度,其他类型的操作对象完全可以被JdbcTemplate轻松替代。基于这一前提,在Spring3.0中将更加提倡基于JdbcTemplate的数据访问实践方式,除了storedProcedure,其他类型的操作对象将不推荐使用。

随着JPA规范的不断推行,各种ORM解决方案之间的差异性将不断被弱化,而且,JPA2.0规范也在紧锣密鼓的制定中。不难想象,Spring3.0为ORM提供集成支持的时候,将更多的向JPA倾斜。正像Spring团队在自己的博客中所说,他们将尽早地提供对JPA2.0的支持,这更使得JPA的排名预期持续升高。另外,因为Toplink一直没有引起更多的目光关注,预计Spring3.0中将放弃对Toplink的集成支持。

16.4 小结

Spring的数据访问层对某些问题的处理方式和最佳实践是值得我们借鉴的。本章我们引申了Spring数据访问层中重用模板方法模式与Callback接口相结合的问题处理的理念,借助于FTPClient和HttpClient的示例,帮助大家思考。希望大家可以举一反三,在日常开发中的合适场景采用Spring数据访问层中重用的这种实践方式。另外,本章也简单分析和阐述了数据访问中涉及的多数据源管理的问题,帮助大家理清思路,以便大家在日常开发工作中得心应手的处理类似问题。

猜你喜欢

转载自blog.csdn.net/qq_34626094/article/details/124895441