在单体应用中,事务的使用已经非常方便了。spring提供注解方式直接实现事务。然而在多数据源动态切换场景增加事务则出现数据源无法切换。因为我们在service开启一个事务,则会缓DataSource、SqlSessionFactory、Connection等。再去切换数据源时则直接从缓存取链接,造成数据源无法正常切换的问题。有兴趣的可以查看DataSourceTransactionManager类的源码。
下面则介绍使用JTA(Java Transactional Api)解决这个问题
1.JTA介绍
XA与JTA
XA规范定义了:
1.1.1. TransactionManager : 这个TransactionManager可以通过管理多个ResourceManager来管理多 个Resouce,也就是管理多个数据源
1.1.2. XAResource : 针对数据资源封装的一个接口
1.1.3. 两段式提交 : 多数据源事务提交的机制
JTA : 是Java规范,是XA在Java上的实现
1.2.1. TransactionManager : 常用方法,可以开启,回滚,获取事务. begin(),rollback()...
1.2.2. XAResouce : 资源管理,通过Session来进行事务管理,commit(xid)...
1.2.3. XID : 每一个事务都分配一个特定的XID
- JTA的作用
JTA的作用是同时管理多个数据源的事务管理,满足多数据源情况下的数据一致性。
3. JTA的实现
3.1 使用Atomikos事务管理器 pom文件中引入starter
org.springframework.boot
spring-boot-starter-jta-atomikos
3.2 项目代码改造(动态多数据源部分省略)
在原有多数据源基础上需要修改的部分,DataSource 更改为 MysqlXADataSource,也可以使用DruidXADataSource。自行选择
重写Transaction类,代码如下:
import com.***.common.jdbc.multi.DynamicDataSourceContextHolder;import lombok.extern.slf4j.Slf4j;import org.apache.ibatis.transaction.Transaction;import org.springframework.jdbc.CannotGetJdbcConnectionException;import org.springframework.jdbc.datasource.DataSourceUtils;import javax.sql.DataSource;import java.sql.Connection;import java.sql.SQLException;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ConcurrentMap;import static org.apache.commons.lang3.Validate.notNull;/** * 多数据源切换,支持事务 */@Slf4jpublic class MultiDataSourceTransaction implements Transaction { private final DataSource dataSource; private Connection connection; private String databaseIdentification; private ConcurrentMap connectionMap; private boolean isConnectionTransactional; private boolean autoCommit; public MultiDataSourceTransaction(DataSource dataSource) { notNull(dataSource, "No DataSource specified"); this.dataSource = dataSource; connectionMap = new ConcurrentHashMap<>(); databaseIdentification = DynamicDataSource.getDataSource(); } /** * 获取数据连接 */ @Override public Connection getConnection() throws SQLException { String databaseIdentification = DynamicDataSource.getDataSource(); if (databaseIdentification.equals(databaseIdentification)) { if (connection != null) return connection; else { openConnection(); databaseIdentification = databaseIdentification; return connection; } } else { if (!connectionMap.containsKey(databaseIdentification)) { try { Connection conn = dataSource.getConnection(); connectionMap.put(databaseIdentification, conn); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex); } } return connectionMap.get(databaseIdentification); } } /** * 打开事务 */ private void openConnection() throws SQLException { this.connection = DataSourceUtils.getConnection(this.dataSource); this.autoCommit = this.connection.getAutoCommit(); this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource); if (log.isDebugEnabled()) { log.debug( "JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring"); } } /** * 提交 */ @Override public void commit() throws SQLException { if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) { if (log.isDebugEnabled()) { log.debug("Committing JDBC Connection [" + this.connection + "]"); } this.connection.commit(); for (Connection connection : connectionMap.values()) { connection.commit(); } } } /** * 回滚 */ @Override public void rollback() throws SQLException { if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) { if (log.isDebugEnabled()) { log.debug("Rolling back JDBC Connection [" + this.connection + "]"); } this.connection.rollback(); for (Connection connection : connectionMap.values()) { connection.rollback(); } } } /** * 关闭 */ @Override public void close() throws SQLException { DataSourceUtils.releaseConnection(this.connection, this.dataSource); for (Connection connection : connectionMap.values()) { DataSourceUtils.releaseConnection(connection, this.dataSource); } } @Override public Integer getTimeout() throws SQLException { return null; }}
重写SpringManagedTransactionFactory
import org.apache.ibatis.session.TransactionIsolationLevel;import org.apache.ibatis.transaction.Transaction;import org.mybatis.spring.transaction.SpringManagedTransactionFactory;import javax.sql.DataSource;/** * 支持多数据源切换的Factory */public class MultiDataSourceTransactionFactory extends SpringManagedTransactionFactory { @Override public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) { return new MultiDataSourceTransaction(dataSource); }}
mybatis配置引入此Factory
@Bean@Qualifier("sqlSessionFactory")public SqlSessionFactoryBean sqlSessionFactory(@Autowired DataSource dataSource) throws IOException {SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSource); // 引入factory sqlSessionFactoryBean.setTransactionFactory(new MultiDataSourceTransactionFactory());org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();configuration.setMapUnderscoreToCamelCase(true);configuration.addInterceptor(new PageInterceptor());sqlSessionFactoryBean.setConfiguration(configuration);ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));return sqlSessionFactoryBean;}
最后正常使用@Transactional注解实现事务。
PS: JTA使用两段式提交事务方式,事务长度增加,则相对性能,吞吐量会造成一定的影响。
本文主要使用XA分布式事务解决多数据源事务问题,后面继续更新微服务下的分布式事务解决方案及使用过程中踩的坑,先踩坑去了~~~~,项目正常撸码中,更新会比较慢。
![f4a9bb6fb23902f121a6eac85fd38828.png](https://i-blog.csdnimg.cn/blog_migrate/50ed8396074e84b6c2aa2e5be24af9c0.jpeg)