`
Donald_Draper
  • 浏览: 951227 次
社区版块
存档分类
最新评论

Spring+Mybatis多数据源的实现

阅读更多
浅谈Spring事务隔离级别:http://www.cnblogs.com/yangy608/archive/2011/06/29/2093478.html
spring里面事务的传播属性和事务隔离级别 :http://blog.csdn.net/it_man/article/details/5074371
第一种方法:创建两个会话工厂
配置如下
<beans>
	<!-- 数据源1 -->
	<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">  
		 <!-- 数据库基本信息配置 -->
		 <property name="url" value="${url}" />  
		 <property name="username" value="${username}" />  
		 <property name="password" value="${password}" />  
		 <property name="driverClassName" value="${driverClassName}" />  
		 <property name="filters" value="${filters}" />  
		<!-- 最大并发连接数 -->
		 <property name="maxActive" value="${maxActive}" />
		 <!-- 初始化连接数量 -->
		 <property name="initialSize" value="${initialSize}" />
		 <!-- 配置获取连接等待超时的时间 -->
		 <property name="maxWait" value="${maxWait}" />
		 <!-- 最小空闲连接数 -->
		 <property name="minIdle" value="${minIdle}" />  
	</bean>  
	<!-- 数据源2 -->
	<bean id="syncDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">  
		 <property name="url" value="${sync.url}" />  
		 <property name="username" value="${sync.username}" />  
		 <property name="password" value="${sync.password}" />  
		 <property name="driverClassName" value="${sync.driverClassName}" />  
		 <property name="filters" value="${filters}" />  
		 <property name="maxActive" value="${maxActive}" />
		 <property name="initialSize" value="${initialSize}" />>
		 <property name="maxWait" value="${maxWait}" />
		 <property name="minIdle" value="${minIdle}" />  
	</bean>  
	<!-- 第一个sqlSessionFactory -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"></property>
		<!-- mapper扫描 -->
		<property name="mapperLocations" value="classpath:mybatis/*/*.xml"></property>
	</bean>
    
	<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg ref="sqlSessionFactory" />
	</bean>
	<!-- 第二个sqlSessionFactory -->
	<bean id="syncSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="syncDataSource" />
		<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"></property>
		<property name="mapperLocations" value="classpath:mybatis/sync/*.xml"></property>
	</bean>		
	<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">   
    		<property name="dataSource" ref="dataSource"></property>
 	</bean>
	<bean id="syncSqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg ref="syncSqlSessionFactory" />
	</bean>
	<!-- <aop:aspectj-autoproxy proxy-target-class="true" />  -->
</beans>

这种方法,在程序中只需要切换sqlSessionTemplate即可,但是这样第二数据源无事务,
容易抛出TransactionSynchronizationManager.unbindResourceIfPossible异常,这也是其缺点
是否可以再添加一个事务管理器?
<bean name="syncTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">   
    		<property name="dataSource" ref="syncDataSource"></property>
</bean>

你可以试试,我测试没问题。
jdbc.properties配置文件
url=jdbc\:mysql\://localhost\:3306/test?useUnicode\=true&characterEncoding\=utf8&characterSetResults\=utf8 
driverClassName=com.mysql.jdbc.Driver
username=donald
password=123456

#sync datasource
sync.url=jdbc\:mysql\://192.168.32.128\:3306/test?useUnicode\=true&characterEncoding\=utf8&characterSetResults\=utf8 
sync.driverClassName=com.mysql.jdbc.Driver
sync.username=donald
sync.password=123456


第二种方法:扩展数据源路由
查看AbstractRoutingDataSource
//抽象数据源路由
public abstract class AbstractRoutingDataSource extends AbstractDataSource
    implements InitializingBean
{

public void afterPropertiesSet()
    {
        if(targetDataSources == null)
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        resolvedDataSources = new HashMap(targetDataSources.size());
        Object lookupKey;
        DataSource dataSource;
	//将配置的多数据源添加到resolvedDataSources中
        for(Iterator iterator = targetDataSources.entrySet().iterator(); iterator.hasNext(); resolvedDataSources.put(lookupKey, dataSource))
        {
            java.util.Map.Entry entry = (java.util.Map.Entry)iterator.next();
            lookupKey = resolveSpecifiedLookupKey(entry.getKey());
            dataSource = resolveSpecifiedDataSource(entry.getValue());
        }

        if(defaultTargetDataSource != null)
	    //默认数据源
            resolvedDefaultDataSource = resolveSpecifiedDataSource(defaultTargetDataSource);
    }
    //数据源key
    protected Object resolveSpecifiedLookupKey(Object lookupKey)
    {
        return lookupKey;
    }
    //获取数据源根据dataSource
    protected DataSource resolveSpecifiedDataSource(Object dataSource)
        throws IllegalArgumentException
    {
        if(dataSource instanceof DataSource)
            return (DataSource)dataSource;
        if(dataSource instanceof String)
	   //从bean容器中获取对应的数据源,(DataSource)beanFactory.getBean(dataSourceName, javax/sql/DataSource);
	   // in BeanFactoryDataSourceLookup.getDataSource(String dataSourceName)
            return dataSourceLookup.getDataSource((String)dataSource);
        else
            throw new IllegalArgumentException((new StringBuilder()).append("Illegal data source value - only [javax.sql.DataSource] and String supported: ").append(dataSource).toString());
    }
    //获取连接
     public Connection getConnection()
        throws SQLException
    {
       //再看determineTargetDataSource
        return determineTargetDataSource().getConnection();
    }
    protected DataSource determineTargetDataSource()
    {
        //获取当前数据源名,这里是关键
        Object lookupKey = determineCurrentLookupKey();
	//获取当前数据源
        DataSource dataSource = (DataSource)resolvedDataSources.get(lookupKey);
        if(dataSource == null && (lenientFallback || lookupKey == null))
	    //如果dataSource为空,则dataSource为默认的数据源resolvedDataSources
            dataSource = resolvedDefaultDataSource;
        if(dataSource == null)
            throw new IllegalStateException((new StringBuilder()).append("Cannot determine target DataSource for lookup key [").append(lookupKey).append("]").toString());
        else
            return dataSource;
    }
    //determineCurrentLookupKey方法,为抽象方法,待子类扩展,这是不是给了我们一种思路
    protected abstract Object determineCurrentLookupKey();
    private Map targetDataSources;//Map<String,DataSource>,key为数据源名,value为DataSource
    private Object defaultTargetDataSource;
    private boolean lenientFallback;
    private DataSourceLookup dataSourceLookup;
    private Map resolvedDataSources;//Map<String,DataSource>,key为数据源名,value为DataSource
    private DataSource resolvedDefaultDataSource;
}

从分析AbstractRoutingDataSource获取数据源得出,想要实现多数据源,只需要扩展AbstractRoutingDataSource,并实现determineCurrentLookupKey方法即可,并在determineCurrentLookupKey方法中切换数据源名即可。
下面实验:
//数据源路由
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
 * 动态切换数据源
 * @author donald
 *
 */
public class MultipleRoutingDataSource extends AbstractRoutingDataSource{
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }

}
//这里我们创建一个数据源上下文句柄,以便切换数据源
/**
 * 数据源上下文
 * @author donald
 *
 */
public  class DataSourceContextHolder {
    public final static String DATA_SOURCE_LOCAL = "dataSource";
    public final static String DATA_SOURCE_SYNC = "syncDataSource";
    //对数据源名,线程隔离
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    
    public static void setDataSourceType(String dataSource) {  
        contextHolder.set(dataSource);  
    }      
    public static String getDataSourceType() {  
        return contextHolder.get();  
    }   
    public static void clearDataSourceType() {  
        contextHolder.remove();  
    }  
}

//配置如下
<beans>
        <!-- 数据源1 -->
	<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">  
		 <!-- 数据库基本信息配置 -->
		 <property name="url" value="${url}" />  
		 <property name="username" value="${username}" />  
		 <property name="password" value="${password}" />  
		 <property name="driverClassName" value="${driverClassName}" />  
		 <property name="filters" value="${filters}" />  
		<!-- 最大并发连接数 -->
		 <property name="maxActive" value="${maxActive}" />
		 <!-- 初始化连接数量 -->
		 <property name="initialSize" value="${initialSize}" />
		 <!-- 配置获取连接等待超时的时间 -->
		 <property name="maxWait" value="${maxWait}" />
		 <!-- 最小空闲连接数 -->
		 <property name="minIdle" value="${minIdle}" />  
	</bean>  
	<!-- 数据源2 -->
	<bean id="syncDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">  
		 <property name="url" value="${sync.url}" />  
		 <property name="username" value="${sync.username}" />  
		 <property name="password" value="${sync.password}" />  
		 <property name="driverClassName" value="${sync.driverClassName}" />  
		 <property name="filters" value="${filters}" />  
		 <property name="maxActive" value="${maxActive}" />
		 <property name="initialSize" value="${initialSize}" />>
		 <property name="maxWait" value="${maxWait}" />
		 <property name="minIdle" value="${minIdle}" />  
	</bean>  
	<!-- 数据源路由 -->
	<bean id="multipleDataSource" class="com.dataSource.MultipleRoutingDataSource">
	        <!-- 默认数据源 -->
		<property name="defaultTargetDataSource" ref="dataSource"/>
                 <!-- 目标数据源 -->
		<property name="targetDataSources">
		    <map>     
			<!-- 注意这里的value是和上面的DataSource的id对应,key要和
						   下面的DataSourceContextHolder中的常量对应 -->
			<entry value-ref="dataSource" key="dataSource"/>
			<entry value-ref="syncDataSource" key="syncDataSource"/>
		    </map>   
		</property>
	</bean>
	<!-- 配置mybatis -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
	         <!-- 这里为multipleDataSource,可以统一管理事务 -->
		<property name="dataSource" ref="multipleDataSource" />
		<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"></property>
		<!-- mapper扫描 -->
		<property name="mapperLocations" value="classpath:mybatis/*/*.xml"></property>
        </bean>
	<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg ref="sqlSessionFactory" />
	</bean>
	<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">   
    		<property name="dataSource" ref="multipleDataSource"></property>
 	</bean>
	<!-- <aop:aspectj-autoproxy proxy-target-class="true" />  -->
</beans>

手动测试
@Controller
@RequestMapping(value="/test")
public class TestController extends BaseController{
	private static Logger log = LoggerFactory.getLogger(TestController.class);
	@Resource(name = "daoSupport")
	private DaoSupport dao;
	
	@SuppressWarnings("unchecked")
	@RequestMapping("/db")
	public void testDbSource(HttpServletResponse response) throws IOException {
		log.debug("=======Into testDbSource==============");
		PageData pd = this.getPageData();
		try {
			DataSourceContextHolder.setDataSourceType(DataSourceContextHolder.DATA_SOURCE_LOCAL);
			List<PageData> lpd = (List<PageData>) dao.findForList("test.list", pd);
			log.info("=============localDao size:"+lpd.size()+","+DataSourceContextHolder.getDataSourceType());
			DataSourceContextHolder.clearDataSourceType();
		} catch (Exception e) {
			log.error(e.getMessage());
			e.printStackTrace();
		}
		try {
			DataSourceContextHolder.setDataSourceType(DataSourceContextHolder.DATA_SOURCE_SYNC);
			List<PageData> lpdTest = (List<PageData>) dao.findForList("test.list", pd);
			log.info("=============syncDao size:"+lpdTest.size()+","+DataSourceContextHolder.getDataSourceType());
			DataSourceContextHolder.clearDataSourceType();
		} catch (Exception e) {
			log.error(e.getMessage());
			e.printStackTrace();
		}
		response.getWriter().write("test");
	}
}

访问http://localhost:8080/r/test/db.do控制台输出
2016-09-21 17:57:13 -40921 [com.controller.test.TestController] INFO    - =============localDao size:5,dataSource
2016-09-21 17:57:13 -40941 [com.controller.test.TestController] INFO    - =============syncDao size:4,syncDataSource
上面的方式是手动切换数据源,下面我们通过Spring AOP实现动态切换数据源:
定义注解
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DbSource {

	String value() default "";

}

定义AOP
/**
 * 根据DAO的DbSource的值,动态切换数据源
 * @author donald
 *
 */
@Aspect
@Component
public class DataSourceAspect {

	private static final Logger log = LoggerFactory.getLogger(DataSourceAspect.class);
        //定义切点
	@Pointcut("@annotation(com.fh.dataSource.DbSource)")
	public void dbSourceAspect() {}
	
/*	@Before("dbSourceAspect()")
	public void doBefore(JoinPoint joinPoint) {
		try {
			System.out.println("Dao-class:" + (joinPoint.getTarget().getClass().getName()));
			System.out.println("DataSource:" + getDbSourceValue(joinPoint));
		} catch (Exception e) {
			// 记录本地异常日志
			logger.error("exception", e.getMessage());
		}
	}*/
        /**
	 * 切换数据源
	 * @param joinPoint
	 */
/*有参数的处理
Object[] args = joinPoint.getArgs();  
	        if(args != null && args.length > 1)  
	        {  
	        	log.info("==============Around-args:"+JsonUtil.toJson(args));
              
	        	obj = joinPoint.proceed(args);
	        }  
	        else{
	        	obj = joinPoint.proceed();
	        }*/
	@Around("dbSourceAspect()")
	public void doAround(ProceedingJoinPoint  joinPoint)  {
		try {
			log.info("=============Dao-class:" + (joinPoint.getTarget().getClass().getName()));
			log.info("=============DataSource:" + getDbSourceValue(joinPoint));
			joinPoint.proceed();
			DataSourceContextHolder.clearDataSourceType();
		} 
		catch(Throwable e){
			log.error("=============Throwable:", e.getMessage());
		}
	}
/**
	 * 切换数据源,当注解方法有返回值的处理情况
	 * @param joinPoint
	 */
/*
	@Around("dbSourceAspect()")
	public Object doAround(ProceedingJoinPoint  joinPoint)  {
		//切入方法返回对象
		Object obj = null;
		try {
			log.debug("=============class:" + (joinPoint.getTarget().getClass().getName()));
			log.debug("=============DataSource:" + getDbSourceValue(joinPoint));
			obj = joinPoint.proceed();
		} 
		catch(Throwable e){
			log.error("=============Throwable:", e.getMessage());
			e.printStackTrace();
		}
		finally{
			DataSourceContextHolder.clearDataSourceType();
		}
		return obj;
	} */
 /**
  * 获取数据源id
  * @param joinPoint
  * @return
  * @throws Exception
  */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static String getDbSourceValue(JoinPoint joinPoint) throws Exception {
		//根据连接点获取class
		String targetName = joinPoint.getTarget().getClass().getName();
		Class targetClass = Class.forName(targetName);
//		DbSource dbSource = (DbSource) targetClass.getAnnotation(DbSource.class);
		//根据连接点获取method
		String methodName = joinPoint.getSignature().getName();
		//根据连接点获取args
		Object[] arguments = joinPoint.getArgs();
		Method[] methods = targetClass.getMethods();
		String dbId ="";
		//获取注解连接点的值
		for (Method method : methods) {
			if (method.getName().equals(methodName)) {
				Class[] clazzs = method.getParameterTypes();
				if (clazzs.length == arguments.length) {
				dbId = method.getAnnotation(DbSource.class).value();
				if(!StringUtils.isBlank(dbId)){
					DataSourceContextHolder.setDataSourceType(dbId);
				}
				else{
					dbId = DataSourceContextHolder.DATA_SOURCE_LOCAL;
					DataSourceContextHolder.setDataSourceType(DataSourceContextHolder.DATA_SOURCE_LOCAL);
				}
				break;
		     }
		  }
		}
		return dbId;
	}
}

测试
@Controller
@RequestMapping(value="/test")
public class TestController extends BaseController{
	private static Logger log = LoggerFactory.getLogger(TestController.class);
	@Resource(name = "daoSupport")
	private DaoSupport dao;
	@SuppressWarnings("unchecked")
	@RequestMapping("/db1")
	@DbSource(DataSourceContextHolder.DATA_SOURCE_LOCAL)
	public void testDbSource1(HttpServletResponse response) throws IOException {
		log.info("=======Into testDbSource1==============");
		PageData pd = this.getPageData();
		try {
			List<PageData> lpd = (List<PageData>) dao.findForList("test.list", pd);
			log.info("=============localDao size:"+lpd.size()+","+DataSourceContextHolder.getDataSourceType());
		} catch (Exception e) {
			log.error(e.getMessage());
			e.printStackTrace();
		}
		response.getWriter().write("db1");
	}
	@SuppressWarnings("unchecked")
	@RequestMapping("/db2")
	@DbSource(DataSourceContextHolder.DATA_SOURCE_SYNC)
	public void testDbSource2(HttpServletResponse response) throws IOException {
		log.info("=======Into testDbSource2==============");
		PageData pd = this.getPageData();
		try {
			List<PageData> lpdTest = (List<PageData>) dao.findForList("test.list", pd);
			log.info("=============synDao size:"+lpdTest.size()+","+DataSourceContextHolder.getDataSourceType());
		} catch (Exception e) {
			log.error(e.getMessage());
			e.printStackTrace();
		}
		response.getWriter().write("db2");
	}
}

访问http://localhost:8080/r/test/db1.do,控制台输出
2016-09-21 17:58:12 -99217 [com.dataSource.DataSourceAspect] INFO    - =============Dao-class:com.controller.test.TestController
2016-09-21 17:58:12 -99220 [com.dataSource.DataSourceAspect] INFO    - =============DataSource:dataSource
2016-09-21 17:58:12 -99220 [com.controller.test.TestController] INFO    - =======Into testDbSource1==============
2016-09-21 17:58:12 -99225 [com.controller.test.TestController] INFO    - =============localDao size:5,dataSource
访问http://localhost:8080/r/test/db2.do,控制台输出
2016-09-21 17:58:16 -104167 [com.dataSource.DataSourceAspect] INFO    - =============Dao-class:com.controller.test.TestController
2016-09-21 17:58:16 -104167 [com.dataSource.DataSourceAspect] INFO    - =============DataSource:syncDataSource
2016-09-21 17:58:16 -104168 [com.controller.test.TestController] INFO    - =======Into testDbSource2==============
2016-09-21 17:58:17 -104180 [com.controller.test.TestController] INFO    - =============synDao size:4,syncDataSource
至此动态切换成功,
从上面可以看出,SqlSessionFactoryBean的获取数据源是通过数据源路由,
我们通过扩展数据源路由来实现,动态切换数据源。

注意:
当动态数据源切换失败时,查看是不是事务因素,要先理解事务,例如你的事务是放在service...,你切换数据库时为了保证事务的完整性,你应该在进入service之前切换掉数据源,因为如果你是在service方法中切换数据源那是不可行的,因为这个时候spring已经打开了一个事务,他会阻止你切换,所以你应在这之前切换,然后进入service方法,.这样spring又给你新切换的数据源加上事务了
0
0
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics