目录
1 什么是XA事务
XA是X/Open DTP组织(X/Open DTP group)定义的两阶段提交协议,XA被许多数据库(如Oracle、DB2、SQL Server、MySQL)和中间件等工具(如CICS 和 Tuxedo).本地支持 。
X/Open DTP模型(1994)包括应用程序(AP)、事务管理器(TM)、资源管理器(RM)、通信资源管理器(CRM)四部分。
在这个模型中,通常事务管理器(TM)是交易中间件,资源管理器(RM)是数据库,通信资源管理器(CRM)是消息中间件。
一般情况下,某一数据库无法知道其它数据库在做什么,因此,在一个DTP环境中,交易中间件是必需的,由它通知和协调相关数据库的提交或回滚。而一个数据库只将其自己所做的操作(可恢复)影射到全局事务中。
XA就是X/Open DTP定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。
2 XA搭建说明
一个应用有时候需要同时连接多个数据库,并且多个数据库间的表操作还需要管理他们间事务的一致性。所以接下来将研究下分布式事务Druid是如何支持的。这里将集成Druid+Atomikos来搭建一个双数据源的XA事务。为什么要引入Atomikos呢?单靠Druid无法管理。
搭建的示例工程目录结构如下:
-
MybatisConfigurer1配置数据源1并交给mybatis的sqlsession1管理
-
MybatisConfigurer2配置数据源2并交给mybatis的sqlsession2管理
-
DruidTestMapper执行操作数据库1的表Druid_test的数据
-
SoulTestMapper操作数据库2的表soul_test的数据
-
DruidTestService连接双数据源做业务逻辑操作
-
DemoDruidApplicationTests单元测试
3 双数据源管理
数据源管理1,配置的代码如下:
/**
* 配置扫描com.example.demo_druid_xa.mapper.ds1目录下的mapper将连接数据源1操作
* 配置sqlSessionFactoryRef为sqlSessionTemplate1
*/
@Configuration
@MapperScan(basePackages = {"com.example.demo_druid_xa.mapper.ds1"},sqlSessionTemplateRef = "sqlSessionTemplate1")
public class MybatisConfigurer1 {
/**
* jdbc.ds1获取配置信息,初始化druidDataSource1
* @return
*/
@Bean(name="druidDataSource1")
@ConfigurationProperties(prefix = "jdbc.ds1")
public DruidXADataSource dataSource0(){
DruidXADataSource dataSource = new DruidXADataSource();
return dataSource;
}
/**
* 实例化AtomikosDataSourceBean,并且set Druid初始化的DruidXADataSource
* @param druidDataSource1
* @return
*/
@Primary
@Bean(name = "dataSource1")
public AtomikosDataSourceBean dataSource(@Qualifier("druidDataSource1") DruidXADataSource druidDataSource1){
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
try {
druidDataSource1.setFilters("stat");
xaDataSource.setXaDataSource(druidDataSource1);
xaDataSource.setMaxPoolSize(10);
xaDataSource.setMinPoolSize(5);
xaDataSource.setUniqueResourceName("dataSource1");
} catch (SQLException e) {
System.out.println("dataSource1 init error!"+e);
}
return xaDataSource;
}
/**
* 将AtomikosDataSourceBean交给SqlSessionFactory
* @param dataSource
* @return
* @throws Exception
*/
@Bean(name = "sqlSessionFactory1")
public SqlSessionFactory sqlSessionFactory1(@Qualifier("dataSource1") DataSource dataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
}
/**
* 将sqlSessionFactory交给SqlSessionTemplate管理
* @param sqlSessionFactory
* @return
*/
@Bean(name = "sqlSessionTemplate1")
public SqlSessionTemplate sqlSessionTemplate1(
@Qualifier("sqlSessionFactory1") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
数据源管理2,配置的代码如下:
/**
* 配置扫描com.example.demo_druid_xa.mapper.ds2目录下的mapper将连接数据源2操作
* 配置sqlSessionFactoryRef为sqlSessionTemplate2
*/
@Configuration
@MapperScan(basePackages = {"com.example.demo_druid_xa.mapper.ds2"},sqlSessionTemplateRef = "sqlSessionTemplate2")
public class MybatisConfigurer2 {
/**
* jdbc.ds1获取配置信息,初始化druidDataSource2
* @return
*/
@Bean(name="druidDataSource2")
@ConfigurationProperties(prefix = "jdbc.ds2")
public DruidXADataSource dataSource0(){
DruidXADataSource dataSource = new DruidXADataSource();
return dataSource;
}
/**
* 实例化AtomikosDataSourceBean,并且set Druid初始化的DruidXADataSource
* @param druidDataSource2
* @return
*/
@Bean(name = "dataSource2")
public AtomikosDataSourceBean dataSource(@Qualifier("druidDataSource2") DruidXADataSource druidDataSource2){
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
try {
druidDataSource2.setFilters("stat");
xaDataSource.setXaDataSource(druidDataSource2);
xaDataSource.setMaxPoolSize(10);
xaDataSource.setMinPoolSize(5);
xaDataSource.setUniqueResourceName("dataSource2");
} catch (SQLException e) {
System.out.println("dataSource2 init error!"+e);
}
return xaDataSource;
}
/**
* 将AtomikosDataSourceBean交给SqlSessionFactory
* @param dataSource
* @return
* @throws Exception
*/
@Bean(name = "sqlSessionFactory2")
public SqlSessionFactory sqlSessionFactory2(@Qualifier("dataSource2") DataSource dataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
}
/**
* 将sqlSessionFactory交给SqlSessionTemplate管理
* @param sqlSessionFactory
* @return
*/
@Bean(name = "sqlSessionTemplate2")
public SqlSessionTemplate sqlSessionTemplate2(
@Qualifier("sqlSessionFactory2") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
application.properties配置文件如下:
#---------------------使用durid连接池
jdbc.datasources=ds1,ds2
jdbc.ds1.url=jdbc:mysql://localhost:3306/shenyu?useUnicode=true&characterEncoding=utf8
jdbc.ds1.username=root
jdbc.ds1.password=123456
jdbc.ds1.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.ds1.initialSize=5
jdbc.ds1.minIdle=10
jdbc.ds1.maxActive=20
jdbc.ds1.maxWait=6000
jdbc.ds1.timeBetweenEvictionRunsMillis=2000
jdbc.ds1.minEvictableIdleTimeMillis=600000
jdbc.ds1.maxEvictableIdleTimeMillis=900000
jdbc.ds1.validationQuery=select 1
jdbc.ds1.testWhileIdle=true
jdbc.ds1.testOnBorrow=false
jdbc.ds1.testOnReturn=false
jdbc.ds1.keepAlive=true
jdbc.ds1.phyMaxUseCount=1000
jdbc.ds1.filters=stat
jdbc.ds2.url=jdbc:mysql://localhost:3306/soul?useUnicode=true&characterEncoding=utf8
jdbc.ds2.username=root
jdbc.ds2.password=123456
jdbc.ds2.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.ds2.initialSize=5
jdbc.ds2.minIdle=10
jdbc.ds2.maxActive=20
jdbc.ds2.maxWait=6000
jdbc.ds2.timeBetweenEvictionRunsMillis=2000
jdbc.ds2.minEvictableIdleTimeMillis=600000
jdbc.ds2.maxEvictableIdleTimeMillis=900000
jdbc.ds2.validationQuery=select 1
jdbc.ds2.testWhileIdle=true
jdbc.ds2.testOnBorrow=false
jdbc.ds2.testOnReturn=false
jdbc.ds2.keepAlive=true
jdbc.ds2.phyMaxUseCount=1000
jdbc.ds2.filters=stat
4 设置主从
由 @Primary来配置主从,@Primary为Spring注解,Bean配置了该注解后,会被默认优先选择,使用如下,这里配置了数据源1为主数据源。
5 运行
Mapper代码如下:
public interface DruidTestMapper {
@Insert("insert into druid_test values ('2','2')")
void insertOne();
}
public interface SoulTestMapper {
@Insert("insert into soul_test values ('2')")
void insertOne();
}
@Service
public class DruidTestService {
@Autowired
private DruidTestMapper druidTestMapper;
@Autowired
private SoulTestMapper soulTestMapper;
@Transactional(rollbackFor = Exception.class)
public void insert(){
druidTestMapper.insertOne();
soulTestMapper.insertOne();
}
}
@SpringBootTest
class DemoDruidApplicationTests {
@Autowired
private DruidTestService druidTestService;
@Test
void test() {
druidTestService.insert();
}
}
run DemoDruidApplicationTests,运行成功。
查看数据库两张表结果,已插入成功2。
再来一个失败测试事务的回滚:
public interface DruidTestMapper {
@Insert("insert into druid_test values ('3','3')")
void insertOne();
}
// 错误代码
public interface SoulTestMapper {
@Insert("insert into soul_test values x ('3')")
void insertOne();
}
@Service
public class DruidTestService {
@Autowired
private DruidTestMapper druidTestMapper;
@Autowired
private SoulTestMapper soulTestMapper;
@Transactional(rollbackFor = Exception.class)
public void insert(){
// 第一步执行成功
druidTestMapper.insertOne();
// 第二步执行失败
soulTestMapper.insertOne();
}
}
查看结果,发现两个数据库都没数据插入,说明事务已回滚: