JTA初步学习
1.介绍
1.1.定义
允许应用程序执行分布式事务处理——在两个或多个网络计算机资源上访问并且更新数据。JDBC驱动程序的JTA支持极大地增强了数据访问能力。
1.2.XA协议
一个分布式事务的规范,其定义了一个分布式事务的处理模型——DTP。在DTP中定义了三个组件:
(1)Application Program(AP):应用程序,即业务层,它定义了事务的边界,以及构成该事务的特定操作;
(2)Resource Manager(RM):资源管理器,可以理解为一个DBMS系统,或者消息服务器管理系统;
(3)Transaction Manager(TM):事务管理器,负责协调和管理事务。
关系:AP与RM之间,AP通过RM提供的API进行交互,当需要进行分布式事务时,则向TM发起一个全局事务,TM与RM之间则通过XA接口进行交互,TM管理了到RM的链接,并实现了两阶段提交。
1.3.两阶段提交流程(2PC)
(1)两个阶段内容:
1.请求阶段(commit-request phase,或称表决阶段,voting phase)在请求阶段,协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)。
2.提交阶段(commit phase)
在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或取消。当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务。参与者在接收到协调者发来的消息后将执行响应的操作。
(2)缺点:
①同步阻塞问题:当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态;
②单点故障:一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作;
③数据不一致:在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。
2.springboot中应用
Atomikos在XA中作为一个事务管理器(TM)存在。在Spring Boot应用中,可以通过Atomikos在应用中方便的引入分布式事务。
步骤:
2.1.建表脚本
数据库db1中创建book表:
CREATE TABLE `book` (
`id` int(255) NOT NULL AUTO_INCREMENT,
`book_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`isbn` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`create_date` datetime(0) NULL DEFAULT NULL,
`price` decimal(10, 0) NULL DEFAULT NULL,
`quantity` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 112 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
数据库db2中创建表book_order:
CREATE TABLE `book_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_num` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` varchar(2) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`comments` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`total` decimal(10, 0) NULL DEFAULT NULL,
`create_date` datetime(0) NULL DEFAULT NULL,
`book_id` int(11) NULL DEFAULT NULL,
`quantity` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
2.2.导入依赖
jta的依赖单独加入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
2.3.配置application.yml
配置两表的数据源:
spring:
datasource:
test1:
jdbcurl: jdbc:mysql://localhost:3306/school?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
username: root
password:
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.jdbc.Driver
minPoolSize: 3
maxPoolSize: 25
maxLifetime: 20000
borrowConnectionTimeout: 30
loginTimeout: 30
maintenanceInterval: 60
maxIdleTime: 60
test2:
jdbcurl: jdbc:mysql://localhost:3306/vue?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
username: root
password:
driver-class-name: com.mysql.jdbc.Driver
minPoolSize: 3
maxPoolSize: 25
maxLifetime: 20000
borrowConnectionTimeout: 30
loginTimeout: 30
maintenanceInterval: 60
maxIdleTime: 60
mybatis:
configuration:
map-underscore-to-camel-case: true
type-aliases-package: com.xxx.proj.jtademo
2.4.数据源-属性配置类
(1)DBConfig1:
package com.xxx.proj.jtademo.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "spring.datasource.test1") // 注意这个前缀要和application.yml文件的前缀一样
public class DBConfig1 {
// @Value("${mysql.datasource.test1.jdbcurl}")
//@Value("${jdbcurl}")
private String jdbcurl;
//private String url;
// 比如这个url在properties中是这样子的mysql.datasource.test1.username = root
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;
}
(2)DBConfig2:
package com.xxx.proj.jtademo.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "spring.datasource.test2") // 注意这个前缀要和application.yml文件的前缀一样
public class DBConfig2 {
// @Value("${mysql.datasource.test1.jdbcurl}")
//@Value("${jdbcurl}")
private String jdbcurl;
//private String url;
// 比如这个url在properties中是这样子的mysql.datasource.test1.username = root
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;
}
2.5.JTA配置类
(1)DbConfig1Atomikos:
package com.xxx.proj.jtademo.config;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.mysql.cj.jdbc.MysqlXADataSource;
import com.xxx.proj.jtademo.properties.DBConfig1;
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;
@Configuration
@MapperScan(basePackages = "com.xxx.proj.jtademo.mapper1", sqlSessionTemplateRef = "test1SqlSessionTemplate")
public class DbConfig1Atomikos {
@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);
// 将本地事务注册到创 Atomikos全局事务
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);
}
}
(2)DbConfig2Atomikos:
package com.xxx.proj.jtademo.config;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.mysql.cj.jdbc.MysqlXADataSource;
import com.xxx.proj.jtademo.properties.DBConfig1;
import com.xxx.proj.jtademo.properties.DBConfig2;
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;
@Configuration
@MapperScan(basePackages = "com.xxx.proj.jtademo.mapper2", sqlSessionTemplateRef = "test2SqlSessionTemplate")
public class DbConfig2Atomikos {
@Bean(name = "test2DataSource") //test2DataSource
public DataSource testDataSource(DBConfig2 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);
// 将本地事务注册到创 Atomikos全局事务
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("test2DataSource");
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 = "test2SqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory(@Qualifier("test2DataSource") DataSource dataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
}
@Bean(name = "test2SqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(
@Qualifier("test2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
2.6.实体类
(1)Book
package com.xxx.proj.jtademo.entity;
import lombok.Data;
import java.util.Date;
@Data
public class Book {
private Integer id;
private String bookName;
private String isbn;
private Date createDate;
private Double price;
private Integer quantity;
}
(2)BookOrder
package com.xxx.proj.jtademo.entity;
import lombok.Data;
import java.util.Date;
@Data
public class BookOrder {
private Integer id;
private String orderNum;
private String status;
private String comments;
private String total;
private Integer bookId;
private String quantity;
private Date createDate;
}
2.7.持久层
注意这里两个mapper属于不同的包!!!!
(1)BookMapper
package com.xxx.proj.jtademo.mapper1;
import com.xxx.proj.jtademo.entity.Book;
public interface BookMapper {
void insert(Book book);
}
(2)BookMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.proj.jtademo.mapper1.BookMapper">
<insert id="insert" parameterType="com.xxx.proj.jtademo.entity.Book">
insert into book( book_name) values(#{bookName})
</insert>
</mapper>
(3)BookOrderMapper
package com.xxx.proj.jtademo.mapper2;
import com.xxx.proj.jtademo.entity.BookOrder;
public interface BookOrderMapper {
void insert(BookOrder bookOrder);
}
(4)BookOrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.proj.jtademo.mapper2.BookOrderMapper">
<insert id="insert" parameterType="com.xxx.proj.jtademo.entity.BookOrder">
insert into book_order( comments) values(#{comments})
</insert>
</mapper>
2.8.服务层
(1)OrderService
package com.xxx.proj.jtademo.service;
public interface OrderService {
void saveSuccess();
void saveFail();
}
(2)OrderServiceImpl
package com.xxx.proj.jtademo.service.impl;
import com.xxx.proj.jtademo.entity.Book;
import com.xxx.proj.jtademo.entity.BookOrder;
import com.xxx.proj.jtademo.mapper1.BookMapper;
import com.xxx.proj.jtademo.mapper2.BookOrderMapper;
import com.xxx.proj.jtademo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private BookMapper bookMapper;
@Autowired
private BookOrderMapper bookOrderMapper;
@Override
@Transactional
public void saveSuccess() {
int x = (int) (Math.random() * 100);
Book book = new Book();
book.setBookName("图书" + x);
BookOrder bookOrder = new BookOrder();
bookOrder.setComments("购买图书" + x);
bookMapper.insert(book);
bookOrderMapper.insert(bookOrder);
}
@Override
@Transactional
public void saveFail() {
int x = (int) (Math.random() * 100);
Book book = new Book();
book.setBookName("图书" + x);
BookOrder bookOrder = new BookOrder();
bookOrder.setComments("购买图书" + x);
bookMapper.insert(book);
bookOrderMapper.insert(bookOrder);
int y = 1 / 0;
}
}
2.9.控制层
OrderController
package com.xxx.proj.jtademo.controller;
import com.xxx.proj.jtademo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@RequestMapping("/s")
public String success() {
orderService.saveSuccess();
return "success";
}
@RequestMapping("/f")
public String fail() {
orderService.saveFail();
return "fail";
}
}
2.10.启动类
package com.xxx.proj.jtademo;
import com.xxx.proj.jtademo.properties.DBConfig1;
import com.xxx.proj.jtademo.properties.DBConfig2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(value = {DBConfig1.class, DBConfig2.class})
public class JtaDemoApplication {
public static void main(String[] args) {
SpringApplication.run(JtaDemoApplication.class, args);
}
}
2.11.网页请求测试
运行JtaDemoApplication 中的main方法以启动服务器:
(1)访问localhost:8080/s:
book数据存入了school数据库中表,而书籍的订单数据存入了vue数据库中的表,则实现了分布式事务处理
(2)访问localhost:8080/f
看回滚结果