多数据源下事务支持

最近工作中遇到一个问题,由于某个业务明细表数据量越来越大,因此对该表进行了分表处理,考虑到其他业务未进行分表,因此重新配置了一套分表的数据源配置,这样项目中同时有两套数据源,两套事务管理器。项目开发完毕后同事在偶然的情况下发现部分数据不正常,经过问题追踪,发现是在某个事务中,同时涉及两个数据源,@Transactional启动事务的时候使用了默认的事务管理器,导致新加的分表数据源不被事务管理,异常出现时无法进行回滚。
之前没有在项目中使用多个数据源,一直没发现这方面的问题,网上查找发现关于这个问题的文章挺多的。然而网上找的一些这方面问题的解决的文章,大多数说明的也只是单一事务的使用问题,并不是在同一事务中多个数据源的使用问题。毕竟也是,多数据源事务是一个分布式事务,想通过本地事务很好的解决分布式事务的确不太可能。

Seata下多数据源事务支持

正好最近在学习Seata框架,先尝试下Seata对多数据源的支持。

搭建项目

1、在zhengcs-seata下新增module:zhengcs-seata-muli-datasource(过程参考《Seata学习笔记二:Spring Boot + Dubbo下AT模式的尝试性使用》)
2、配置两个数据源信息

spring:
  application:
    name: zhengcs-seata-multi-datasource
  datasource:
    order:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/order
      username: test
      password: 123456
    account:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/account
      username: test
      password: 123456

3、配置两个数据源

@Configuration
@MapperScan(basePackages = "com.zhengcs.seata.multi.datasource.mapper.account",
sqlSessionTemplateRef = "accountSqlSessionTemplate")
public class DBAccountConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.account")
    public DataSource accountDataSource(){
        return new DruidDataSource();
    }


    @Bean
    public SqlSessionFactory accountSqlSessionFactory(@Qualifier("accountDataSource") DataSource dataSource) throws Exception{
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/account/*.xml"));
        //factoryBean.setConfigLocation(new ClassPathResource("mybatis-configuration.xml"));
        return factoryBean.getObject();
    }

    @Bean
    public SqlSessionTemplate accountSqlSessionTemplate(@Qualifier("accountSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}
@Configuration
@MapperScan(basePackages = "com.zhengcs.seata.multi.datasource.mapper.order",
sqlSessionTemplateRef = "orderSqlSessionTemplate")
public class DBOrderConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.order")
    public DataSource orderDataSource(){
        return new DruidDataSource();
    }


    @Bean
    public SqlSessionFactory orderSqlSessionFactory(@Qualifier("orderDataSource") DataSource dataSource) throws Exception{
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/order/*.xml"));
        //factoryBean.setConfigLocation(new ClassPathResource("mybatis-configuration.xml"));
        return factoryBean.getObject();
    }

    @Bean
    public SqlSessionTemplate orderSqlSessionTemplate(@Qualifier("orderSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}

3、业务编码
OrderService.java

@GlobalTransactional
    public void create(String userId, String commodityCode, Integer count) {

        BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
        Order order = new Order();
        order.setUserId(userId);
        order.setCommodityCode(commodityCode);
        order.setCount(count);
        order.setMoney(orderMoney);
        orderMapper.insert(order);
        log.info("全局事务ID[{}], 订单记录增加成功", RootContext.getXID());


        AccountRequest accountRequest = AccountRequest.builder()
                .userId(userId)
                .money(orderMoney)
                .build();
        accountService.debit(userId, orderMoney);
        log.info("全局事务ID[{}], 账户金额扣减成功", RootContext.getXID());

    }

AccountService.java

public void debit(String userId, BigDecimal num) {
        log.info("账户扣减接口,全局事务ID:{}");
        Account account = accountMapper.selectByUserId(userId);
        account.setMoney(account.getMoney().subtract(num));
        if(account.getMoney().compareTo(BigDecimal.ZERO) < 0){
            throw new RuntimeException("account branch exception");
        }
        accountMapper.updateById(account);
    }

模拟测试

场景1:订单创建成功,账户扣减成功

@Test
	void test() {

		orderService.create("001", "123", 10);

	}

在这里插入图片描述
场景2:订单创建成功,账户扣减失败

@Test
	void test() {

		orderService.create("001", "123", 100);

	}

在这里插入图片描述
账户服务发生异常,订单服务事务回滚
场景3、订单创建成功,账户扣减成功,抛出异常
在这里插入图片描述
在这里插入图片描述
事务顺利进行了回滚,而且是先account进行回滚,然后order进行回滚。

从上述场景来看,Seata对多数据源下事务的支持的还是比较好的,而且最重要的是对业务逻辑的侵入比较小,就我们当前的服务来说,引入Seata后只需要将@Transactional注解替换为@GlobalTransactional注解即可。但是缺点是,对于一个未搭建Seata服务的系统来说,成本还是比较大的。
那么在不引入分布式事务的前提下,如何解决在同一个方法中使用多个数据源的事务一致性问题呢?提供两个不完善的解决办法,欢迎大家补充。

本地事务解决多数据源事务一致性问题

1、使用单一事务

这个应该是最简单直接的处理方式,但是成本就未必是最小的了。
这个方案也是我的同事采取的处理方案----将mapper进行分类调整,不同的数据源对应不同的mapper,避免在同一个事务中出现mapper交叉,保证同一个事务方法中只会出现一类mapper操作(非查询)。
该方案的好处是,单一事务管理,避免了分布式事务的复杂性。
坏处主要有以下几点:
1)对于一套已经长久运行的成熟的业务复杂的单数据源单事务管理系统,改造成本较大,风险较高。
2)会出现同一个表对应多个mapper的情况,特别是对于一些insert等涉及到表结构的操作,如果表结构改变会导致多处需要改变,维护成本较高;
3)如果允许同一事务方法中使用非事务管理器对应数据源执行查询操作,可能会出现对同一个表数据的更新无法在接下来的查询中查到的情况(事务隔离级别:读已提交及以上)。

2、使用多个事务管理器

在网上发现一个比较有趣的思路 — 通过注解指定多个事务管理器,然后通过aop在方法执行前依次开启多个事务,捕捉方法执行异常,方法执行成功则依次反序提交事务,否则依次反序回滚事务。
这个思路的重点就是手动控制事务的开启、提交与回滚。基于这种思路,简单写了一个测试用例,如下:

@Autowired
    @Qualifier( "orderTransactionManager")
    DataSourceTransactionManager orderTransactionManager;
    @Autowired
    @Qualifier( "accountTransactionManager")
    DataSourceTransactionManager accountTransactionManager;


    public void create(String userId, String commodityCode, Integer count) {

        //开启事务
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        /*PROPAGATION_REQUIRES_NEW:  事物隔离级别,开启新事务*/
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        TransactionStatus status =  orderTransactionManager.getTransaction(def);
        TransactionStatus status2 =  accountTransactionManager.getTransaction(def);
        try {
            BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
            Order order = new Order();
            order.setUserId(userId);
            order.setCommodityCode(commodityCode);
            order.setCount(count);
            order.setMoney(orderMoney);
            orderMapper.insert(order);
            log.info("全局事务ID[{}], 订单记录增加成功");


            AccountRequest accountRequest = AccountRequest.builder()
                    .userId(userId)
                    .money(orderMoney)
                    .build();
            accountService.debit(userId, orderMoney);
            log.info("全局事务ID[{}], 账户金额扣减成功");
            //int a = 1/0;
        } catch (Exception e) {
            e.printStackTrace();
            accountTransactionManager.rollback(status2);
            orderTransactionManager.rollback(status);
            return;
        }

        accountTransactionManager.commit(status2);
        orderTransactionManager.commit(status);

    }
}

这种方案实现倒是简单,而且相对于方案1来说,改造成本也要小的多,但是这种方案问题还是比较多的。
1)系统无法保证多个事务的提交或者回滚操作的原子性,这样就失去了事务一致性的意义。
2)由于涉及多个事务,在数据库隔离级别为RC及以上的前提下,事务之间的数据变动不可见,稍不注意就会出现问题。

结语

以上是本人在查找多数据源下事务支持问题时总结的一些知识及简单尝试,并没有过于深入的了解底层的原理,所以如果有不对的地方希望大家及时指正。
在尝试过程中,我还尝试过在事务执行中途进行dataSource的切换,这样可以支持同一个事务里面进行多个数据源的操作,但是无助于事务回滚,因为事务启动的时候数据源和事务都已经确认,并且dataSource是存储在DataSourceTransactionManager对象中,但是transaction对象是维护在TransactionStatus对象中的,中途可以切换数据源,但是事务的提交和回滚是依赖TransactionStatus对象进行的。当然,可能有人说那就重写TransactionStatus对象,有必要吗?如果真的这样做的话还有必要使用事务管理器吗?自己手动控制吧。

参考资料

https://github.com/seata/seata-samples

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章