mybatis guice 事务代理切面
在mybatis Guice 事务源码解析的基础上,我们得以自定义事务代理切面
1
单数据源事务
引用计数传播法
2 目前做法,mybatis-guice框架下
单数据源事务
引用计数传播法
3
单数据源事务,支持多第一数据源事务,运行期第二数据源不享受回滚
引用计数传播法
与2的区别在于,3支持第二数据源作为service层开启事务
4 mybatis-guice
多数据源事务
sqlmanager绑定传播法
模型
N=0 | 1 | 2 | 3 | 4 | 5 (2022.7.25) |
service tran |
以con1开启事务 N++ |
以sqlManager1开启事务 N++ |
以sqlManager1 or 2开启事务 N++ |
if(!isSessionInherited1) 以sqlManager1开启事务 |
以sqlManager1 or 2开启事务 N1++ |
mapper1.xxx | 执行 | 执行 | 执行 | 执行 | 执行 |
mapper2.xxx? | 数据源报错 |
以sqlManager2非事务执行 SqlSessionInterceptor finally sqlSession.close |
以sqlManager2非事务执行 SqlSessionInterceptor finally sqlSession.close |
以sqlManager2非事务执行 SqlSessionInterceptor finally sqlSession.close |
以sqlManager2非事务执行
SqlSessionInterceptor finally sqlSession.close |
dao tran |
本打算开启事务 但N==1,退出 N++ |
本打算开启事务 但N==1,退出 N++ |
if(!isSessionInherited2) 以sqlManager2开启事务 |
N1==1,N2==0 N2++ 开启事务 |
|
mapper2.xxx | 数据源报错 |
以sqlManager2非事务执行 SqlSessionInterceptor finally sqlSession.close |
以sqlManager2非事务执行 SqlSessionInterceptor finally sqlSession.close |
以sqlManager2事务执行 |
以sqlManager2事务执行 |
非事务 | 非事务 |
if(!isSessionInherited2) 提交sqlManager2 |
N2==1 提交sqlManager2 |
||
catch rollback? throw |
N!=1 退出 |
N!=1 退出 |
N!=1 退出 |
if(!isSessionInherited2) 回滚mapper2 |
N2==1 回滚mapper2 |
finally | |||||
close? return? |
N-- N!=0 退出 |
N-- N!=0 退出 |
N-- N!=0 退出 |
if(!isSessionInherited2) sqlManager2.close |
N2-- N2==0 sqlManager2.close |
N==1
提交mapper1 |
N==1 提交mapper1 |
N1==1
提交mapper1 |
if(!isSessionInherited1)
提交sqlManager1 |
N1==1 提交mapper1 |
|
catch rollback |
N==1 回滚mapper1 |
N==1 回滚mapper1 |
N==1 回滚mapper1 |
if(!isSessionInherited1) 回滚mapper1 无法回滚mapper2 |
N1==1 回滚mapper1 |
finally | |||||
close |
N-- N==0 return con1 |
N-- N==0 sqlManager1.close |
N-- N==0 s qlManager1.close |
if(!isSessionInherited1) sqlManager1.close |
N1-- N1==0 sqlManager1.close |
*isSessionInherited 借用局部变量,相当聪明
4*
isSessionInherited1=SqlSessionManager1.isManagedSessionStarted=SqlSessionManager1.localSqlSession.get() != null==false
借助非静态threadlocal,ThreadLocal内存泄漏问题实践(三)非静态threadlocal,得以实现多数据源事务嵌套,当然也是不完全版
1 与2 3 4 的根本区别:非静态threadlocal多数据源事务应用
4 与 2 3 的根本区别:在非静态threadlocal基础上,更准确的把握多数据源事务传播,2 3 忽略内层无论第1还是第2数据源的开启事务请求
TransactionalMethodInterceptor实践下来:
(1)
service
@Transactional
public void multi() {
myMapper1.insert(); 以事务1回滚
myMapper2.insert(); 以非事务执行
myDao.multi();
int i=1/0;
}
dao
@Transactional
public void multi() {
myMapper2.insert2(); 以事务2提交
}
(2)
service
@Transactional
public void multi() {
myMapper1.insert(); 以事务1回滚
myMapper2.insert(); 以非事务执行
myDao.multi();
// int i=1/0;
}
dao
@Transactional
public void multi() {
myMapper2.insert2(); 以事务2回滚
int i=1/0;
}
本例使用方案2时,dao层语句是以非事务执行的,故没有回滚
因为dao层在内层,N!=0,不开启事务
(3)
service
@Transactional
public void multi() {
myMapper1.insert(); 以事务1回滚
myMapper2.insert(); 以非事务执行
myDao.multi();
// int i=1/0;
}
dao
// @Transactional
public void multi() {
myMapper2.insert2(); 以非事务执行
int i=1/0;
}
项目实际情况:
第一数据源有事务,第二数据源只有简单的查询,不需要事务,用方案2
import com.google.inject.Injector; import org.apache.ibatis.session.SqlSessionManager; import java.lang.reflect.*; import java.sql.SQLException; public class OdsTransactionProxyFactory implements InvocationHandler { private static final ScefLogbackFactory.ScefLogger logger = ScefLogbackFactory.getLogger(OdsTransactionProxyFactory.class); private static final ThreadLocal<Integer> hasInTransaction = new ThreadLocal<>(); private static volatile SqlSessionManager sqlSessionManager = null; private Object target; public OdsTransactionProxyFactory(Object target){ this.target = target; } private <T> T getBeanFromFactoryGuice(Class<T> c) { Injector injector = CRFGuiceContext.getInjector(); return injector.getInstance(c); } private Class getBeanInjectAnnotationGuice() { return com.google.inject.Inject.class; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Class clProxy = target.getClass(); Field [] fields = clProxy.getDeclaredFields(); for(Field field : fields) { field.setAccessible(true); if(!field.isAnnotationPresent(getBeanInjectAnnotationGuice())) continue; String name = field.getName(); Class fieldType = field.getType(); Object obj = getBeanFromFactoryGuice(fieldType); if(obj != null) field.set(target, obj); } SCEF_DB_TRANSACTIONAL scef_db_transactional = method.getAnnotation(SCEF_DB_TRANSACTIONAL.class); if(scef_db_transactional != null) { return invokeHasTransactional(scef_db_transactional, method, args); } else { return invokeNoTransactional(method, args); } } private Object invokeHasTransactional(SCEF_DB_TRANSACTIONAL scef_db_transactional, Method method, Object[] args) throws Throwable { Boolean readOnly = scef_db_transactional.readOnly(); int isolation = scef_db_transactional.isolation(); try { startTransaction(readOnly, isolation); Object returnValue = method.invoke(target, args); commit(); return returnValue; } catch (InvocationTargetException ie) { Throwable throwable = ie.getTargetException(); Class c1 = throwable.getClass(); Class [] c2 = scef_db_transactional.noRollbackFor(); int sum = 0; for(Class c : c2) { if(c.equals(c1)) sum ++ ; } //logger.error(throwable); if (sum == 0) { rollback(); } else { commit(); } // RuntimeException will popup the default error popup view of ecore throw throwable; } catch (Exception e) { throw new SCEFApplicationException(e); } finally { endTransaction(); } } private Object invokeNoTransactional(Method method, Object[] args) throws Throwable { try { Object returnValue = method.invoke(target, args); return returnValue; } catch (InvocationTargetException e) { Throwable throwable = e.getTargetException(); throw throwable; } } public Object getProxyInstance(){ return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } private void startTransaction(boolean readOnly, int isolation) throws DBException { if (hasInTransaction.get() == null || hasInTransaction.get() == 0) { startTransactionReal(readOnly, isolation); hasInTransaction.set(1); } else { hasInTransaction.set(hasInTransaction.get() + 1); } } private void startTransactionReal(boolean readOnly, int isolation) { SqlSessionManager sqlSessionManager = getOdsSqlSessionManager(); sqlSessionManager.startManagedSession(false); } private SqlSessionManager getOdsSqlSessionManager() { if(sqlSessionManager == null) { synchronized (OdsTransactionProxyFactory.class) { if(sqlSessionManager == null) { OdsTransactionMapper odsTransactionMapper = (OdsTransactionMapper)getBeanFromFactoryGuice(OdsTransactionMapper.class); InvocationHandler lvInvHandler0 = Proxy.getInvocationHandler(odsTransactionMapper); ManagedMapperProvider managedMapperProvider0 = (ManagedMapperProvider) lvInvHandler0; sqlSessionManager = managedMapperProvider0.getSqlSessionManager(); } } } return sqlSessionManager; } private void commit() { if (hasInTransaction.get() == 1) { getOdsSqlSessionManager().commit(); } } private void rollback() { if (hasInTransaction.get() == 1) { getOdsSqlSessionManager().rollback(); } } private void endTransaction() { if (hasInTransaction.get() != null && hasInTransaction.get() > 0) { hasInTransaction.set(hasInTransaction.get() - 1); if (hasInTransaction.get() == 0) { endTransactionReal(); hasInTransaction.remove(); } } } private void endTransactionReal() { getOdsSqlSessionManager().close(); } }
在自己的实践中:
private SqlSessionManager getOdsSqlSessionManager() { if(sqlSessionManager == null) { synchronized (OdsTransactionProxyFactory.class) { if(sqlSessionManager == null) { try { MyMapper1 mapper1 = (MyMapper1) getBeanFromFactoryGuice(MyMapper1.class); InvocationHandler lvInvHandler = Proxy.getInvocationHandler(mapper1); MapperProxy lvT = (MapperProxy) lvInvHandler; Field lvSessField = lvT.getClass().getDeclaredField("sqlSession"); lvSessField.setAccessible(true); sqlSessionManager = (SqlSessionManager) lvSessField.get(lvT); }catch (Exception e) { e.printStackTrace(); } } } } return sqlSessionManager; }
借助:mybatis 动态代理 事务 初探中的方法,由mapper获取sqlSessionManager
类加载的并发,单例模式,静态资源加载 中使用静态内部类加载sqlSessionManager,此部分不进入test git仓库,这里看看就好
5
2020.10.29 运行期修改数据源
DefaultSqlSessionFactory.openSessionFromConnection 调用(本部分代码不入自己的代码库)
this.bindTransactionFactoryType(ScefJdbcTransactionFactory.class);
public class ScefJdbcTransactionFactory implements TransactionFactory {
private DataSourceDictionary dataSourceDictionary;
public void setProperties(Properties props) {
}
public Transaction newTransaction(Connection conn) {
return new JdbcTransaction(conn);
}
public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
DataSourceDictionary dictionary = this.getDataSourceDictionary();
if (dictionary == null) {
throw new RuntimeException("Can not get DataSourceDictionary form Injector");
}
DataSource datasource = dictionary.getDataSource("xxx");
return new JdbcTransaction(datasource, level, autoCommit);
}
无视掉传入的初始DataSource(由provider提供),换成运行期的,即可
((ComboPooledDataSource)((LazyDataSource)dataSource).getDelegateDataSourceForOps()).softResetAllUsers();
6
2022.7.25 多数据源,与mybatis-guice一致
做了实践:
@project_DB_TRANSACTIONAL(noRollbackFor = {projectApplicationEmailSendingException.class}) public projectRequestDto saveCommunication(projectRequestDto dto, projectRequestCommunicationDto requestComm, projectUserDto loggedInUser) throws projectApplicationException, projectOptimisticLockCheckedException { requestComm.setReconFlag(projectConstants.FLAG_N); tranSecondSourceMainMethodWithAnno();
数据源2已提交 if(configuration.getInt("project.xxx") == 3) 不会回滚 throw new RuntimeException(); return save(dto, requestComm, loggedInUser); } @project_DB_TRANSACTIONAL(mapper = MatrixTransactionMapper.class) protected void tranSecondSourceMainMethodWithAnno() { CdsIndexNettingMap cdsIndexNettingMap = cdsIndexNettingMapMatrixMapper.queryForObject(11667L); tranSecondSourceSubMethodWithoutAnno(cdsIndexNettingMap); 数据源2未提交 if(configuration.getInt("project.xxx") == 2) 会回滚 throw new RuntimeException(); } protected void tranSecondSourceSubMethodWithoutAnno(CdsIndexNettingMap cdsIndexNettingMap) { cdsIndexNettingMap.setTableId(cdsIndexNettingMap.getTableId() + " MM"); cdsIndexNettingMapMatrixMapper.update(cdsIndexNettingMap); 数据源2未提交 if(configuration.getInt("project.xxx") == 1) 会回滚 throw new RuntimeException(); }
与mybatis-guice相比:
1 mybatis-guice service与sqlSessionManager绑定恒定,所以service必须注册在private db module中,单个数据源
而本文方式可以允许service两个方法使用不同的数据源事务,所以本文方式可能存在性能问题,因为每次切方法都要重新获取该方法所对应的sqlSessionManager
2 threadlocal N对象是static的,所以无法处理多数据源,方案有
1)把facotry 变成一个数据源单例,像mybatis-guice那样,threadlocal改为非静态,但是这样的话ioc就不能用第一次进来在执行一次的方法了
2)后来采用 threadlocal map,以sqlSessionManager为key➕双检锁(双检锁的另一种形式)