项目中我们经常会遇到多数据源的问题,尤其是数据同步或定时任务等项目更是如此。spring动态配置多数据源,即在大型应用中对数据进行切分,并且采用多个数据库实例进行管理,这样可以有效提高系统的水平伸缩性。而这样的方案就会不同于常见的单一数据实例的方案,这就要程序在运行时根据当时的请求及系统状态来动态的决定将数据存储在哪个数据库实例中,以及从哪个数据库提取数据。
Spring2.x以后的版本中采用Proxy模式,就是我们在方案中实现一个虚拟的数据源,并且用它来封装数据源选择逻辑,这样就可以有效地将数据源选择逻辑从Client中分离出来。Client提供选择所需的上下文(因为这是Client所知道的),由虚拟的DataSource根据Client提供的上下文来实现数据源的选择。
从原始的单个数据源思考:
正如上图所示,每一块都是指定绑死的,如果是多个数据源,也只能是下图中那种方式。
可看出在Dao层代码中写死了两个SessionFactory,这样日后如果再多一个数据源,还要改代码添加一个SessionFactory,显然这并不符合开闭原则。
那么正确的做法应该是
因而最终的实现过程:
虚拟的DataSource仅需继承AbstractRoutingDataSource实现determineCurrentLookupKey()在其中封装数据源的选择逻辑。
一、动态配置多数据源
1.首先在spring配置文件applicationContext.xml文件中进行多数据源映射关系的配置:
先来个完整配置:
<!-- 读取dataSource.properties配置文件 -->
<context:property-placeholder location="classpath*:dataSource.properties" />
<!-- 多数据源相同的信息 -->
<bean id="parentDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<property name="driverClass"><!-- 指定连接数据库的驱动 -->
<value>${jdbc.driver}</value>
</property>
<property name="idleConnectionTestPeriod"><!--每60秒检查所有连接池中的空闲连接。Default: 0 -->
<value>${idleConnectionTestPeriod}</value>
</property>
<property name="acquireRetryAttempts"><!--定义在从数据库获取新连接失败后重复尝试的次数。Default: 30 -->
<value>${acquireRetryAttempts}</value>
</property>
<property name="breakAfterAcquireFailure">
<!--
获取连接失败将会引起所有等待连接池来获取连接的线程抛出异常。但是数据源仍有效保留,并在下次调用getConnection()的时候继续尝试获取连接。如果设为true,那么在尝试获取连接失败后该数据源将申明已断开并永久关闭。Default:
false
-->
<value>${breakAfterAcquireFailure}</value>
</property>
<property name="testConnectionOnCheckout">
<!--
因性能消耗大请只在需要的时候使用它。如果设为true那么在每个connection提交的时候都将校验其有效性。建议使用idleConnectionTestPeriod或automaticTestTable
等方法来提升连接测试的性能。Default: false
-->
<value>${testConnectionOnCheckout}</value>
</property>
<property name="minPoolSize"><!-- 最小链接数 -->
<value>${minPoolSize}</value>
</property>
<property name="maxPoolSize"><!-- 最大连接数 -->
<value>${maxPoolSize}</value>
</property>
<property name="initialPoolSize"> <!--连接池初始化时获取的链接数,介于minPoolSize和maxPoolSize之间 -->
<value>${initialPoolSize}</value>
</property>
<property name="acquireIncrement"><!-- 在当前连接数耗尽的时候,一次获取的新的连接数 -->
<value>${acquireIncrement}</value>
</property>
<property name="maxIdleTime"><!-- 最大空闲的时间,单位是秒,无用的链接再过时后会被回收 -->
<value>${maxIdleTime}</value>
</property>
<property name="maxStatements">
<!--
JDBC的标准参数,用以控制数据源内加载的PreparedStatements数量。但由于预缓存的statements属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素
-->
<value>${maxStatements}</value>
</property>
</bean>
<!-- Credit System,征信平台 数据源配置 -->
<bean parent="parentDataSource" id="c_dataSource">
<property name="user"><!-- 指定连接数据库的用户名 -->
<value>${jdbc.c_username}</value>
</property>
<property name="password"><!-- 指定连接数据库的密码 -->
<value>${jdbc.c_password}</value>
</property>
<property name="jdbcUrl">
<value>${jdbc.c_url}</value>
</property>
</bean>
<!-- 数据采集数据源配置 -->
<bean parent="parentDataSource" id="sjcjDataSource">
<property name="user"><!-- 指定连接数据库的用户名 -->
<value>${jdbc.sjcj_username}</value>
</property>
<property name="password"><!-- 指定连接数据库的密码 -->
<value>${jdbc.sjcj_password}</value>
</property>
<property name="jdbcUrl">
<value>${jdbc.sjcj_url}</value>
</property>
</bean>
<!-- 报告数据源配置 -->
<bean parent="parentDataSource" id="r_dataSource">
<property name="user"><!-- 指定连接数据库的用户名 -->
<value>${jdbc.r_username}</value>
</property>
<property name="password"><!-- 指定连接数据库的密码 -->
<value>${jdbc.r_password}</value>
</property>
<property name="jdbcUrl">
<value>${jdbc.r_url}</value>
</property>
</bean>
<!-- 读取hive的数据源的配置
<bean id="impalaDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<property name="driverClass">
<value>${jdbc.impala_driver}</value>
</property>
<property name="jdbcUrl">
<value>${jdbc.impala_url}</value>
</property>
</bean>
-->
<!--动态数据源配置 -->
<bean class="com.ixinnuo.credit.common.datasource.DynamicDataSource" id="dataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry value-ref="c_dataSource" key="c_ds"></entry> //其中key值可以作为唯一的标志进行数据源之间的切换
<entry value-ref="r_dataSource" key="r_ds"></entry>
<entry value-ref="impalaDataSource" key="impala_ds"></entry>
<entry value-ref="sjcjDataSource" key="sjcj_ds"></entry>
</map>
</property>
<property name="defaultTargetDataSource" ref="c_dataSource">
</property>
</bean>
<!--配置数据库会话工厂-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="mapperLocations">
<array>
<value>classpath*:com/ixinnuo/credit/**/mapping/**/*.xml</value>
</array>
</property>
<property name="typeAliasesPackage" value="com.ixinnuo.credit" />
<property name="plugins">
<array>
<!-- 性能拦截器,用于输出每条 SQL 语句及其执行时间 -->
<bean class="com.ixinnuo.credit.common.util.interceptor.PerformanceInterceptor"></bean>
<!-- MyBatis分页插件 -->
<bean class="com.github.pagehelper.PageHelper">
<property name="properties">
<value>
dialect=mysql
reasonable=true
</value>
</property>
</bean>
<!-- MyBatis通用Mapper -->
<bean class="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
<property name="properties">
<value>
mappers=tk.mybatis.mapper.common.Mapper
</value>
</property>
</bean>
</array>
</property>
</bean>
<!-- 事务管理器配置 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.ixinnuo.credit.**.dao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>
<!-- 通知配置 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="delete*" rollback-for="Exception" />
<tx:method name="save*" rollback-for="Exception" />
<tx:method name="insert*" rollback-for="Exception" />
<tx:method name="update*" rollback-for="Exception" />
<tx:method name="select*" read-only="true" rollback-for="Exception" />
<tx:method name="get*" read-only="true" rollback-for="Exception" />
<tx:method name="getIxinnuoData" rollback-for="Exception" />
</tx:attributes>
</tx:advice>
<!-- 事务切面配置 -->
<aop:config>
<aop:pointcut id="serviceOperation" expression="execution(* *..service..*(..))" />
<aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice" />
</aop:config>
关于使用中的重点配置:
- <bean class="com.xxxx.datasouce.DynamicDataSource" id="dataSource">
- <property name="targetDataSources">
- <map key-type="java.lang.String">
- <entry value-ref="testDataSource" key="test"></entry>
- <entry value-ref="UserDataSource" key="User"></entry>
- </map>
- </property>
- <property name="defaultTargetDataSource" ref="testDataSource" ></property>
- </bean>
在这个配置中第一个property属性配置目标数据源,<map key-type="Java.lang.String">中的key-type必须要和静态键值对照类DataSourceConst中的值的类型相 同;<entry key="User" value-ref="userDataSource"/>中key的值必须要和静态键值对照类中的值相同,如果有多个值,可以配置多个< entry>标签。第二个property属性配置默认的数据源。
二:
创建切换动态数据源需要的类:
1. 数据源的名称常量类:
-
-
- (常量名字如test/User是在spring配置文件中数据源对应的key值)
-
-
-
- public class DataSourceConstant{
- public static final String TEST="test";
- public static final String USER="User";
- }
2. 建立一个获得和设置上下文环境的类,主要负责
改变上下文数据源的名称:
-
-
-
-
-
-
- public class DataSourceContextHolder {
- private static final ThreadLocal contextHolder = new ThreadLocal();
-
-
- public static void setDataSourceType(String dataSourceType) {
- contextHolder.set(dataSourceType);
- }
-
- public static String getDataSourceType() {
- return (String) contextHolder.get();
- }
-
- public static void clearDataSourceType() {
- contextHolder.remove();
- }
-
- }
3. 建立动态数据源类,注意,这个类必须继承AbstractRoutingDataSource,且实现方法determineCurrentLookupKey,该方法返回一个Object,一般是返回字符串:
-
-
-
-
-
-
- public class DynamicDataSource extends AbstractRoutingDataSource {
-
- protected Object determineCurrentLookupKey() {
-
- return DataSourceContextHolder.getDataSourceType();
- }
-
- }
动态切换是数据源类:
在需要用到其他数据源的情况下只需要进行set方法将数据源类型更改即可:
- DataSourceContextHolder.setDataSourceType(DataSourceConst.TEST);
该方案的优势
首先,这个方案完全是在spring的框架下解决的,数据源依然配置在spring的配置文件中,sessionFactory依然去配置它的dataSource属性,它甚至都不知道dataSource的改变。唯一不同的是在真正的dataSource与sessionFactory之间增加了一个
MultiDataSource。
其次,实现简单,易于维护。这个方案虽然我说了这么多东西,其实都是分析,真正需要我们写的代码就只有MultiDataSource、SpObserver两个类。MultiDataSource类真正要写的只有getDataSource()和getDataSource(sp)两个方法,而SpObserver类更简单了。实现越简单,出错的可能就越小,维护性就越高。
最后,这个方案可以使单数据源与多数据源兼容。这个方案完全不影响BUS和DAO的编写。如果我们的项目在开始之初是单数据源的情况下开发,随着项目的进行,需要变更为多数据源,则只需要修改spring配置,并少量修改MVC层以便在请求中写入需要的数据源名,变更就完成了。如果我们的项目希望改回单数据源,则只需要简单修改配置文件。这样,为我们的项目将增加更多的弹性。
该方案的缺点
没有能够解决多用户访问单例“sessionFactory”时共享“dataSource”变量,导致产生争抢“dataSource”的结果,本质类似于操作系统中的“生产者消费者”问题。因此当多用户访问时,多数据源可能会导致系统性能下降的后果。