Java架构直通车——多数据源下事务强一致性解决方案

引入

在多数据源下,我们事务一致性是很难保障的,比如我们配置了两个数据源,一个交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);

    }

但是,这里我们有两个事务tm337tm338,我们没有办法显试的直接配置两个数据源:

@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,需要准备:

  • mysql5.7以上支持XA协议。
  • mysql connector/j 5.0以上支持XA协议。
  • 数据源采用Atomikos,这是目前比较流行的基于XA协议的数据源。

引入包:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </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")这一处。

运行,两处数据源事务回滚,成功。

解决方案三:使用Mycat或者sharding-jdbc

之前学习分库分表、分布式ID的时候用到了Mycat和sharding-jdbc,这里实现基于xa协议2PC的时候也可以用到mycat和sharding-jdbc。


  • mycat

mycat需要在配置文件server.xml中配置是否过滤分布式事务:

<!--分布式事务开关,0为不过滤分布式事务,1为过滤分布式事务(如果分布式事务内只涉及全局表,则不过滤),2为不过滤分布式事务,但是记录分布式事务日志-->
<property name="handleDistributedTransactions">0</property>

运行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

使用sharding-jdbc就更简单了,不需要其他显试配置,直接用@Transactional就能实现强一致性。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值