因为事务需要实现ACID,即原子性、一致性、隔离性、持久性,所以需要采用一定的机制来保证,通常采用的是分阶段提交的方式。
XA:XA协议,规定事务管理器和资源管理器接口,采用二阶段提交协议。
一阶段提交协议
一阶段提交协议相对简单。当然,前提是开启了事务,然后在应用程序发出提交/回滚请求后,数据库执行操作,而后将成功/失败返回给应用程序,程序继续执行。
一阶段提交协议相对简单,简单带来的优点就是,它不用再与其他的对象交互,节省了判断步骤和时间,所以在性能上是在阶段提交协议中对好的。
二阶段提交协议
一阶段提交协议有其优点,但缺点也很明显:
数据库确认执行事务的时间较长,出问题的可能性就随之增大。
如果有多个数据源,一阶段提交协议无法协调他们之间的关系。
所以在一阶段协议的基础上,有了二阶段协议,二阶段协议的好处是添加了一个管理者角色,如下:
很明显,二阶段协议通过将两层变为三层,增加了中间的管理者角色,从而协调多个数据源之间的关系,二阶段提交协议分为两个阶段。
为什么要分两步执行?一是因为分两步,就有了事务管理器统一管理的机会;二尽可能晚地提交事务,让事务在提交前尽可能地完成所有能完成的工作,这样,最后的提交阶段将是耗时极短,耗时极短意味着操作失败的可能性也就降低。
同时,二阶段提交协议为了保证事务的一致性,不管是事务管理器还是各个资源管理器,每执行一步操作,都会记录日志,为出现故障后的恢复准备依据。
二阶段提交协议的存在的弊端是阻塞,因为事务管理器要收集各个资源管理器的响应消息,如果其中一个或多个一直不返回消息,则事务管理器一直等待,应用程序也被阻塞,甚至可能永久阻塞。
手写XA,理解其原理
事务管理器
public class TM {
public void execute(Connection accountConn,Connection redConn) throws SQLException {
//打印XA的事务日志 true 代表打印
boolean logXaCommands = true;
//获取RM1 的接口实例
XAConnection xaConn1 = new MysqlXAConnection((com.mysql.jdbc.ConnectionImpl) accountConn, logXaCommands);
XAResource rm1 = xaConn1.getXAResource();
//获取RM2 的接口实例
XAConnection xaConn2 = new MysqlXAConnection((com.mysql.jdbc.ConnectionImpl) redConn, logXaCommands);
XAResource rm2 = xaConn2.getXAResource();
//生成一个全局事务ID
byte[] globalid = "agan12345".getBytes();
int formatId = 1;
try {
//TM 把rm1的事务分支id,注册到全局事务ID
byte[] bqual1 = "b00001".getBytes();
Xid xid1 = new MysqlXid(globalid, bqual1, formatId);
//start...end 开始 结束 rm1的本地事务
rm1.start(xid1, XAResource.TMNOFLAGS);
//模拟购物买一个物品,用余额支付90㡰
String sql="update capital_account set balance_amount=balance_amount-90 where user_id=1";
PreparedStatement ps1 = accountConn.prepareStatement(sql);
ps1.execute();
rm1.end(xid1, XAResource.TMSUCCESS);
//TM 把rm2的事务分支id,注册到全局事务ID
byte[] bqual2 = "b00002".getBytes();
Xid xid2 = new MysqlXid(globalid, bqual2, formatId);
//start...end 开始 结束 rm2的本地事务
rm2.start(xid2, XAResource.TMNOFLAGS);
//模拟购物一个物品,用红包支付10元。
sql="update red_packet_account set balance_amount=balance_amount-10 where user_id=1";
PreparedStatement ps2 = redConn.prepareStatement(sql);
ps2.execute();
rm2.end(xid2, XAResource.TMSUCCESS);
//2阶段提交中得第一个阶段:准备提交
int rm1_prepare = rm1.prepare(xid1);
int rm2_prepare = rm2.prepare(xid2);
//2阶段提交中得第二个阶段:真正提交
if (rm1_prepare == XAResource.XA_OK && rm2_prepare == XAResource.XA_OK) {
boolean onePhase = false;
rm1.commit(xid1, onePhase);//提交事务
rm2.commit(xid2, onePhase);//提交事务
} else {//全部回滚
rm1.rollback(xid1);
rm1.rollback(xid2);
}
} catch (XAException e) {
// 如果出现异常,也要进行回滚
e.printStackTrace();
}
}
}
两个不同的数据源,红包账户和余额账户扣钱
public class AP {
public Connection getRmAccountConn(){
try {
Connection conn = DriverManager.getConnection("jdbc:mysql://192.168.0.138:3307/xa_account?characterEncoding=utf8&useSSL=false&autoReconnect=true", "root", "agan");
return conn;
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public Connection getRmRedConn(){
try {
Connection conn = DriverManager.getConnection("jdbc:mysql://192.168.0.138:3308/xa_red_account?characterEncoding=utf8&useSSL=false&autoReconnect=true", "root", "agan");
return conn;
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}
测试代码:
public class XaTest {
AP ap=new AP();
TM tm=new TM();
@Test
public void test(){
try {
tm.execute(ap.getRmAccountConn(),ap.getRmRedConn());
} catch (SQLException e) {
e.printStackTrace();
}
}
}
使用Atomikos
第一步:导入核心的依赖包
、、、 org.springframework.boot spring-boot-starter-jta-atomikos
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
、、、
第二步: 改配置多数据域
配置atomikos 事务管理器,并配置druid作为数据源并且进行监控 application.properties 文件中配置druid的2个数据源。
##account表数据库配置
spring.atomikos.datasource.account.max-pool-size=25
spring.atomikos.datasource.account.min-pool-size=3
spring.atomikos.datasource.account.max-lifetime=20000
spring.atomikos.datasource.account.borrow-connection-timeout=10000
spring.atomikos.datasource.account.unique-resource-name=account
spring.atomikos.datasource.account.xa-properties.url=jdbc:mysql://192.168.0.138:3307/xa_account?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
spring.atomikos.datasource.account.xa-properties.username=root
spring.atomikos.datasource.account.xa-properties.password=agan
spring.atomikos.datasource.account.xa-properties.driverClassName=com.mysql.jdbc.Driver
# 初始化大小,最小,最大
spring.atomikos.datasource.account.xa-properties.initialSize=10
spring.atomikos.datasource.account.xa-properties.minIdle=20
spring.atomikos.datasource.account.xa-properties.maxActive=100
## 配置获取连接等待超时的时间
spring.atomikos.datasource.account.xa-properties.maxWait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.atomikos.datasource.account.xa-properties.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.atomikos.datasource.account.xa-properties.minEvictableIdleTimeMillis=300000
spring.atomikos.datasource.account.xa-properties.testWhileIdle=true
spring.atomikos.datasource.account.xa-properties.testOnBorrow=false
spring.atomikos.datasource.account.xa-properties.testOnReturn=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.atomikos.datasource.account.xa-properties.poolPreparedStatements=true
spring.atomikos.datasource.account.xa-properties.maxPoolPreparedStatementPerConnectionSize=20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.atomikos.datasource.account.xa-properties.filters=stat,slf4j,wall
spring.atomikos.datasource.account.xa-data-source-class-name=com.alibaba.druid.pool.xa.DruidXADataSource
#------------------------------ 分隔符-------------------------------------
##redpacket表数据库配置
spring.atomikos.datasource.redpacket.max-pool-size=25
spring.atomikos.datasource.redpacket.min-pool-size=3
spring.atomikos.datasource.redpacket.max-lifetime=20000
spring.atomikos.datasource.redpacket.borrow-connection-timeout=10000
spring.atomikos.datasource.redpacket.unique-resource-name=redpacket
spring.atomikos.datasource.redpacket.xa-properties.url=jdbc:mysql://192.168.0.138:3308/xa_red_account?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
spring.atomikos.datasource.redpacket.xa-properties.username=root
spring.atomikos.datasource.redpacket.xa-properties.password=agan
spring.atomikos.datasource.redpacket.xa-properties.driverClassName=com.mysql.jdbc.Driver
spring.atomikos.datasource.redpacket.xa-properties.initialSize=10
spring.atomikos.datasource.redpacket.xa-properties.minIdle=20
spring.atomikos.datasource.redpacket.xa-properties.maxActive=100
spring.atomikos.datasource.redpacket.xa-properties.maxWait=60000
spring.atomikos.datasource.redpacket.xa-properties.timeBetweenEvictionRunsMillis=60000
spring.atomikos.datasource.redpacket.xa-properties.minEvictableIdleTimeMillis=300000
spring.atomikos.datasource.redpacket.xa-properties.testWhileIdle=true
spring.atomikos.datasource.redpacket.xa-properties.testOnBorrow=false
spring.atomikos.datasource.redpacket.xa-properties.testOnReturn=false
spring.atomikos.datasource.redpacket.xa-properties.poolPreparedStatements=true
spring.atomikos.datasource.redpacket.xa-properties.maxPoolPreparedStatementPerConnectionSize=20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.atomikos.datasource.redpacket.xa-properties.filters=stat,slf4j,wall
spring.atomikos.datasource.redpacket.xa-data-source-class-name=com.alibaba.druid.pool.xa.DruidXADataSource
#jta相关参数配置
#spring.jta.transaction-manager-id=txManager
#spring.jta.log-dir=transaction-logs-agan
logging.level.root=INFO
第三步:将配置的数据库连接信息,注入数据源,并且设置druid的监控中心。
MybatisConfiguration 的目的是配置DataSource
@Configuration
@EnableConfigurationProperties
@EnableTransactionManagement(proxyTargetClass = true)
public class MybatisConfiguration {
/**
* account数据库配置前缀.
*/
final static String ACCOUNT_PREFIX = "spring.atomikos.datasource.account";
/**
* redpacket数据库配置前缀.
*/
final static String REDPACKET_PREFIX = "spring.atomikos.datasource.redpacket";
/**
* The constant logger.
*/
final static Logger logger = LoggerFactory.getLogger(MybatisConfiguration.class);
/**
* 配置druid显示监控统计信息
* 开启Druid的监控平台 http://localhost:8080/druid
*
* @return servlet registration bean
*/
@Bean
public ServletRegistrationBean druidServlet() {
logger.info("Init Druid Servlet Configuration ");
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
// IP白名单,不设默认都可以
// servletRegistrationBean.addInitParameter("allow", "192.168.2.25,127.0.0.1");
// IP黑名单(共同存在时,deny优先于allow)
servletRegistrationBean.addInitParameter("deny", "192.168.1.100");
//控制台管理用户
servletRegistrationBean.addInitParameter("loginUsername", "root");
servletRegistrationBean.addInitParameter("loginPassword", "agan");
//是否能够重置数据 禁用HTML页面上的“Reset All”功能
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
}
/**
* 注册一个filterRegistrationBean
*
* @return filter registration bean
*/
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
//添加过滤规则
filterRegistrationBean.addUrlPatterns("/*");
//添加不需要忽略的格式信息
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
/**
* 配置Account数据库的数据源
*
* @return the data source
*/
@Bean(name = "AccountDataSource")
@ConfigurationProperties(prefix = ACCOUNT_PREFIX) // application.properties中对应属性的前缀
public DataSource accountDataSource() {
return new AtomikosDataSourceBean();
}
/**
* 配置RedPacket数据库的数据源
*
* @return the data source
*/
@Bean(name = "RedPacketDataSource")
@ConfigurationProperties(prefix = REDPACKET_PREFIX) // application.properties中对应属性的前缀
public DataSource redPacketDataSource() {
return new AtomikosDataSourceBean();
}
}
AccountDataSourceConfiguration 作用:配置account的数据源的sessionfactory ,同时关联mybaits RedAccountDataSourceConfiguration 作用:配置RedAccount的数据源的sessionfactory ,同时关联mybaits
@Configuration
@MapperScan(basePackages = {"com.agan.dtp.atomikos.mapper.account.mapper"}, sqlSessionFactoryRef = "accountSqlSessionFactory")
public class AccountDataSourceConfiguration {
/**
* The constant MAPPER_XML_LOCATION.
*/
public static final String MAPPER_XML_LOCATION = "classpath*:com/agan/dtp/atomikos/mapper/account/mapper/*.xml";
/**
* The Open plat form data source.
*/
@Autowired
@Qualifier("AccountDataSource")
DataSource accountDataSource;
/**
* 配置Sql Session模板
*
* @return the sql session template
* @throws Exception the exception
*/
@Bean
public SqlSessionTemplate springSqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(accountSqlSessionFactory());
}
/**
* 配置SQL Session工厂
*
* @return the sql session factory
* @throws Exception the exception
*/
@Bean
public SqlSessionFactory accountSqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(accountDataSource);
//指定XML文件路径
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_XML_LOCATION));
return factoryBean.getObject();
}
}
第四步:service体验 atomikos
PayServiceImpl 作用:模拟下订单的同时扣除,账户余额,红包余额的钱。
@Service
public class PayServiceImpl implements PayService {
@Autowired
private CapitalAccountMapper capitalAccountMapper;
@Autowired
private RedPacketAccountMapper redPacketAccountMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void pay(int userId,int account, int redAccount) {
CapitalAccount ca=new CapitalAccount();
ca.setUserId(userId);
CapitalAccount capitalDTO=this.capitalAccountMapper.selectOne(ca);
System.out.println(capitalDTO);
//账户余额扣除
capitalDTO.setBalanceAmount(capitalDTO.getBalanceAmount()-account);
this.capitalAccountMapper.updateByPrimaryKey(capitalDTO);
RedPacketAccount red= new RedPacketAccount();
red.setUserId(userId);
RedPacketAccount redDTO=this.redPacketAccountMapper.selectOne(red);
System.out.println(redDTO);
//红包余额扣除
redDTO.setBalanceAmount(redDTO.getBalanceAmount()-redAccount);
this.redPacketAccountMapper.updateByPrimaryKey(redDTO);
//int n=9/0;
}
}
第五步: junit 测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class AtomikosTests {
@Autowired
PayService payService;
@Test
//@Transactional
public void contextLoads() {
try {
this.payService.pay(1,10,10);
} catch (Exception e) {
e.printStackTrace();
}
}
}
第六步: 查看Atomikos的日志
1.如何测试atomikos的事务运行结果? 查看Atomikos的日志,默认情况下,在项目的根目录下会自动创建transaction-logs文件夹,每个Atomikos实例都会有一个全局ID,这个ID为Atomikos运行机器的IP地址; 这个唯一ID会自动关联多个数据库的事务信息,也就是会关联分支事务id.
2.atomikos日志的自定义配置 spring.jta.log-dir=transaction-logs-agan