在多数据源下,我们事务一致性是很难保障的,比如我们配置了两个数据源,一个交db131,另一个交db132:
@Configuration
@MapperScan(value = "com.bonjour.learnmutipledatasourceconsistency.dao.dao337",
sqlSessionFactoryRef = "sqlsession337")
public class Db337Config {
@Bean("db337")
public DataSource db337(){
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setUser("root");
dataSource.setPassword("woshixiao");
dataSource.setUrl("jdbc:mysql://xxxxxx:30337/sharding_order");
return dataSource;
}
@Bean("sqlsession337")
public SqlSessionFactoryBean factoryBean(@Qualifier("db337") DataSource dataSource) throws IOException {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resourceResolver.getResources("mybatis/mybatis337/*.xml"));
return factoryBean;
}
@Bean("tm337")
public PlatformTransactionManager transactionManager(@Qualifier("db337") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
上面的代码很容易理解,配置了数据源datasource和连接sqlsession和事务transaction。
另一个数据源配置的代码就不做展示了,只要把上面代码中所有的337改成338即可,数据库地址也是这样的,说明使用的不同数据源连接的不同数据库。
数据库设计如下:
然后,我们使用mybatis-generator生成了两个数据源的mapper文件,接下来核心就是我们的service。
此时,如果我们有如下的事务:
@Service
public class BalanceAccountService {
@Autowired
Account337Mapper account337Mapper;
@Autowired
Account338Mapper account338Mapper;
@Transactional()
public void balanceAccount(){
//扣除200块钱
Account337 account337=account337Mapper.selectByPrimaryKey(1);
account337.setAmount(account337.getAmount().subtract(new BigDecimal(200)));
account337Mapper.updateByPrimaryKey(account337);
int i=1/0;//故意设置RuntimeException错误,检验事务是否具有一致性
//增加200块钱
Account338 account338=account338Mapper.selectByPrimaryKey(2);
account338.setAmount(account338.getAmount().add(new BigDecimal(200)));
account338Mapper.updateByPrimaryKey(account338);
}
}
这里描述的是转账操作的事务,一个账户扣的钱要转到另一个账户中去,现在我们同时设置两个账户的金额是1000:
不过这段代码中出现了一个RuntimeException的错误,1/0
的除0错误,我们故意为之,直接运行这段代码。
出现的结果是:数据库db131扣除了200块钱,数据库db132并没有增加200块钱。
为什么会出现这种情况呢?我们知道如果是只有一个事务,那么出现错误必然就会回滚:
@Transactional()
public void balanceAccount(){
//扣除200块钱
Account337 account1=account337Mapper.selectByPrimaryKey(1);
account1.setAmount(account1.getAmount().subtract(new BigDecimal(200)));
account337Mapper.updateByPrimaryKey(account1);
int i=1/0;//故意设置RuntimeException错误,检验事务是否具有一致性
//增加200块钱
Account337 account2=account337Mapper.selectByPrimaryKey(1);
account2.setAmount(account2.getAmount().add(new BigDecimal(200)));
account337Mapper.updateByPrimaryKey(account2);
// //增加200块钱
// Account338 account338=account338Mapper.selectByPrimaryKey(2);
// account338.setAmount(account338.getAmount().add(new BigDecimal(200)));
// account338Mapper.updateByPrimaryKey(account338);
}
但是,这里我们有两个事务tm337
和tm338
,我们没有办法显试的直接配置两个数据源:
@Transactional(transactionManager = "db337,db338")
或者
@Transactional(transactionManager = "db337")
@Transactional(transactionManager = "db338")
上面两种配置都是不对的
那么有没有什么办法做到强一致性呢?
比如这样,做一个回滚:
@Transactional()
public void balanceAccount(){
//扣除200块钱
Account337 account337=account337Mapper.selectByPrimaryKey(1);
account337.setAmount(account337.getAmount().subtract(new BigDecimal(200)));
account337Mapper.updateByPrimaryKey(account337);
try {
int i=1/0;//故意设置RuntimeException错误,检验事务是否具有一致性
}catch (Exception e){
//还原事务
Account337 new_account377=account337Mapper.selectByPrimaryKey(1);
new_account377.setAmount(new_account377.getAmount().add(new BigDecimal(200)));
account337Mapper.updateByPrimaryKey(new_account377);
}
//增加200块钱
Account338 account338=account338Mapper.selectByPrimaryKey(2);
account338.setAmount(account338.getAmount().add(new BigDecimal(200)));
account338Mapper.updateByPrimaryKey(account338);
}
又比如,使用一个事务,另一个做补偿
@Transactional(transactionManager = "tm337",rollbackFor = Exception.class)
public void balanceAccount(){
//扣除200块钱
Account337 account337=account337Mapper.selectByPrimaryKey(1);
account337.setAmount(account337.getAmount().subtract(new BigDecimal(200)));
account337Mapper.updateByPrimaryKey(account337);
try {
//增加200块钱
Account338 account338=account338Mapper.selectByPrimaryKey(2);
account338.setAmount(account338.getAmount().add(new BigDecimal(200)));
account338Mapper.updateByPrimaryKey(account338);
int i=1/0;//故意设置RuntimeException错误,检验事务是否具有一致性
}catch (Exception e){
//还原事务
Account337 new_account338=account337Mapper.selectByPrimaryKey(1);
new_account338.setAmount(new_account338.getAmount().subtract(new BigDecimal(200)));
account337Mapper.updateByPrimaryKey(new_account338);
throw e;
}
}
把之前扣除的200,给他加回去即可。但是这里面有很多的细节,由于我这里不是真正的业务代码,所以这个catch做的很随意,如果catch中的代码出错了又怎么办?如果错误是在语句Account337 new_account377=account337Mapper.selectByPrimaryKey(1);
中抛出的,又怎么办呢?增加重试次数?
所以这种方式操作复杂,代码难度大(重试部分),并不推荐。
之前,我们了解过2PC、3PC、Paxos、ZAB等等一系列强一致性协议。
这里我们介绍基于XA协议的2PC,并用实际的代码做演示。
什么是基于XA协议的2PC呢?XA是由X/Open组织提出的分布式事务规范,它由一个事务管理器(TM)和多个资源管理器(RM)组成。 当然,2PC我们很熟悉,不做赘述了,直接上图:
第一阶段:
第二阶段:
2PC的缺点就是在commit阶段出现问题,会出现不一致的情况,需要人工处理。
并且2PC性能比较低,与本地事务效率相差10倍。
我们现在使用代码来模拟基于XA协议的2PC,需要准备:
引入包:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jta-atomikosartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
同样的,配置数据源如下:
@Configuration
@MapperScan(value = "com.bonjour.learnmutipledatasourceconsistency.dao.dao337",
sqlSessionFactoryRef = "sqlsession337")
public class Db337Config {
@Bean("db337")
public DataSource db337(){
MysqlXADataSource xadataSource = new MysqlXADataSource();
xadataSource.setUser("root");
xadataSource.setPassword("woshixiao");
xadataSource.setUrl("jdbc:mysql://xxxxxxx:30337/sharding_order");
AtomikosDataSourceBean dataSourceBean=new AtomikosDataSourceBean();
dataSourceBean.setXaDataSource(xadataSource);
return dataSourceBean;
}
@Bean("sqlsession337")
public SqlSessionFactoryBean factoryBean(@Qualifier("db337") DataSource dataSource) throws IOException {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resourceResolver.getResources("mybatis/mybatis337/*.xml"));
return factoryBean;
}
}
这里用了AtomikosDataSourceBean
来封装并统一管理datasource,另一个也是如此。
当然,还需要配置XA的事务管理器,这里只需要配置一个就可以了:
@Configuration
public class TMConfig {
@Bean("xaTransaction")
public JtaTransactionManager jtaTransactionManager(){
UserTransaction userTransaction=new UserTransactionImp();
UserTransactionManager userTransactionManager=new UserTransactionManager();
return new JtaTransactionManager(userTransaction,userTransactionManager);
}
}
配置好了后,我们利用之前写过的servie稍加修改:
@Transactional(transactionManager = "xaTransaction", rollbackFor = Exception.class)
public void balanceAccount() {
//扣除200块钱
Account337 account337 = account337Mapper.selectByPrimaryKey(1);
account337.setAmount(account337.getAmount().subtract(new BigDecimal(200)));
account337Mapper.updateByPrimaryKey(account337);
int i = 1 / 0;//故意设置RuntimeException错误,检验事务是否具有一致性
//增加200块钱
Account338 account338 = account338Mapper.selectByPrimaryKey(2);
account338.setAmount(account338.getAmount().add(new BigDecimal(200)));
account338Mapper.updateByPrimaryKey(account338);
}
注意,我只修改了@Transactional(transactionManager = "xaTransaction")
这一处。
运行,两处数据源事务回滚,成功。
之前学习分库分表、分布式ID的时候用到了Mycat和sharding-jdbc,这里实现基于xa协议2PC的时候也可以用到mycat和sharding-jdbc。
mycat需要在配置文件server.xml
中配置是否过滤分布式事务:
<property name="handleDistributedTransactions">0property>
运行mycat后,只需要在application.properties里面配置mycat就行了:
spring.datasource.username=root
spring.datasource.password=woshixiao
spring.datasource.url=jdbc:mysql://xxxx:8066/user?serverTimezone=Asia/Shanghai&useSSL=false
然后只需要加上@Transactional
注解即可,无需配置其他东西,即可实现强一致性。
使用sharding-jdbc就更简单了,不需要其他显试配置,直接用@Transactional
就能实现强一致性。