1.what is atomikos
Atomikos It is a lightweight distributed transaction manager that implements Java Transaction API (JTA)Standard, can be easily combined with Spring Boot Integration supports cross-node global transactions in microservice scenarios. Atomikos The company’s official website is: https://www.atomikos.com/。 Its most famous product is the transaction manager. The product is divided into two versions:
- TransactionEssentials: Open source free products
- ExtremeTransactions: There is a fee for using the commercial version.
2.Environment setup
First mysql database
docker run --name docker-mysql -e MYSQL_ROOT_PASSWORD=123456 -p 3333:3306 -d mysql
the second mysql database
docker run --name docker-mysql-2 -e MYSQL_ROOT_PASSWORD=123456 -p 3334:3306 -d mysql
Initialization data
create database demo;
create table user_info
(
user_id varchar(64) not null primary key,
username varchar(100) null ,
age int(3) null ,
gender tinyint(1) null ,
remark varchar(255) null ,
create_time datetime null ,
create_id varchar(64) null ,
update_time datetime null ,
update_id varchar(64) null ,
enabled tinyint(1) default 1 null
);
illustrate
msyql username:root
mysql password:123456
3.Project code
Experimental purpose: to achieve 2 mysql Distributed transaction management of data, either all succeed, or it will roll off as long as one fails.
pom..xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springboot-demo</artifactId>
<groupId>com.et</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>atomikos</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jta-atomikos -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
<!--<version>5.1.46</version>-->
<!--<scope>runtime</scope>-->
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.2</version>
</dependency>
</dependencies>
</project>
mapper
Create 2 mapper Connect to different databases
package com.et.atomikos.mapper1;
import org.apache.catalina.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
public interface UserInfoMapper1 {
// query
@Select("SELECT * FROM user_info WHERE username = #{username}")
User findByName(@Param("username") String username);
// add
@Insert("INSERT INTO user_info(user_id,username, age) VALUES(#{userId},#{username}, #{age})")
int insert(@Param("userId") String userId,@Param("username") String username, @Param("age") Integer age);
}
package com.et.atomikos.mapper2;
import org.apache.catalina.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
public interface UserInfoMapper2 {
// query
@Select("SELECT * FROM user_info WHERE username = #{username}")
User findByName(@Param("username") String username);
// add
@Insert("INSERT INTO user_info(user_id,username, age) VALUES(#{userId},#{username}, #{age})")
int insert(@Param("userId") String userId,@Param("username") String username, @Param("age") Integer age);
}
service
Create 2 service, Use different mapper
package com.et.atomikos.mapper1;
import com.et.atomikos.mapper1.UserInfoMapper1;
import com.et.atomikos.mapper2.UserInfoMapper2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ManyService1 {
@Autowired
private UserInfoMapper1 userInfoMapper1;
@Autowired
private UserInfoMapper2 userInfoMapper2;
@Transactional
public int insert(String userId,String username, Integer age) {
int insert = userInfoMapper1.insert(userId,username, age);
int i = 1 / age;// if age is zero ,then a error will be happened.
return insert;
}
@Transactional
public int insertDb1AndDb2(String userId,String username, Integer age) {
int insert = userInfoMapper1.insert(userId,username, age);
int insert2 = userInfoMapper2.insert(userId,username, age);
int i = 1 / age;// if age is zero ,then a error will be happened.
return insert + insert2;
}
}
package com.et.atomikos.mapper2;
import com.et.atomikos.mapper2.UserInfoMapper2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ManyService2 {
@Autowired
private UserInfoMapper2 userInfoMapper2;
@Transactional
public int insert(String userId,String username, Integer age) {
int i = userInfoMapper2.insert(userId,username, age);
System.out.println("userInfoMapper2.insert end :" + null);
int a = 1 / 0;//touch a error
return i;
}
}
config
Initialize data source 1 and data source 2
package com.et.atomikos.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "spring.datasource.test1")
public class DBConfig1 {
// @Value("${mysql.datasource.test1.jdbcurl}")
//@Value("${jdbcurl}")
private String jdbcurl;
//private String url;
private String username;
private String password;
private int minPoolSize;
private int maxPoolSize;
private int maxLifetime;
private int borrowConnectionTimeout;
private int loginTimeout;
private int maintenanceInterval;
private int maxIdleTime;
private String testQuery;
}
package com.et.atomikos.config;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.mysql.cj.jdbc.MysqlXADataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.sql.SQLException;
/**
* @author liuhaihua
* @version 1.0
* @ClassName MyBatisConfig1
* @Description todo
* @date 2024年04月18日 13:37
*/
@Configuration
@MapperScan(basePackages = "com.et.atomikos.mapper1", sqlSessionTemplateRef = "test1SqlSessionTemplate")
public class MyBatisConfig1 {
@Bean(name = "test1DataSource") //test1DataSource
public DataSource testDataSource(DBConfig1 testConfig) throws SQLException {
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
//mysqlXaDataSource.setUrl(testConfig.getUrl());
mysqlXaDataSource.setUrl(testConfig.getJdbcurl());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXaDataSource.setPassword(testConfig.getPassword());
mysqlXaDataSource.setUser(testConfig.getUsername());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("test1DataSource");
xaDataSource.setMinPoolSize(testConfig.getMinPoolSize());
xaDataSource.setMaxPoolSize(testConfig.getMaxPoolSize());
xaDataSource.setMaxLifetime(testConfig.getMaxLifetime());
xaDataSource.setBorrowConnectionTimeout(testConfig.getBorrowConnectionTimeout());
xaDataSource.setLoginTimeout(testConfig.getLoginTimeout());
xaDataSource.setMaintenanceInterval(testConfig.getMaintenanceInterval());
xaDataSource.setMaxIdleTime(testConfig.getMaxIdleTime());
xaDataSource.setTestQuery(testConfig.getTestQuery());
return xaDataSource;
}
@Bean(name = "test1SqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory(@Qualifier("test1DataSource") DataSource dataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
}
@Bean(name = "test1SqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(
@Qualifier("test1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
Data source 2 has a similar configuration.
controller
package com.et.atomikos.controller;
import com.et.atomikos.mapper1.ManyService1;
import com.et.atomikos.mapper2.ManyService2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class HelloWorldController {
@Autowired
private ManyService1 manyService1;
@Resource
private ManyService2 manyService2;
//http://localhost:8088/datasource1?userId=9&username=datasource1&age=2
@RequestMapping(value = "datasource1")
public int datasource1(String userId,String username, Integer age) {
return manyService1.insert(userId,username, age);
}
//http://localhost:8088/datasource2?userId=9&username=datasource2&age=2
@RequestMapping(value = "datasource2")
public int datasource2(String userId,String username, Integer age) {
return manyService2.insert(userId,username, age);
}
//http://localhost:8088/insertDb1AndDb2?userId=1&username=tom5&age=2
//http://localhost:8088/insertDb1AndDb2?userId=2&username=tom5&age=0 //touch a error
@RequestMapping(value = "insertDb1AndDb2")
public int insertDb1AndDb2(String userId,String username, Integer age) {
return manyService1.insertDb1AndDb2(userId,username, age);
}
}
application.yaml
server:
port: 8088
spring:
application:
name: manyDatasource
datasource:
# spring.datasource.test1
# druid:
test1:
jdbcurl: jdbc:mysql://localhost:3333/demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
minPoolSize: 3
maxPoolSize: 25
maxLifetime: 20000
borrowConnectionTimeout: 30
loginTimeout: 30
maintenanceInterval: 60
maxIdleTime: 60
test2:
jdbcurl: jdbc:mysql://localhost:3334/demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
minPoolSize: 3
maxPoolSize: 25
maxLifetime: 20000
borrowConnectionTimeout: 30
loginTimeout: 30
maintenanceInterval: 60
maxIdleTime: 60
mybatis:
mapper-locations: classpath:mapper/*.xml
spring.resources.static-locations: classpath:static/,file:static/
logging:
level:
czs: debug
org.springframework: WARN
org.spring.springboot.dao: debug
The above are just some key codes. For all codes, please see the code repository below.
code repository
4.test
start up Spring Boot application
Insert the first data test
http://localhost:8088/datasource1?userId=9&username=datasource1&age=2
Insert into second database
http://localhost:8088/datasource2?userId=9&username=datasource2&age=2
Insert into 2 databases at the same time
http://localhost:8088/insertDb1AndDb2?userId=1&username=tom5&age=2
Exception rollback test
http://localhost:8088/insertDb1AndDb2?userId=2&username=tom5&age=0 //touch a error