Mybatis+ Spring + JTA
多库数据源深入分析(Mybatis+ Spring + JTA)(一)博客分类:
[*]java
多库多数据源AbstractRoutingDataSourceJTA
最近搭建架构,碰到JTA和事务Transaction的问题,在此做个总结:
架构:Mybatis+ Spring
技术:spring的AbstractRoutingDataSource和JTA
老规矩,先贴代码,在讲原理,刚开始的时候不使用JTA,代码如下:
Java代码
[*]/**
[*]* DataSource上下文句柄,通过此类设置需要访问的对应数据源
[*]*
[*]*/
[*]public class DataSourceContextHolder {
[*]
[*] /**
[*] * DataSource上下文,每个线程对应相应的数据源key
[*] */
[*] public static final ThreadLocal contextHolder = new ThreadLocal();
[*]
[*] public static void setDataSourceType(String dataSourceType)
[*] {
[*] contextHolder.set(dataSourceType);
[*] }
[*]
[*] public static String getDataSourceType()
[*] {
[*] return contextHolder.get();
[*] }
[*]
[*] public static void clearDataSourceType()
[*] {
[*] contextHolder.remove();
[*] }
[*]}
Java代码
[*]/**
[*]* 动态数据源
[*]*
[*]*/
[*]public class DynamicDataSource extends AbstractRoutingDataSource {
[*]
[*] @Override
[*] protected Object determineCurrentLookupKey() {
[*] return DataSourceContextHolder.getDataSourceType();
[*] }
[*]
[*]}
spring中配置如下:
Xml代码
[*]<!-- 配置数据源 -->
[*] <bean id="ds1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"
[*] lazy-init="false">
[*] <property name="driverClassName" value="${jdbc.ds1.driverClassName}" />
[*] <property name="url" value="${jdbc.ds1.url}" />
[*] <property name="username" value="${jdbc.ds1.username}" />
[*] <property name="password" value="${jdbc.ds1.password}" />
[*] <property name="initialSize" value="5" />
[*] <property name="maxActive" value="10" />
[*] <property name="maxWait" value="60000" />
[*] <property name="poolPreparedStatements" value="true" />
[*] </bean>
[*]
[*] <bean id="ds2" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"
[*] lazy-init="false">
[*] <property name="driverClassName" value="${jdbc.ds2.driverClassName}" />
[*] <property name="url" value="${jdbc.ds2.url}" />
[*] <property name="username" value="${jdbc.ds2.username}" />
[*] <property name="password" value="${jdbc.ds2.password}" />
[*] <property name="initialSize" value="5" />
[*] <property name="maxActive" value="10" />
[*] <property name="maxWait" value="60000" />
[*] <property name="poolPreparedStatements" value="true" />
[*] </bean>
[*]
[*] <!-- 动态数据源 -->
[*] <bean id="dataSource" class="xxx.DynamicDataSource">
[*] <property name="targetDataSources">
[*] <map>
[*] <entry key="ds1" value-ref="ds1" />
[*] <entry key="ds2" value-ref="ds2" />
[*] </map>
[*] </property>
[*] <property name="defaultTargetDataSource" ref="ds1" />
[*]
[*] </bean>
[*]
[*] <!-- 事务管理 -->
[*] <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
[*] <property name="dataSource" ref="dataSource" />
[*] </bean>
[*] <tx:annotation-driven/>
[*]
[*] <!-- myBatis配置 -->
[*] <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
[*] <property name="configLocation" value="classpath:mybatis-config.xml" />
[*] <property name="dataSource" ref="dataSource" />
[*] </bean>
[*]
[*]<span style="white-space: pre;"> </span><!-- DAO层由 MapperScannerConfigurer自动生成mapper bean -->
[*] <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
[*] <property name="basePackage" value="xxx.mapper" />
[*] </bean>
因为每个Service目前只可能访问一个DataSource,所以在调用Service的时候,调用DataSourceContextHolder.setDataSourceType(key)(key可以为ds1,ds2),
就可以动态切换数据源了(当然最好用AOP思想,技术上spring + AspectJ,在每个Service需要的方法切上一刀),
而且对于spring的@Transactional事务管理是起作用的
OK,按照这种模式,如果Service可能访问多个库,就将DataSourceTransactionManager换成JtaTransactionManager
Xml代码
[*]<bean id="transactionManager"
[*] class="org.springframework.transaction.jta.JtaTransactionManager" />
[*] <tx:annotation-driven transaction-manager="transactionManager" />
当然,Datasource换成JNDI获取
Xml代码
[*]<!-- 创建数据源。 -->
[*] <bean id="ds1" class="org.springframework.jndi.JndiObjectFactoryBean">
[*] <property name="jndiName">
[*] <value>ds1</value>
[*] </property>
[*] <property name="resourceRef">
[*] <value>true</value>
[*] </property>
[*] </bean>
[*] <bean id="ds2" class="org.springframework.jndi.JndiObjectFactoryBean">
[*] <property name="jndiName">
[*] <value>ds2</value>
[*] </property>
[*] <property name="resourceRef">
[*] <value>true</value>
[*] </property>
[*] </bean>
在spring的@Transactional事务管理中,那是死活无法切换数据源
接上篇,为什么此种模式下,在spring托管CMT管理的JTA事务中,无法切换数据源,忙活了好久,对着日志流程和源代码,貌似问题出现在下面的代码中:
Java代码
[*]org.mybatis.spring .SqlSessionUtils
[*]
[*]public static SqlSession getSqlSession方法:
[*]
[*]
[*]SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
[*]
[*]//7.当前在事务中,且session的holder存在,则取得当前事务的session
[*]if (holder != null && holder.isSynchronizedWithTransaction()) {
[*]
[*] if (logger.isDebugEnabled()) {
[*] logger.debug("Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");
[*] }
[*]
[*] return holder.getSqlSession();
[*] }
[*]
[*]
[*]if (logger.isDebugEnabled()) {
[*] logger.debug("Creating SqlSession with JDBC Connection [" + conn + "]");
[*]}
[*]
[*]//1.创建SqlSession
[*]SqlSession session = sessionFactory.openSession(executorType, conn);
[*]
[*]//2.判断当前有事务
[*]if (TransactionSynchronizationManager.isSynchronizationActive()) {
[*] if (logger.isDebugEnabled()) {
[*] logger.debug("Registering transaction synchronization for SqlSession [" + session + "]");
[*] }
[*]
[*]//3.创建当前session的holder
[*] holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
[*]
[*]//4.将session的holder注册到事务中
[*] TransactionSynchronizationManager.bindResource(sessionFactory, holder);
[*]TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
[*] holder.setSynchronizedWithTransaction(true);
[*] holder.requested();
[*]
[*]//5.(8.)执行sql。。。。
[*]
[*]public static void closeSqlSession方法:
[*]SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
[*]
[*]//6.(9.)释放掉当前事务的session
[*]if ((holder != null) && (holder.getSqlSession() == session)) {
[*] if (logger.isDebugEnabled()) {
[*]logger.debug("Releasing transactional SqlSession [" + session + "]");
[*] }
[*]holder.released();
[*]
[*]
[*]public void beforeCommit(boolean readOnly) 方法:
[*]
[*]//10.session提交
[*]if (TransactionSynchronizationManager.isActualTransactionActive()) {
[*] try {
[*] if (logger.isDebugEnabled()) {
[*] logger.debug("Transaction synchronization committing SqlSession [" + this.holder.getSqlSession() + "]");
[*] }
[*] this.holder.getSqlSession().commit();
[*]
[*]
[*]public void afterCompletion(int status) 方法:
[*]
[*]//11.解除事务绑定的session并关闭
[*] if (!this.holder.isOpen()) {
[*] TransactionSynchronizationManager.unbindResource(this.sessionFactory);
[*] try {
[*] if (logger.isDebugEnabled()) {
[*] logger.debug("Transaction synchronization closing SqlSession [" + this.holder.getSqlSession() + "]");
[*] }
[*] this.holder.getSqlSession().close();
在事务中,mybatis操作两个数据库的步骤流程:
1.创建SqlSession--第一个DAO,操作第一个DB
2.判断当前有事务
3.创建当前session的holder
4.将当前session的sessionFacotry的holder注册到事务中
5.执行sql。。。。
6.holder释放掉当前事务的session
7.当前在事务中,且sessionFactory的holder存在,则取得当前事务的session--第二个DAO,操作第二个DB
8.执行sql。。。。
9.释放掉当前事务的session
10.session提交
11.解除事务绑定的sessionFactory并关闭
可以知道在操作第二个DAO的时候取得的是,在事务中绑定的第一个SqlSession,整个Service用同一个SqlSession,所以无法切换数据源。
问题解决思路:通过上面的源代码
Java代码
[*]SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
[*]
[*]
[*]/4.将session的holder注册到事务中
[*]TransactionSynchronizationManager.bindResource(sessionFactory, holder);
[*]ransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
[*] holder.setSynchronizedWithTransaction(true);
可以知道,事务绑定的是mybatis的当前SqlSessionFactory,如果SqlSessionFactory变了,则事务TransactionSynchronizationManager通过SqlSessionFactory(getResource(sessionFactory))获取
的SqlSessionHolder必定不是上一个事务中的,即holder.isSynchronizedWithTransaction()为false。
由此,可以找出一个方法解决,动态切换SqlSessionFactory
OK,代码如下:
Java代码
[*]/**
[*] * 上下文Holder
[*] *
[*] */
[*]@SuppressWarnings("unchecked")
[*]public class ContextHolder<T> {
[*]
[*] private static final ThreadLocal contextHolder = new ThreadLocal();
[*]
[*] public static <T> void setContext(T context)
[*] {
[*] contextHolder.set(context);
[*] }
[*]
[*] public static <T> T getContext()
[*] {
[*] return (T) contextHolder.get();
[*] }
[*]
[*] public static void clearContext()
[*] {
[*] contextHolder.remove();
[*] }
[*]}
Java代码
[*]/**
[*] * 动态切换SqlSessionFactory的SqlSessionDaoSupport
[*] *
[*] * @see org.mybatis.spring.support.SqlSessionDaoSupport
[*] */
[*]public class DynamicSqlSessionDaoSupport extends DaoSupport {
[*]
[*] private Map<Object, SqlSessionFactory> targetSqlSessionFactorys;
[*]
[*] private SqlSessionFactory defaultTargetSqlSessionFactory;
[*]
[*] private SqlSession sqlSession;
[*]
[*] private boolean externalSqlSession;
[*]
[*] @Autowired(required = false)
[*] public final void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
[*] if (!this.externalSqlSession) {
[*] this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
[*] }
[*] }
[*]
[*] @Autowired(required = false)
[*] public final void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
[*] this.sqlSession = sqlSessionTemplate;
[*] this.externalSqlSession = true;
[*] }
[*]
[*] /**
[*] * Users should use this method to get a SqlSession to call its statement
[*] * methods This is SqlSession is managed by spring. Users should not
[*] * commit/rollback/close it because it will be automatically done.
[*] *
[*] * @return Spring managed thread safe SqlSession
[*] */
[*] public final SqlSession getSqlSession() {
[*] SqlSessionFactory targetSqlSessionFactory = targetSqlSessionFactorys.get(ContextHolder
[*] .getContext());
[*] if (targetSqlSessionFactory != null) {
[*] setSqlSessionFactory(targetSqlSessionFactory);
[*] } else if (defaultTargetSqlSessionFactory != null) {
[*] setSqlSessionFactory(defaultTargetSqlSessionFactory);
[*] }
[*] return this.sqlSession;
[*] }
[*]
[*] /**
[*] * {@inheritDoc}
[*] */
[*] protected void checkDaoConfig() {
[*] Assert.notNull(this.sqlSession,
[*] "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
[*] }
[*]
[*] public Map<Object, SqlSessionFactory> getTargetSqlSessionFactorys() {
[*] return targetSqlSessionFactorys;
[*] }
[*]
[*] /**
[*] * Specify the map of target SqlSessionFactory, with the lookup key as key.
[*] * @param targetSqlSessionFactorys
[*] */
[*] public void setTargetSqlSessionFactorys(Map<Object, SqlSessionFactory> targetSqlSessionFactorys) {
[*] this.targetSqlSessionFactorys = targetSqlSessionFactorys;
[*] }
[*]
[*] public SqlSessionFactory getDefaultTargetSqlSessionFactory() {
[*] return defaultTargetSqlSessionFactory;
[*] }
[*]
[*] /**
[*] * Specify the default target SqlSessionFactory, if any.
[*] * @param defaultTargetSqlSessionFactory
[*] */
[*] public void setDefaultTargetSqlSessionFactory(SqlSessionFactory defaultTargetSqlSessionFactory) {
[*] this.defaultTargetSqlSessionFactory = defaultTargetSqlSessionFactory;
[*] }
[*]
[*]}
Java代码
[*]//每一个DAO由继承SqlSessionDaoSupport全部改为DynamicSqlSessionDaoSupport
[*]public class xxxDaoImpl extends DynamicSqlSessionDaoSupport implements xxxDao {
[*]
[*] public int insertUser(User user) {
[*]
[*] return this.getSqlSession().insert("xxx.xxxDao.insertUser", user);
[*] }
[*]
[*]}
spring配置如下:
Xml代码
[*]<!-- 创建数据源。 -->
[*] <bean id="ds1" class="org.springframework.jndi.JndiObjectFactoryBean">
[*] <property name="jndiName">
[*] <value>testxa</value>
[*] </property>
[*] <property name="resourceRef">
[*] <value>true</value>
[*] </property>
[*] </bean>
[*] <bean id="ds2" class="org.springframework.jndi.JndiObjectFactoryBean">
[*] <property name="jndiName">
[*] <value>testxa1</value>
[*] </property>
[*] <property name="resourceRef">
[*] <value>true</value>
[*] </property>
[*] </bean>
[*]
[*]
[*]<!-- sqlSessionFactory工厂 -->
[*] <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
[*] <property name="dataSource" ref="ds1" />
[*] <property name="configLocation" value="classpath:config/mybatis-config.xml" />
[*] </bean>
[*] <bean id="sqlSessionFactory1" class="org.mybatis.spring.SqlSessionFactoryBean">
[*] <property name="dataSource" ref="ds2" />
[*] <property name="configLocation" value="classpath:config/mybatis-config.xml" />
[*] </bean>
[*]
[*]<span style="white-space: pre;"> </span><!-- 动态切换SqlSessionFactory -->
[*] <bean id="dynamicSqlSessionDaoSupport" class="com.suning.cmp.common.multidatasource.DynamicSqlSessionDaoSupport">
[*] <property name="targetSqlSessionFactorys">
[*] <map value-type="org.apache.ibatis.session.SqlSessionFactory">
[*] <entry key="ds1" value-ref="sqlSessionFactory" />
[*] <entry key="ds2" value-ref="sqlSessionFactory1" />
[*] </map>
[*] </property>
[*] <property name="defaultTargetSqlSessionFactory" ref="sqlSessionFactory" />
[*] </bean>
[*]
[*] <bean id="xxxDao" class="xxx.xxxDaoImpl" parent="dynamicSqlSessionDaoSupport"></bean>
Service测试代码如下:
Java代码
[*]@Transactional
[*] public void testXA() {
[*]
[*] ContextHolder.setContext("ds1");
[*]
[*] xxxDao.insertUser(user);
[*]
[*]
[*] ContextHolder.setContext("ds2");
[*]
[*] xxxDao.insertUser(user);
[*]
[*]
[*] }
通过Service代码,每个DAO访问都会调用getSqlSession()方法,此时就会调用DynamicSqlSessionDaoSupport的如下代码:
Java代码
[*]public final SqlSession getSqlSession() {
[*] SqlSessionFactory targetSqlSessionFactory = targetSqlSessionFactorys.get(ContextHolder
[*] .getContext());
[*] if (targetSqlSessionFactory != null) {
[*] setSqlSessionFactory(targetSqlSessionFactory);
[*] } else if (defaultTargetSqlSessionFactory != null) {
[*] setSqlSessionFactory(defaultTargetSqlSessionFactory);
[*] }
[*] return this.sqlSession;
[*] }
起到动态切换SqlSessionFactory(每一个SqlSessionFactory对应一个DB)。OK,到此圆满解决,动态切换和事务这两个问题。
在此,我补充下为什么到用到动态切换,其实每一个SqlSessionFactory对应一个DB,而关于此DB操作的所有DAO对应此SqlSessionFactory,在Service中不去切换,直接用对应不同SqlSessionFactory
的DAO也可以,此种方式可以参考附件:《Spring下mybatis多数据源配置》
问题就在于,项目中不同DB存在相同的Table,动态可以做到只配置一个DAO,且操作哪个DB是通过路由Routing或者通过什么获取才能知道(延迟到Service时才知道对应哪个DB),此种情况用到动态切换,就显得方便很多。。。
页:
[1]