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➕双检锁(双检锁的另一种形式)
浙公网安备 33010602011771号