在实际的项目中,我们很少就使用一个数据库,出于灾难恢复或者负载均衡之类目的考虑,生产环境中通常都会存在多台数据库服务器,相应的,在应用程序对这些数据库进行数据访问的时候, 我们通常就会碰到一个比较常见的问题,即如何管理数据访问过程中牵扯的多个数据源。
下面我们不妨从两个角度来阐述一下在应用程序中如何对多个数据源进行管理…
所谓“ 主权独立 ”是指系统中的每一个数据源都对外独立承担暴露数据库资源的职能:
具体的应用场景可能是:
- 每一个数据库所存储的数据性质不同,比如数据库A存储重要的交易信息,数据库B存储次要的系统管理 信息等等, 如果要访问交易信息,那么通常可以明确指定使用对应数据库A的dataSourceA进行数据访问;如果要访问系统管理信息,则明确指定使用对应数据库B 的dataSourceB,其他依次类推。
- 每一个数据库分别承担不同的数据访问请求形式,比如数据库A只允许更新操作不允许查询,数据库B只允许查询不允许更新等, 这个时候,也是可以明确指定使用哪一个dataSource进行数据访问;
当然,类似的场景并非只有这些,但总的意图是相似的,那就是每一个dataSource的职能对于使用它们的客户端来说足够明确,完全是各自独立使 用。 笔者经历的FX项目中就使用了这样的多数据源管理方式。在FX中,设置的MAIN数据库主要存储顾客或者银行与FX Broker之间的交易信息,设置的INFO数据库主要存储汇率以及系统履历之类的信息 [ 32 ] 。 通常情况下,针对交易以及汇率之间的信息存储在逻辑上是可以分开进行的,所以现在,应用程序对应的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.passwork}"/> <!-- other property settings --> </bean> <bean id="infoDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value="${info.jdbcUrl}"/> <property name="driverClassName" value="${info.driver}"/> <property name="username" value="${info.username}"/> <property name="password" value="${info.password}"/> <!-- other property settings --> </bean> <bean id="mainJdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="mainDataSource"/> </bean> <bean id="infoJdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="infoDataSource"/> </bean> <bean id="dataAccessResourceSupport" abstract="true"> <property name="mainJdbcTemplate" ref=""/> <property name="infoJdbcTemplate" ref=""/> </bean> <bean id="someDaoWithMainDS" class="..."> <property name="mainJdbcTemplate" ref="mainJdbcTemplate"/> <!-- other property settings --> </bean> <bean id="someDaoWithInfoDS" class="..."> <property name="infoJdbcTemplate" ref="infoJdbcTemplate"/> <!-- other property settings --> </bean> <bean id="someDaoWithBothDS" class="..." parent="dataAccessResourceSupport"> <!-- other property settings --> </bean>
项目中这种情况下的多数据源管理是最简单的,也是比较容易管理的方式,所以,在你考虑下面将要谈到的这种更加动态,更加复杂的多数据源管理方式之 前, 请先对你的数据访问场景做一个评估,看一下当前这种方式是否已经足够满足项目的数据访问需要,实在不行的话,再考虑后继方案,也就是在运行期间来决定到底 使用多个数据源中的哪一个。
社区中经常提到的“ 多数据源互换 ”即属于这种场景,之所以用“ 合纵连横 ”来形容这些数据源是因为,对于使用它们的数据访问类来说, 这些数据源已经丧失了“ 独立自主 ”的地位,所有与数据访问类进行的交互需要通过“ 盟主 ”进行,该盟主本质上也是一个DataSource, 但它的职责更加倾向于对“ 联盟 ”内的多个DataSource的职能进行协调和管理,最终数据访问所需要的资源由“ 盟主 ”来决定要哪一个DataSource贡献出来。
使用这种多数据源管理方式的具体场景可能有:
- 系统中设置多台“ 地位相当 ”的数据库以实现多机热备,从而保证数据库的高可用性(HA,High Availability),这个时候,如果某一台数据库挂掉的话, 可以迅速切换到另一台数据库,而对于数据访问类来说,这样的切换对其是透明的;
- 系统中存在的多台服务器也是“ 地位相当 ” 的,不过,同一时间他们都处于活动(Active)状态,处于负载均衡等因素考虑,数据访问请求需要在这几台数据库服务器之间进行合理分配, 这个时候,通过统一的一个DataSource来屏蔽这种请求分配的需求,从而屏蔽数据访问类与具体DataSource的耦合;
- 系统中存在的多台数据库服务器现在地位可能相当也可能不相当,但数据访问类在系统启动时间无法明确到底应该使用哪一个数据源进行数据访问,而必须在系统运行期间通过某种条件来判定到底应该使用哪一个数据源, 这个时候,我们也得使用这种“ 合纵连横 ”的方式向数据访问类暴露一个统一的DataSource,由该DataSource来解除数据访问类与具体数据源之间的过紧耦合;
更多场景需要读者根据具体的应用来判定,不过,并非所有的应用要做这样的处理,如果能够保持简单,那尽量保持简单,毕竟,我们提倡K.I.S.S.(Keep It Simple,Stupid)嘛! 要实现这种“ 合纵连和 ” 的多数据源管理方式,总的指导原则就是实现一个自定义的DataSource,让该DataSource来管理系统中存在的多个与具体数据库挂钩的数据 源, 数据访问类只跟这个自定义的DataSource打交道即可。在spring2.0.1发布之前,各个项目中可能存在多种针对这种情况下的多数据源管理方 式, 不过,spring2.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="dataSourc1" 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=".."/> <!-- other property settings --> </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=".."/> <!-- other property settings --> </bean> <bean id="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=".."/> <!-- other property settings --> </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="dataSourc1"/> <property name="targetDataSources" ref="dataSources"/> <property name="dataSourceLookup" ref=""/> </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""/> <!-- other property settings --> </bean>
因为我们不想使用AbstractRoutingDataSource的默认键值(Key)查找行为(根据指定的键值(Key)通过JNDI进行查 找), 所以,我们为PrototypeLoadBalanceDataSource重新设置了DataSourceLookup,转而使用 MapDataSourceLookup。
Tip
在PrototypeLoadBalanceDataSource中,我们直接将查找的键值(Key)写死到了代码中,实际上,更多时候,我们会将 键值 (Key)绑定到当前线程上,而determineCurrentLookupKey()方法内直接从当前线程取得绑定的Key返回即可。 而这种情况下对Key的更改也变得更加灵活多变,比如,我可以在数据访问类内直接将我要访问的数据源对应的查找Key绑定到当前线程,我也可以在系统的某 个位置设置拦截器(Interceptor),当拦截到相应事件的时候,根据逻辑设置绑定到当前线程的查找键值(Key)等等。 读者可以先思考如果让你来实现这么一个AbstractRoutingDataSource,你应该如何处理。不过,如果你已经迫不及待,那可以直接转向 spring的事务管理一章的扩展篇,我将在那里为你展示详细的实现过程。
有了AbstractRoutingDataSource之后,实现这种“ 合纵连横 ”的多数据源管理,将不再像最初看起来那么复杂而神秘。
4.4.2.3. 结束语
因为以上两种多数据源的管理方式在实际的使用过程中可能还有一些变数,所以,最后笔者还是觉得应该提及两点:
-
不管是“
独立主权
”的多数据源管理方式还是“
合纵连横
”的多数据源管理方式, 单独使用任何一种都是有其特定的应用场景的,不过,这并不意味着二者是相互竞争甚至割裂的,实际上,如果必要,我们完全可以组合两种多数据源管理方式。
如果我们将“ 合纵连横 ”的多数据源作为一个整体放入“ 独立主权 ”的多数据源场景中的话,我们可以得到如下的一幅画卷:Figure 4.11. “ 合纵连横 ”争取“ 主权独立 ”
如果我们将“ 独立主权 ”的多个数据源先分别注入上一层对象,然后将上一层对象和数据源作为一个整体再并入“ 合纵连横 ”的多数据源场景的话,那么,我们又可以得出另一幅图景:
Figure 4.12. 从“ 主权独立 ”并入“ 合纵连横 ”
当然,如果愿意,你还可以根据情况采取进一步的组合措施,不过,在进行之前,还是需要你首先全面评估一下整体情况,看是否真的需要这么做,毕竟,复杂度的过多引入有些时候并非必要的。
- 在“ 主权独立 ”的多数据源场景中,我们是将独立的数据源注入给了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,那就看你的啦!
最后,祝各位在多数据源管理的道路上一帆风顺,即使不顺,那么应用以上的方式“ 排除万难去争取胜利 ”也不再是难事了吧?!