组件整合之使用Atomikos实现跨库实现分布式事务
背景
在Java后端开发过程中事务控制非常重要,而Spring为我们提供了方便的声明式事务方法@Transactional。但是默认的Spring事务只支持单数据源,而实际上一个系统往往需要写多个数据源,这个时候我们就需要考虑如何通过Spring实现对分布式事务的支持。
对于数据库层面的分布式事务而言,JTA(Java Transaction API,XA的JAVA实现方案)是一个不错的解决方案,通常JTA需要应用服务器的支持。 而Atomikos 是一个为Java平台提供增值服务的并且开源类的事务管理器,主要用于处理跨数据库事务,比如某个指令在A库和B库都有写操作,业务上要求A库和B库的写操作要具有原子性,这时候就可以用到atomikos。
Atomikos分布式事务
Atomikos公司旗下有两款著名的分布事务产品:
- TransactionEssentials:开源的免费产品
- ExtremeTransactions:商业版,需要收费
这两个产品的关系如下图所示:
可以看到,在开源版本中支持JTA/XA、JDBC、JMS的分布式事务。
最简单的情况下,你只需要引入如下依赖:
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-jdbc</artifactId>
<version>4.0.6</version>
</dependency>
atomikos也支持与spring事务整合。spring事务管理器的顶级抽象是PlatformTransactionManager接口,其提供了个重要的实现类:
- DataSourceTransactionManager:用于实现本地事务
- JTATransactionManager:用于实现分布式事务
显然,在这里,我们需要配置的是JTATransactionManager。
下面的代码片段了,演示了与spring事务整合后的分布式事务的案例代码。假设有两个mybatis映射器接口UserMapper和AccountMapper,操作不同的库。
public class JTAService {
//操作db_user库
@Autowired
private UserMapper userMapper;
//操作db_account库
@Autowired
private AccountMapper accountMapper;
@Transactional
public void insert() {
User user = new User();
user.setName("wangxiaoxiao");
userMapper.insert(user);
//模拟异常,spring回滚后,db_user库中user表中也不会插入记录
Account account = new Account();
account.setUserId(user.getId());
account.setMoney(123456789);
accountMapper.insert(account);
}
}
可以发现分布式事务的逻辑,与操作单库事务基本上是完全相同的,底层的复杂逻辑对应用程序开发者来说完全屏蔽。
快速入门
1、依赖
<!--spring-tx-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- jta -->
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>
<!--atomikos-->
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-jdbc</artifactId>
<version>4.0.6</version>
</dependency>
2、数据源配置
application.properties
jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
jdbc.username=root
jdbc.password=root
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc2.url=jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
jdbc2.username=root
jdbc2.password=root
jdbc2.driverClassName=com.mysql.jdbc.Driver
AppPropertiesConfig.java
@Data
@Configuration
@PropertySource("classpath:application.properties")
public class AppPropertiesConfig {
@Value("${jdbc.url}")
private String jdbcUrl;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Value("${jdbc.driverClassName}")
private String driverClassName;
}
AppPropertiesConfig2.java
@Data
@Configuration
@PropertySource("classpath:application.properties")
public class AppPropertiesConfig2 {
@Value("${jdbc2.url}")
private String jdbcUrl;
@Value("${jdbc2.username}")
private String userName;
@Value("${jdbc2.password}")
private String password;
@Value("${jdbc2.driverClassName}")
private String driverClassName;
}
@Bean("springBuiltDataSource")
public AtomikosDataSourceBean springBuiltDataSource(AppPropertiesConfig propertiesConfig) throws SQLException {
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
mysqlXaDataSource.setUrl(propertiesConfig.getJdbcUrl());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXaDataSource.setPassword(propertiesConfig.getPassword());
mysqlXaDataSource.setUser(propertiesConfig.getUserName());
// 将本地事务注册到创 Atomikos全局事务
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("dataSourceOne");
xaDataSource.setMinPoolSize(10);
xaDataSource.setMaxPoolSize(1000);
xaDataSource.setMaxLifetime(20000);
xaDataSource.setBorrowConnectionTimeout(30);
xaDataSource.setLoginTimeout(30);
xaDataSource.setMaintenanceInterval(60);
xaDataSource.setMaxIdleTime(60);
xaDataSource.setTestQuery("select now()");
return xaDataSource;
}
@Bean("springBuiltDataSource2")
public AtomikosDataSourceBean springBuiltDataSource2(AppPropertiesConfig2 propertiesConfig2) throws SQLException {
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
mysqlXaDataSource.setUrl(propertiesConfig2.getJdbcUrl());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXaDataSource.setPassword(propertiesConfig2.getPassword());
mysqlXaDataSource.setUser(propertiesConfig2.getUserName());
// 将本地事务注册到创 Atomikos全局事务
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("dataSourceTwo");
xaDataSource.setMinPoolSize(10);
xaDataSource.setMaxPoolSize(1000);
xaDataSource.setMaxLifetime(20000);
xaDataSource.setBorrowConnectionTimeout(30);
xaDataSource.setLoginTimeout(30);
xaDataSource.setMaintenanceInterval(60);
xaDataSource.setMaxIdleTime(60);
xaDataSource.setTestQuery("select now()");
return xaDataSource;
}
@Bean("jdbcTemplateOne")
public JdbcTemplate jdbcTemplateOne(DataSource springBuiltDataSource){
return new JdbcTemplate(springBuiltDataSource);
}
@Bean("jdbcTemplateTwo")
public JdbcTemplate jdbcTemplateTwo(DataSource springBuiltDataSource2){
return new JdbcTemplate(springBuiltDataSource2);
}
3、JtaTransactionManager事务管理器
@Bean("transactionManager")
public PlatformTransactionManager transactionManager() throws SystemException {
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(true);
UserTransactionImp atomikosUserTransaction = new UserTransactionImp();
atomikosUserTransaction.setTransactionTimeout(300);
return new JtaTransactionManager(atomikosUserTransaction, userTransactionManager);
}
4、测试
@Resource(name = "jdbcTemplateOne")
private JdbcTemplate jdbcTemplateOne;
@Resource(name = "jdbcTemplateTwo")
private JdbcTemplate jdbcTemplateTwo;
@Transactional
public void testJta(){
jdbcTemplateOne.update("insert into user_test(id, name) values(100, '老板')");
int a = 1/0;
jdbcTemplateTwo.update("insert into user_test(id, name) values(100, '老板')");
}