shushan 2013-06-27
最近搭建架构,碰到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),此种情况用到动态切换,就显得方便很多。。。