多库数据源深入分析(Mybatis+ Spring + JTA)(二)
接上篇,为什么此种模式下,在spring托管CMT管理的JTA事务中,无法切换数据源,忙活了好久,对着日志流程和源代码,貌似问题出现在下面的代码中:
?
org.mybatis.spring .SqlSessionUtilspublic static SqlSession getSqlSession方法:SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); //7.当前在事务中,且session的holder存在,则取得当前事务的sessionif (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.创建SqlSessionSqlSession 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.)释放掉当前事务的sessionif ((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); TransactionSynchronizationManager.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全部改为DynamicSqlSessionDaoSupportpublic class xxxDaoImpl extends DynamicSqlSessionDaoSupport implements xxxDao { public int insertUser(User user) { return this.getSqlSession().insert("xxx.xxxDao.insertUser", user); }}?
?
?
spring配置如下:
?
?
<!-- 创建数据源。 --><bean id="ds1" ref="ds1" /><property name="configLocation" value="classpath:config/mybatis-config.xml" /></bean><bean id="sqlSessionFactory1" ref="ds2" /><property name="configLocation" value="classpath:config/mybatis-config.xml" /></bean><!-- 动态切换SqlSessionFactory --><bean id="dynamicSqlSessionDaoSupport" value-ref="sqlSessionFactory" /><entry key="ds2" value-ref="sqlSessionFactory1" /></map></property><property name="defaultTargetSqlSessionFactory" ref="sqlSessionFactory" /></bean><bean id="xxxDao" parent="dynamicSqlSessionDaoSupport"></bean>?
?
?
Service测试代码如下:
?
?
@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),此种情况用到动态切换,就显得方便很多。。。
?
?
?
?
?
?
?