Mybatis+ Spring + JTA

shushan 2013-06-27

多库数据源深入分析(Mybatis+ Spring + JTA)(一)

博客分类:
  • java
多库多数据源AbstractRoutingDataSourceJTA 

最近搭建架构,碰到JTA和事务Transaction的问题,在此做个总结:

架构:Mybatis+ Spring

技术:spring的AbstractRoutingDataSource和JTA

老规矩,先贴代码,在讲原理,刚开始的时候不使用JTA,代码如下:

/** 
* 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();  
    }  
}  
/** 
* 动态数据源 
* 
*/  
public class DynamicDataSource extends AbstractRoutingDataSource {  
  
    @Override  
    protected Object determineCurrentLookupKey() {  
        return DataSourceContextHolder.getDataSourceType();  
    }  
  
}  

spring中配置如下:

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,所以无法切换数据源。

问题解决思路:通过上面的源代码

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,代码如下:

/** 
 * 上下文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();  
    }  
}  
/** 
 * 动态切换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;  
    }  
  
}  
//每一个DAO由继承SqlSessionDaoSupport全部改为DynamicSqlSessionDaoSupport  
public class xxxDaoImpl extends DynamicSqlSessionDaoSupport implements xxxDao {  
  
    public int insertUser(User user) {  
          
        return this.getSqlSession().insert("xxx.xxxDao.insertUser", user);  
    }  
  
}  

spring配置如下:

@Transactional  
    public void testXA() {  
  
        ContextHolder.setContext("ds1");  
  
        xxxDao.insertUser(user);  
  
          
        ContextHolder.setContext("ds2");  
  
        xxxDao.insertUser(user);  
          
          
    }  

通过Service代码,每个DAO访问都会调用getSqlSession()方法,此时就会调用DynamicSqlSessionDaoSupport的如下代码:

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),此种情况用到动态切换,就显得方便很多。。。

相关推荐