Spring, MyBatis 多数据源的配置和管理

原文链接:http://www.2cto.com/kf/201505/400273.html


同一个项目有时会涉及到多个数据库,也就是多数据源。多数据源又可以分为两种情况:

 
1)两个或多个数据库没有相关性,各自独立,其实这种可以作为两个项目来开发。比如在游戏开发中一个数据库是平台数据库,其它还有平台下的游戏对应的数据库;
 
2)两个或多个数据库是master-slave的关系,比如有mysql搭建一个 master-master,其后又带有多个slave;或者采用MHA搭建的master-slave复制;
 
目前我所知道的 Spring 多数据源的搭建大概有两种方式,可以根据多数据源的情况进行选择。
 
1. 采用spring配置文件直接配置多个数据源
 

比如针对两个数据库没有相关性的情况,可以采用直接在spring的配置文件中配置多个数据源,然后分别进行事务的配置,如下所示:

[html]  view plain  copy
  1. <context:component-scan base-package="net.aazj.service,net.aazj.aop" />  
  2. <context:component-scan base-package="net.aazj.aop" />  
  3. <!-- 引入属性文件 -->  
  4. <context:property-placeholder location="classpath:config/db.properties" />  
  5.    
  6. <!-- 配置数据源 -->  
  7. <bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">  
  8.     <property name="url" value="${jdbc_url}" />  
  9.     <property name="username" value="${jdbc_username}" />  
  10.     <property name="password" value="${jdbc_password}" />  
  11.     <!-- 初始化连接大小 -->  
  12.     <property name="initialSize" value="0" />  
  13.     <!-- 连接池最大使用连接数量 -->  
  14.     <property name="maxActive" value="20" />  
  15.     <!-- 连接池最大空闲 -->  
  16.     <property name="maxIdle" value="20" />  
  17.     <!-- 连接池最小空闲 -->  
  18.     <property name="minIdle" value="0" />  
  19.     <!-- 获取连接最大等待时间 -->  
  20.     <property name="maxWait" value="60000" />  
  21. </bean>  
  22.    
  23. <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">  
  24.   <property name="dataSource" ref="dataSource" />  
  25.   <property name="configLocation" value="classpath:config/mybatis-config.xml" />  
  26.   <property name="mapperLocations" value="classpath*:config/mappers/**/*.xml" />  
  27. </bean>  
  28.    
  29. <!-- Transaction manager for a single JDBC DataSource -->  
  30. <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
  31.     <property name="dataSource" ref="dataSource" />  
  32. </bean>  
  33.    
  34. <!-- 使用annotation定义事务 -->  
  35. <tx:annotation-driven transaction-manager="transactionManager" />   
  36.    
  37. <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
  38.   <property name="basePackage" value="net.aazj.mapper" />  
  39.   <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>  
  40. </bean>  
  41.    
  42. <!-- Enables the use of the @AspectJ style of Spring AOP -->  
  43. <aop:aspectj-autoproxy/>  
  44.    
  45. <!-- ===============第二个数据源的配置=============== -->  
  46. <bean name="dataSource_2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">  
  47.     <property name="url" value="${jdbc_url_2}" />  
  48.     <property name="username" value="${jdbc_username_2}" />  
  49.     <property name="password" value="${jdbc_password_2}" />  
  50.     <!-- 初始化连接大小 -->  
  51.     <property name="initialSize" value="0" />  
  52.     <!-- 连接池最大使用连接数量 -->  
  53.     <property name="maxActive" value="20" />  
  54.     <!-- 连接池最大空闲 -->  
  55.     <property name="maxIdle" value="20" />  
  56.     <!-- 连接池最小空闲 -->  
  57.     <property name="minIdle" value="0" />  
  58.     <!-- 获取连接最大等待时间 -->  
  59.     <property name="maxWait" value="60000" />  
  60. </bean>  
  61.    
  62. <bean id="sqlSessionFactory_slave" class="org.mybatis.spring.SqlSessionFactoryBean">  
  63.   <property name="dataSource" ref="dataSource_2" />  
  64.   <property name="configLocation" value="classpath:config/mybatis-config-2.xml" />  
  65.   <property name="mapperLocations" value="classpath*:config/mappers2/**/*.xml" />  
  66. </bean>  
  67.    
  68. <!-- Transaction manager for a single JDBC DataSource -->  
  69. <bean id="transactionManager_2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
  70.     <property name="dataSource" ref="dataSource_2" />  
  71. </bean>  
  72.    
  73. <!-- 使用annotation定义事务 -->  
  74. <tx:annotation-driven transaction-manager="transactionManager_2" />   
  75.    
  76. <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
  77.   <property name="basePackage" value="net.aazj.mapper2" />  
  78.   <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory_2"/>  
  79. </bean>  

如上所示,我们分别配置了两个 dataSource,两个sqlSessionFactory,两个transactionManager,以及关键的地方在于MapperScannerConfigurer 的配置——使用sqlSessionFactoryBeanName属性,注入不同的sqlSessionFactory的名称,这样的话,就为不同的数据库对应的 mapper 接口注入了对应的 sqlSessionFactory。
 
需要注意的是,多个数据库的这种配置是不支持分布式事务的,也就是同一个事务中,不能操作多个数据库。这种配置方式的优点是很简单,但是却不灵活。对于master-slave类型的多数据源配置而言不太适应,master-slave性的多数据源的配置,需要特别灵活,需要根据业务的类型进行细致的配置。比如对于一些耗时特别大的select语句,我们希望放到slave上执行,而对于update,delete等操作肯定是只能在master上执行的,另外对于一些实时性要求很高的select语句,我们也可能需要放到master上执行——比如一个场景是我去商城购买一件兵器,购买操作的很定是master,同时购买完成之后,需要重新查询出我所拥有的兵器和金币,那么这个查询可能也需要防止master上执行,而不能放在slave上去执行,因为slave上可能存在延时,我们可不希望玩家发现购买成功之后,在背包中却找不到兵器的情况出现。
 
所以对于master-slave类型的多数据源的配置,需要根据业务来进行灵活的配置,哪些select可以放到slave上,哪些select不能放到slave上。所以上面的那种所数据源的配置就不太适应了。
 
2. 基于 AbstractRoutingDataSource 和 AOP 的多数据源的配置
 
基本原理是,我们自己定义一个DataSource类ThreadLocalRountingDataSource,来继承AbstractRoutingDataSource,然后在配置文件中向ThreadLocalRountingDataSource注入 master 和 slave 的数据源,然后通过 AOP 来灵活配置,在哪些地方选择  master 数据源,在哪些地方需要选择 slave数据源。下面看代码实现:
[java]  view plain  copy
  1. package net.aazj.enums;  
  2.    
  3. /** 
  4.  * 数据源的类别:master/slave 
  5.  */  
  6. public enum DataSources {  
  7.     MASTER, SLAVE  
  8. }  

2)通过 TheadLocal 来保存每个线程选择哪个数据源的标志(key):

[java]  view plain  copy
  1. package net.aazj.util;  
  2.    
  3. import net.aazj.enums.DataSources;  
  4.    
  5. public class DataSourceTypeManager {  
  6.     private static final ThreadLocal<DataSources> dataSourceTypes = new ThreadLocal<DataSources>(){  
  7.         @Override  
  8.         protected DataSources initialValue(){  
  9.             return DataSources.MASTER;  
  10.         }  
  11.     };  
  12.        
  13.     public static DataSources get(){  
  14.         return dataSourceTypes.get();  
  15.     }  
  16.        
  17.     public static void set(DataSources dataSourceType){  
  18.         dataSourceTypes.set(dataSourceType);  
  19.     }  
  20.        
  21.     public static void reset(){  
  22.         dataSourceTypes.set(DataSources.MASTER0);  
  23.     }  
  24. }  

3)定义 ThreadLocalRountingDataSource,继承AbstractRoutingDataSource:

[java]  view plain  copy
  1. package net.aazj.util;  
  2.    
  3. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;  
  4.    
  5. public class ThreadLocalRountingDataSource extends AbstractRoutingDataSource {  
  6.     @Override  
  7.     protected Object determineCurrentLookupKey() {  
  8.         return DataSourceTypeManager.get();  
  9.     }  
  10. }  


4)在配置文件中向 ThreadLocalRountingDataSource 注入 master 和 slave 的数据源:
[html]  view plain  copy
  1. <context:component-scan base-package="net.aazj.service,net.aazj.aop" />  
  2. <context:component-scan base-package="net.aazj.aop" />  
  3. <!-- 引入属性文件 -->  
  4. <context:property-placeholder location="classpath:config/db.properties" />      
  5. <!-- 配置数据源Master -->  
  6. <bean name="dataSourceMaster" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">  
  7.     <property name="url" value="${jdbc_url}" />  
  8.     <property name="username" value="${jdbc_username}" />  
  9.     <property name="password" value="${jdbc_password}" />  
  10.     <!-- 初始化连接大小 -->  
  11.     <property name="initialSize" value="0" />  
  12.     <!-- 连接池最大使用连接数量 -->  
  13.     <property name="maxActive" value="20" />  
  14.     <!-- 连接池最大空闲 -->  
  15.     <property name="maxIdle" value="20" />  
  16.     <!-- 连接池最小空闲 -->  
  17.     <property name="minIdle" value="0" />  
  18.     <!-- 获取连接最大等待时间 -->  
  19.     <property name="maxWait" value="60000" />  
  20. </bean>      
  21. <!-- 配置数据源Slave -->  
  22. <bean name="dataSourceSlave" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">  
  23.     <property name="url" value="${jdbc_url_slave}" />  
  24.     <property name="username" value="${jdbc_username_slave}" />  
  25.     <property name="password" value="${jdbc_password_slave}" />  
  26.     <!-- 初始化连接大小 -->  
  27.     <property name="initialSize" value="0" />  
  28.     <!-- 连接池最大使用连接数量 -->  
  29.     <property name="maxActive" value="20" />  
  30.     <!-- 连接池最大空闲 -->  
  31.     <property name="maxIdle" value="20" />  
  32.     <!-- 连接池最小空闲 -->  
  33.     <property name="minIdle" value="0" />  
  34.     <!-- 获取连接最大等待时间 -->  
  35.     <property name="maxWait" value="60000" />  
  36. </bean>      
  37. <bean id="dataSource" class="net.aazj.util.ThreadLocalRountingDataSource">  
  38.     <property name="defaultTargetDataSource" ref="dataSourceMaster" />  
  39.     <property name="targetDataSources">  
  40.         <map key-type="net.aazj.enums.DataSources">  
  41.             <entry key="MASTER" value-ref="dataSourceMaster"/>  
  42.             <entry key="SLAVE" value-ref="dataSourceSlave"/>  
  43.             <!-- 这里还可以加多个dataSource -->  
  44.         </map>  
  45.     </property>  
  46. </bean>      
  47. <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">  
  48.   <property name="dataSource" ref="dataSource" />  
  49.   <property name="configLocation" value="classpath:config/mybatis-config.xml" />  
  50.   <property name="mapperLocations" value="classpath*:config/mappers/**/*.xml" />  
  51. </bean>      
  52. <!-- Transaction manager for a single JDBC DataSource -->  
  53. <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
  54.     <property name="dataSource" ref="dataSource" />  
  55. </bean>      
  56. <!-- 使用annotation定义事务 -->  
  57. <tx:annotation-driven transaction-manager="transactionManager" />   
  58. <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
  59.   <property name="basePackage" value="net.aazj.mapper" />  
  60.   <!-- <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> -->  
  61. </bean>  

上面spring的配置文件中,我们针对master数据库和slave数据库分别定义了dataSourceMaster和dataSourceSlave两个dataSource,然后注入到<bean id="dataSource" class="net.aazj.util.ThreadLocalRountingDataSource"> 中,这样我们的dataSource就可以来根据 key 的不同来选择dataSourceMaster和 dataSourceSlave了。
 
5)使用Spring AOP 来指定 dataSource 的 key ,从而dataSource会根据key选择 dataSourceMaster 和 dataSourceSlave:
[java]  view plain  copy
  1. package net.aazj.aop;  
  2.    
  3. import net.aazj.enums.DataSources;  
  4. import net.aazj.util.DataSourceTypeManager;  
  5.    
  6. import org.aspectj.lang.JoinPoint;  
  7. import org.aspectj.lang.annotation.Aspect;  
  8. import org.aspectj.lang.annotation.Before;  
  9. import org.aspectj.lang.annotation.Pointcut;  
  10. import org.springframework.stereotype.Component;  
  11.    
  12. @Aspect    // for aop  
  13. @Component // for auto scan  
  14. public class DataSourceInterceptor {      
  15.     @Pointcut("execution(public * net.aazj.service..*.getUser(..))")  
  16.     public void dataSourceSlave(){};  
  17.        
  18.     @Before("dataSourceSlave()")  
  19.     public void before(JoinPoint jp) {  
  20.         DataSourceTypeManager.set(DataSources.SLAVE);  
  21.     }  
  22.     // ... ...  
  23. }  

这里我们定义了一个 Aspect 类,我们使用 @Before 来在符合 @Pointcut("execution(public * net.aazj.service..*.getUser(..))") 中的方法被调用之前,调用 DataSourceTypeManager.set(DataSources.SLAVE) 设置了 key 的类型为 DataSources.SLAVE,所以 dataSource 会根据key=DataSources.SLAVE 选择 dataSourceSlave 这个dataSource。所以该方法对于的sql语句会在slave数据库上执行。
 
我们可以不断的扩充 DataSourceInterceptor  这个 Aspect,在中进行各种各样的定义,来为某个service的某个方法指定合适的数据源对应的dataSource。
 
这样我们就可以使用 Spring AOP 的强大功能来,十分灵活进行配置了。
 
6)AbstractRoutingDataSource原理剖析
 
ThreadLocalRountingDataSource继承了AbstractRoutingDataSource,实现其抽象方法protected abstract Object determineCurrentLookupKey(); 从而实现对不同数据源的路由功能。我们从 源码入手分析下其中原理:
[java]  view plain  copy
  1. public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean  
  2. AbstractRoutingDataSource 实现了 InitializingBean 那么spring在初始化该bean时,会调用InitializingBean的接口  
  3. void afterPropertiesSet() throws Exception; 我们看下AbstractRoutingDataSource是如何实现这个接口的:  
  4.    
  5.     @Override  
  6.     public void afterPropertiesSet() {  
  7.         if (this.targetDataSources == null) {  
  8.             throw new IllegalArgumentException("Property 'targetDataSources' is required");  
  9.         }  
  10.         this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());  
  11.         for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {  
  12.             Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());  
  13.             DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());  
  14.             this.resolvedDataSources.put(lookupKey, dataSource);  
  15.         }  
  16.         if (this.defaultTargetDataSource != null) {  
  17.             this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);  
  18.         }  
  19.     }  

targetDataSources 是我们在xml配置文件中注入的 dataSourceMaster 和 dataSourceSlave. afterPropertiesSet方法就是使用注入的
dataSourceMaster 和 dataSourceSlave来构造一个HashMap——resolvedDataSources。方便后面根据 key 从该map 中取得对应的dataSource。
我们在看下 AbstractDataSource 接口中的 Connection getConnection() throws SQLException; 是如何实现的:
    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }
关键在于 determineTargetDataSource(),根据方法名就可以看出,应该此处就决定了使用哪个 dataSource :
[java]  view plain  copy
  1. protected DataSource determineTargetDataSource() {  
  2.     Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");  
  3.     Object lookupKey = determineCurrentLookupKey();  
  4.     DataSource dataSource = this.resolvedDataSources.get(lookupKey);  
  5.     if (dataSource == null && (this.lenientFallback || lookupKey == null)) {  
  6.         dataSource = this.resolvedDefaultDataSource;  
  7.     }  
  8.     if (dataSource == null) {  
  9.         throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");  
  10.     }  
  11.     return dataSource;  
  12. }  

Object lookupKey = determineCurrentLookupKey(); 该方法是我们实现的,在其中获取ThreadLocal中保存的 key 值。获得了key之后,
在从afterPropertiesSet()中初始化好了的resolvedDataSources这个map中获得key对应的dataSource。而ThreadLocal中保存的 key 值
是通过AOP的方式在调用service中相关方法之前设置好的。OK,到此搞定!

3. 总结
本文中我们可以体会到AOP的强大和灵活。
本文使用的是mybatis,其实使用Hibernate也应该是相似的配置。

猜你喜欢

转载自blog.csdn.net/bwh0520/article/details/80058465
今日推荐