diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/MybatisFlexBootstrap.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/MybatisFlexBootstrap.java index 51e65df6..8254335c 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/MybatisFlexBootstrap.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/MybatisFlexBootstrap.java @@ -15,7 +15,7 @@ */ package com.mybatisflex.core; -import com.mybatisflex.core.datasource.RoutingDataSource; +import com.mybatisflex.core.datasource.FlexDataSource; import com.mybatisflex.core.mybatis.FlexConfiguration; import com.mybatisflex.core.mybatis.FlexSqlSessionFactoryBuilder; import org.apache.ibatis.logging.Log; @@ -36,7 +36,6 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; -import java.util.function.Supplier; /** * MybatisFlex 的启动类 @@ -56,10 +55,10 @@ public class MybatisFlexBootstrap { protected final AtomicBoolean started = new AtomicBoolean(false); - protected String environmentId; + protected String environmentId = FlexConsts.NAME; protected TransactionFactory transactionFactory; - protected DataSource dataSource; + protected FlexDataSource dataSource; protected Configuration configuration; protected List> mappers; @@ -67,14 +66,13 @@ public class MybatisFlexBootstrap { protected Class logImpl; private Map, Object> mapperObjects = new ConcurrentHashMap<>(); - private ThreadLocal sessionThreadLocal = new ThreadLocal<>(); /** * 虽然提供了 getInstance,但也允许用户进行实例化, * 用于创建多个 MybatisFlexBootstrap 实例达到管理多数据源的目的 */ - public MybatisFlexBootstrap(String environmentId) { - this.environmentId = environmentId; + public MybatisFlexBootstrap() { + } private static volatile MybatisFlexBootstrap instance; @@ -83,7 +81,7 @@ public class MybatisFlexBootstrap { if (instance == null) { synchronized (MybatisFlexBootstrap.class) { if (instance == null) { - instance = new MybatisFlexBootstrap(FlexConsts.NAME); + instance = new MybatisFlexBootstrap(); } } } @@ -124,7 +122,6 @@ public class MybatisFlexBootstrap { //init sqlSessionFactory this.sqlSessionFactory = new FlexSqlSessionFactoryBuilder().build(configuration); - //init mappers if (mappers != null) { mappers.forEach(configuration::addMapper); @@ -147,10 +144,6 @@ public class MybatisFlexBootstrap { protected SqlSession openSession() { - SqlSession sqlSession = sessionThreadLocal.get(); - if (sqlSession != null) { - return sqlSession; - } return sqlSessionFactory.openSession(configuration.getDefaultExecutorType(), true); } @@ -175,34 +168,6 @@ public class MybatisFlexBootstrap { } - /** - * 执行事务操作,不支持嵌套事务 - * - * @param supplier - * @return false 回滚事务,true 正常执行 - */ - public boolean tx(Supplier supplier) { - SqlSession sqlSession = sqlSessionFactory.openSession(configuration.getDefaultExecutorType()); - boolean success = false; - boolean rollback = true; - try { - sessionThreadLocal.set(sqlSession); - success = supplier.get(); - } catch (Throwable e) { - rollback = false; - sqlSession.rollback(); - } finally { - sessionThreadLocal.remove(); - if (!success && rollback) { - sqlSession.rollback(); - } else if (success) { - sqlSession.commit(); - } - } - return success; - } - - public String getEnvironmentId() { return environmentId; } @@ -226,18 +191,15 @@ public class MybatisFlexBootstrap { } public MybatisFlexBootstrap setDataSource(DataSource dataSource) { - this.dataSource = dataSource; + this.dataSource = new FlexDataSource(FlexConsts.NAME, dataSource); return this; } public MybatisFlexBootstrap addDataSource(String dataSourceKey, DataSource dataSource) { if (this.dataSource == null) { - this.dataSource = new RoutingDataSource(dataSourceKey, dataSource); - } else if (this.dataSource instanceof RoutingDataSource) { - ((RoutingDataSource) this.dataSource).addDataSource(dataSourceKey, dataSource); + this.dataSource = new FlexDataSource(dataSourceKey, dataSource); } else { - this.dataSource = new RoutingDataSource("default", this.dataSource); - ((RoutingDataSource) this.dataSource).addDataSource(dataSourceKey, dataSource); + this.dataSource.addDataSource(dataSourceKey, dataSource); } return this; } diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/datasource/RoutingDataSource.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/datasource/FlexDataSource.java similarity index 59% rename from mybatis-flex-core/src/main/java/com/mybatisflex/core/datasource/RoutingDataSource.java rename to mybatis-flex-core/src/main/java/com/mybatisflex/core/datasource/FlexDataSource.java index b021c6e9..e6174b6a 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/datasource/RoutingDataSource.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/datasource/FlexDataSource.java @@ -17,6 +17,8 @@ package com.mybatisflex.core.datasource; import com.mybatisflex.core.dialect.DbType; import com.mybatisflex.core.dialect.DbTypeUtil; +import com.mybatisflex.core.transaction.TransactionContext; +import com.mybatisflex.core.transaction.TransactionalManager; import com.mybatisflex.core.util.StringUtil; import javax.sql.DataSource; @@ -25,13 +27,16 @@ import java.sql.SQLException; import java.util.HashMap; import java.util.Map; -public class RoutingDataSource extends AbstractDataSource { +public class FlexDataSource extends AbstractDataSource { private final Map dataSourceMap = new HashMap<>(); private final Map dbTypeHashMap = new HashMap<>(); + + private final String defaultDataSourceKey; private final DataSource defaultDataSource; - public RoutingDataSource(String dataSourceKey, DataSource dataSource) { + public FlexDataSource(String dataSourceKey, DataSource dataSource) { + this.defaultDataSourceKey = dataSourceKey; this.defaultDataSource = dataSource; dataSourceMap.put(dataSourceKey, dataSource); dbTypeHashMap.put(dataSourceKey, DbTypeUtil.getDbType(dataSource)); @@ -42,20 +47,55 @@ public class RoutingDataSource extends AbstractDataSource { dbTypeHashMap.put(dataSourceKey, DbTypeUtil.getDbType(dataSource)); } - public DbType getDbType(String dataSourceKey){ + public DbType getDbType(String dataSourceKey) { return dbTypeHashMap.get(dataSourceKey); } @Override public Connection getConnection() throws SQLException { - return getDataSource().getConnection(); + String xid = TransactionContext.getXID(); + if (StringUtil.isNotBlank(xid)) { + String dataSourceKey = DataSourceKey.get(); + if (StringUtil.isBlank(dataSourceKey)) { + dataSourceKey = defaultDataSourceKey; + } + + Connection connection = TransactionalManager.getConnection(xid, dataSourceKey); + if (connection != null) { + return connection; + } else { + connection = getDataSource().getConnection(); + TransactionalManager.hold(xid, dataSourceKey, connection); + return connection; + } + } else { + return getDataSource().getConnection(); + } } + @Override public Connection getConnection(String username, String password) throws SQLException { - return getDataSource().getConnection(username, password); + String xid = TransactionContext.getXID(); + if (StringUtil.isNotBlank(xid)) { + String dataSourceKey = DataSourceKey.get(); + if (StringUtil.isBlank(dataSourceKey)) { + dataSourceKey = defaultDataSourceKey; + } + Connection connection = TransactionalManager.getConnection(xid, dataSourceKey); + if (connection != null) { + return connection; + } else { + connection = getDataSource().getConnection(username, password); + TransactionalManager.hold(xid, dataSourceKey, connection); + return connection; + } + } else { + return getDataSource().getConnection(username, password); + } } + @Override @SuppressWarnings("unchecked") public T unwrap(Class iface) throws SQLException { @@ -70,6 +110,7 @@ public class RoutingDataSource extends AbstractDataSource { return (iface.isInstance(this) || getDataSource().isWrapperFor(iface)); } + private DataSource getDataSource() { DataSource dataSource = defaultDataSource; if (dataSourceMap.size() > 1) { diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/MapperInvocationHandler.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/MapperInvocationHandler.java index ddd9b3ab..4f25bf45 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/MapperInvocationHandler.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/MapperInvocationHandler.java @@ -18,7 +18,7 @@ package com.mybatisflex.core.mybatis; import com.mybatisflex.annotation.UseDataSource; import com.mybatisflex.core.FlexGlobalConfig; import com.mybatisflex.core.datasource.DataSourceKey; -import com.mybatisflex.core.datasource.RoutingDataSource; +import com.mybatisflex.core.datasource.FlexDataSource; import com.mybatisflex.core.dialect.DbType; import com.mybatisflex.core.dialect.DialectFactory; import com.mybatisflex.core.row.RowMapper; @@ -28,7 +28,6 @@ import com.mybatisflex.core.util.StringUtil; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.util.MapUtil; -import javax.sql.DataSource; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.Map; @@ -39,11 +38,11 @@ public class MapperInvocationHandler implements InvocationHandler { private static final Map methodDataSourceKeyMap = new ConcurrentHashMap<>(); private final Object mapper; - private final DataSource dataSource; + private final FlexDataSource dataSource; public MapperInvocationHandler(Object mapper, Configuration configuration) { this.mapper = mapper; - this.dataSource = configuration.getEnvironment().getDataSource(); + this.dataSource = (FlexDataSource) configuration.getEnvironment().getDataSource(); } @@ -67,8 +66,8 @@ public class MapperInvocationHandler implements InvocationHandler { //优先获取用户自己配置的 dbType DbType dbType = DialectFactory.getHintDbType(); if (dbType == null) { - if (dataSourceKey != null && dataSource instanceof RoutingDataSource) { - dbType = ((RoutingDataSource) dataSource).getDbType(dataSourceKey); + if (dataSourceKey != null) { + dbType = dataSource.getDbType(dataSourceKey); } if (dbType == null) { dbType = FlexGlobalConfig.getDefaultConfig().getDbType(); diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/Db.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/Db.java index 29ee62ca..49e047f5 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/Db.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/Db.java @@ -18,13 +18,17 @@ package com.mybatisflex.core.row; import com.mybatisflex.core.FlexGlobalConfig; import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.core.transaction.TransactionContext; +import com.mybatisflex.core.transaction.TransactionalManager; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.util.MapUtil; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; /** * 针对 RowMapper 的静态方法进行封装 @@ -387,4 +391,36 @@ public class Db { public static Page paginate(String tableName, Page page, QueryWrapper queryWrapper) { return invoker().paginate(tableName, page, queryWrapper); } + + + public static boolean tx(Supplier supplier) { + //上一级事务的id,支持事务嵌套 + String prevXID = TransactionContext.getXID(); + try { + String xid = UUID.randomUUID().toString(); + TransactionContext.hold(xid); + boolean success = false; + boolean rollbacked = false; + try { + success = supplier.get(); + } catch (Exception e) { + rollbacked = true; + TransactionalManager.rollback(xid); + e.printStackTrace(); + } finally { + if (success) { + TransactionalManager.commit(xid); + } else if (!rollbacked) { + TransactionalManager.rollback(xid); + } + TransactionContext.release(); + } + return success; + } finally { + //恢复上一级事务 + if (prevXID != null) { + TransactionContext.hold(prevXID); + } + } + } } diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/transaction/TransactionContext.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/transaction/TransactionContext.java new file mode 100644 index 00000000..80692e14 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/transaction/TransactionContext.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.transaction; + + +public class TransactionContext { + + private static final ThreadLocal XID_HOLDER = new ThreadLocal<>(); + + public static String getXID() { + return XID_HOLDER.get(); + } + + public static void release() { + XID_HOLDER.remove(); + } + + public static void hold(String xid) { + XID_HOLDER.set(xid); + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/transaction/TransactionalManager.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/transaction/TransactionalManager.java new file mode 100644 index 00000000..316c89c6 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/transaction/TransactionalManager.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.transaction; + +import org.apache.ibatis.logging.Log; +import org.apache.ibatis.logging.LogFactory; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 事务管理器 + */ +public class TransactionalManager { + + private static final Log log = LogFactory.getLog(TransactionalManager.class); + + //> + private static final ThreadLocal>> CONNECTION_HOLDER + = ThreadLocal.withInitial(ConcurrentHashMap::new); + + public static void hold(String xid, String ds, Connection connection) { + Map> holdMap = CONNECTION_HOLDER.get(); + Map connMap = holdMap.get(xid); + if (connMap == null) { + connMap = new ConcurrentHashMap<>(); + holdMap.put(xid, connMap); + } + + if (connMap.containsKey(ds)) { + return; + } + + try { + connection.setAutoCommit(false); + } catch (SQLException e) { + if (log.isDebugEnabled()) { + log.debug("Error set AutoCommit to false. Cause: " + e); + } + } + } + + + public static Connection getConnection(String xid, String ds) { + Map connections = CONNECTION_HOLDER.get().get(xid); + return connections == null || connections.isEmpty() ? null : connections.get(ds); + } + + + public static void commit(String xid) { + release(xid, true); + } + + public static void rollback(String xid) { + release(xid, false); + } + + + private static void release(String xid, boolean commit) { + Exception exception = null; + Map> holdMap = CONNECTION_HOLDER.get(); + try { + if (holdMap.isEmpty()) { + return; + } + Map connections = holdMap.get(xid); + for (Connection conn : connections.values()) { + try { + if (commit) { + conn.commit(); + } else { + conn.rollback(); + } + } catch (SQLException e) { + exception = e; + } finally { + try { + conn.close(); + } catch (SQLException e) { + //ignore + } + } + } + } finally { + holdMap.remove(xid); + if (holdMap.isEmpty()) { + CONNECTION_HOLDER.remove(); + } + if (exception != null) { + log.error("TransactionalManager.release() is error. cause: " + exception.getMessage(), exception); + } + } + } +} diff --git a/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MultiDataSourceAutoConfiguration.java b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MultiDataSourceAutoConfiguration.java index 4a0cdc00..54e98a2c 100644 --- a/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MultiDataSourceAutoConfiguration.java +++ b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MultiDataSourceAutoConfiguration.java @@ -16,7 +16,7 @@ package com.mybatisflex.spring.boot; import com.mybatisflex.core.datasource.DataSourceBuilder; -import com.mybatisflex.core.datasource.RoutingDataSource; +import com.mybatisflex.core.datasource.FlexDataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -52,13 +52,13 @@ public class MultiDataSourceAutoConfiguration { @ConditionalOnMissingBean public DataSource dataSource() { - RoutingDataSource routingDataSource = null; + FlexDataSource routingDataSource = null; if (dataSourceProperties != null && !dataSourceProperties.isEmpty()) { for (String key : dataSourceProperties.keySet()) { DataSource dataSource = new DataSourceBuilder(dataSourceProperties.get(key)).build(); if (routingDataSource == null) { - routingDataSource = new RoutingDataSource(key, dataSource); + routingDataSource = new FlexDataSource(key, dataSource); } else { routingDataSource.addDataSource(key, dataSource); } diff --git a/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/FlexSqlSessionFactoryBean.java b/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/FlexSqlSessionFactoryBean.java index 945f39c0..06bcf34f 100644 --- a/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/FlexSqlSessionFactoryBean.java +++ b/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/FlexSqlSessionFactoryBean.java @@ -16,6 +16,7 @@ package com.mybatisflex.spring; import com.mybatisflex.core.FlexConsts; +import com.mybatisflex.core.datasource.FlexDataSource; import com.mybatisflex.core.mybatis.FlexConfiguration; import com.mybatisflex.core.mybatis.FlexSqlSessionFactoryBuilder; import com.mybatisflex.core.mybatis.FlexXMLConfigBuilder; @@ -582,7 +583,8 @@ public class FlexSqlSessionFactoryBean extends SqlSessionFactoryBean } targetConfiguration.setEnvironment(new Environment(this.environment, - this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory, dataSource)); + this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory, + dataSource instanceof FlexDataSource ? dataSource : new FlexDataSource(FlexConsts.NAME, dataSource))); if (this.mapperLocations != null) { if (this.mapperLocations.length == 0) {