当项目上遇到读写分离,分表分库,版本升级数据导出等多数据源需求的时候,如何实现动态切换数据源?
原理 Spring提供了AbstractRoutingDataSource 这个datasource,看源码发现:
1 2 3 4 5 6 7 8 @Override public Connection getConnection () throws SQLException { return determineTargetDataSource().getConnection(); } @Override public Connection getConnection (String username, String password) throws SQLException { return determineTargetDataSource().getConnection(username, password); }
实现interface DataSource 的getConnection()方法都是使用determineTargetDataSource()方法返回的connection,再观察这个方法发现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 * Retrieve the current target DataSource. Determines the * {@link #determineCurrentLookupKey() current lookup key}, performs * a lookup in the {@link #setTargetDataSources targetDataSources} map, * falls back to the specified * {@link #setDefaultTargetDataSource default target DataSource} if necessary. * @see #determineCurrentLookupKey() */ protected DataSource determineTargetDataSource () { Assert.notNull(this .resolvedDataSources, "DataSource router not initialized" ); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this .resolvedDataSources.get(lookupKey); if (dataSource == null && (this .lenientFallback || lookupKey == null )) { dataSource = this .resolvedDefaultDataSource; } if (dataSource == null ) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]" ); } return dataSource; }
返回的key是通过determineCurrentLookupKey()这个抽象方法获取的,说明只要重写这个方法返回需要使用的数据源key即可。
思考
但是如何在程序使用过程中自动切换数据源呢?如何方便的自动标记此时需要使用哪个数据源?
实现 spring中的aop功能,可以以切入的方式,在系统使用的过程中动态切换数据源。我们可以对service进行aop实现。但是如何准确方便的标记那些service使用A数据源,哪些service使用B数据源呢? 此时我们可以使用java的annotation 来对使用不用的service进行注解标记。
下面是具体的代码实现:
多数据源配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <util:properties id ="dbConfig" location ="classpath:/config/jdbc.properties" > </util:properties > <beans:bean id ="testDataSorce" class ="org.apache.commons.dbcp.BasicDataSource" lazy-init ="default" autowire ="default" > <beans:property name ="driverClassName" value ="#{dbConfig['driver']}" > </beans:property > <beans:property name ="url" value ="#{dbConfig['test.url']}" > </beans:property > <beans:property name ="username" value ="#{dbConfig['test.user']}" > </beans:property > <beans:property name ="password" value ="#{dbConfig['test.password']}" > </beans:property > <beans:property name ="maxWait" value ="#{dbConfig['maxwait']}" > </beans:property > </beans:bean > <beans:bean id ="devDataSorce" class ="org.apache.commons.dbcp.BasicDataSource" lazy-init ="default" autowire ="default" > <beans:property name ="driverClassName" value ="#{dbConfig['driver']}" > </beans:property > <beans:property name ="url" value ="#{dbConfig['dev.url']}" > </beans:property > <beans:property name ="username" value ="#{dbConfig['dev.user']}" > </beans:property > <beans:property name ="password" value ="#{dbConfig['dev.password']}" > </beans:property > <beans:property name ="maxWait" value ="#{dbConfig['maxwait']}" > </beans:property > </beans:bean > <beans:bean id ="dataSource" class ="com.yyy.web.mess.datasource.DynamicDataSource" lazy-init ="default" autowire ="default" > <beans:property name ="targetDataSources" > <beans:map key-type ="java.lang.String" > <beans:entry key ="testDataSorce" value-ref ="testDataSorce" /> <beans:entry key ="devDataSorce" value-ref ="devDataSorce" /> </beans:map > </beans:property > <beans:property name ="defaultTargetDataSource" ref ="testDataSorce" /> <beans:property name ="defaultDataSourceKey" value ="testDataSorce" /> </beans:bean >
此处可以配置多个,且不同类型的数据源,然后需要将数据源加入到dataSource这个bean下的targetDataSources中。
实现 com.yyy.web.mess.datasource.DynamicDataSource继承自前面所提到的AbstractRoutingDataSource,重写determineCurrentLookupKey方法,返回需要使用的数据源key(上面配置的bean id),具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.yyy.web.mess.datasource;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; * Created by 程祥 on 15/10/21. * Function: */ public class DynamicDataSource extends AbstractRoutingDataSource { private String defaultDataSourceKey; @Override protected Object determineCurrentLookupKey () { String dataSource = DataSourceSwitcher.getDataSource(); if (dataSource != null ){ return dataSource; } return defaultDataSourceKey; } public String getDefaultDataSourceKey () { return defaultDataSourceKey; } public void setDefaultDataSourceKey (String defaultDataSourceKey) { this .defaultDataSourceKey = defaultDataSourceKey; } }
使用DataSourceSwitcher单开一个线程,记录当前使用的数据源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.yyy.web.mess.datasource;import org.apache.commons.lang.StringUtils; * Created by 程祥 on 15/10/21. * Function: */ public class DataSourceSwitcher { @SuppressWarnings ("rawtypes" ) private static final ThreadLocal contextHolder = new ThreadLocal(); @SuppressWarnings ("unchecked" ) public static void setDataSource (String dataSource) { if (StringUtils.isNotEmpty(dataSource)){ contextHolder.set(dataSource); } } public static String getDataSource () { String currDataSource = (String) contextHolder.get(); return currDataSource; } public static void clearDataSource () { contextHolder.remove(); } }
AOP切入 添加一个advice,在方法执行前设置需要使用的数据源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class AnnotationMultipleDataSourceAdvice implements MethodBeforeAdvice , AfterReturningAdvice ,ThrowsAdvice { public void before (Method method, Object[] args, Object target) throws Throwable { DataSource dataSource = method.getAnnotation(DataSource.class); String datasourceKey=dataSource!=null ? dataSource.value():null ; DataSourceSwitcher.setDataSource(datasourceKey); } public void afterReturning (Object arg0, Method method, Object[] args, Object target) throws Throwable { DataSourceSwitcher.clearDataSource(); } public void afterThrowing (Method method, Object[] args, Object target, Exception ex) throws Throwable { DataSourceSwitcher.clearDataSource(); } }
annotation的实现
1 2 3 4 5 6 @Inherited @Retention (RetentionPolicy.RUNTIME)@Target ({ElementType.METHOD,ElementType.PACKAGE})public @interface DataSource { String value () ; }
aop配置文件设置:
1 2 3 4 5 6 <aop:config > <aop:pointcut id ="serviceMethods" expression ="execution(* com.yyy.web.mess.service..*(..))" /> <aop:advisor advice-ref ="annotationMultipleDataSourceAdvice" pointcut-ref ="serviceMethods" /> </aop:config > <beans:bean id ="annotationMultipleDataSourceAdvice" class ="com.yyy.web.mess.datasource.impl.AnnotationMultipleDataSourceAdvice" > </beans:bean >
至此,所有配置及实现均已完成。
使用 使用的时候只需要在对用的service方法前加上对应的key,mybatis在请求数据源的时候就会根据当前数据源去执行对应操作。如果不设置,则使用的是默认数据源~
1 2 3 4 5 6 7 8 9 10 11 * Created by 程祥 on 15/10/21. * Function: */ public interface OrderDevService { @DataSource ("devDataSorce" ) int insertDatas2Dev (List<Order> orders) throws Exception ; @DataSource ("devDataSorce" ) List<Order> selectTest () throws Exception ; }
总结 通过annotation能够方便且清晰的告知当前需要使用哪个数据源,使用aop通过解析annotation的value,动态的切换数据源。这样在需要多数据源系统中能够方便高效的编写业务逻辑代码。