分布式事务利器--SEATA

882 阅读12分钟

Seata

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务

Seate发展历史

bfed367f6c23438b4445eb873d24a8f

Seata产品模块

如下图所示,Seata 中有三大模块,分别是 TM、RM 和 TC。 其中 TM 和 RM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。 3e83470c394f389f1662f3ec94c1a84

尝鲜Seate环境配置

使用Seate之前需要把Seate和nacos的服务搭建, 后续会出相关的软文

依赖版本
SpringBoot2.2.4.RELEASE
SpringCloudHoxton.SR8
SpringCloudAlibaba2.2.3.RELEASE
mysql-connector-java8.0.11
spring-cloud-alibaba-seata2.2.0.RELEASE
seata-spring-boot-starter1.4.2
注:以上依赖请大家注意搭配,免得版本冲突

AT 模式

2019年1月,Seata 开源了 AT 模式。AT 模式是一种无侵入的分布式事务解决方案。在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作 17d7481eced97c0a2007d9b06d55409 Seata(AT 模式)的默认全局隔离级别是读未提交,如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理

搭建AT模式

  • 创建数据库

#########################seata_orderuse database seata_order;
CREATE TABLE `orders` (
  `id` mediumint(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `product_id` int(11) DEFAULT NULL,
  `COUNT` int(11) DEFAULT NULL COMMENT '数量',
  `pay_amount` decimal(10,2) DEFAULT NULL,
  `status` varchar(100) DEFAULT NULL,
  `add_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `last_update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

#########################seata_payuse database seata_pay;
DROP TABLE account;
CREATE TABLE `account` (
  `id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
  `total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
  `used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
  `balance` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度',
  `last_update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seata_pay`.`account` (`id`, `user_id`, `total`, `used`, `balance`) VALUES ('1', '1', '1000', '0', '100');

#########################seata_storageuse database seata_storage;
CREATE TABLE `storage` (
  `id` BIGINT(11) NOT NULL AUTO_INCREMENT,
  `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
  `total` INT(11) DEFAULT NULL COMMENT '总库存',
  `used` INT(11) DEFAULT NULL COMMENT '已用库存',
  `residue` INT(11) DEFAULT NULL COMMENT '剩余库存',
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seata_storage`.`storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');

  • undo_log表前面三个库都需要创建

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8

  • 配置文件

seata.enabled=true
seata.application-id=${spring.application.name}
seata.tx-service-group=my_test_tx_group
# seata.enable-auto-data-source-proxy=true
# seata.use-jdk-proxy=false
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=192.168.11.140:8091
seata.service.enable-degrade=false
# seata.service.disable-global-transaction=false
seata.config.type=file
seata.config.file.name=file.conf
seata.registry.type=file

三个配置文件都一样

  • 编写Account服务接口

    @Autowired
    private IAccountService accountService;
    /**
     * 扣减账户余额
     * @param userId id
     * @param money 金额
     * @return
     */
    @PostMapping("decrease")
    public R decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
        accountService.decrease(userId,money);
        return R.ok().put("date","Acount decrease success");
    }

  • 编写Storage服务接口
    /**
     * 扣减库存
     * @param productId 产品id
     * @param count 数量
     * @return
     */
    @PostMapping("decrease")
    public R decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count){
        storageService.decrease(productId,count);
        return R.ok().put("date","Decrease storage success");
    }
  • 编写Order服务 1.MySeataConfig配置类
@Configuration
public class MySeataConfig {

    @Autowired
    DataSourceProperties dataSourceProperties;
    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties) {

        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())) {
            dataSource.setPoolName(dataSourceProperties.getName());
        }
        //AT 代理
        return new DataSourceProxy(dataSource);
    }

}

2.编写创建订单接口

    /**
     * 创建订单
     * @param order
     * @return
     */
    @PostMapping("create")
    public String create(@RequestBody Orders order){
        iOrdersService.create(order);
        return "Create order success";
    }

3.编写OrdersService

    @Override
    @GlobalTransactional(name = "create-order",rollbackFor = Exception.class)
    public void create(Orders order) {
        log.info("------->交易开始");
        //本地方法
        this.baseMapper.create(order);

//        //远程方法 扣减库存
        storageApi.decrease(order.getProductId(),order.getCount());
//
//        //远程方法 扣减账户余额
//
        log.info("------->扣减账户开始order中");
       accountApi.decrease(order.getUserId(),order.getPayAmount());

        log.info("------->扣减账户结束order中");
        int a=10/0;

        log.info("------->交易结束");
    }

使用Feign继续远程完成Account服务的账号余额的扣除及Storage服务的库存扣减。方法上使用@GlobalTransactional开启Seata事务处理!

XA模式

在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。执行阶段:可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况) 完成阶段:分支提交:执行 XA 分支的 commit 分支回滚:执行 XA 分支的 rollback 01c5c93fcdad1975f49c46828c73c9f

AT 与 XA 的关系

首先,我们要明确,无论是AT还是XA,他们都是有利用到数据库自带的事务特性,来保证数据一致性和隔离性

比如AT一阶段提交和二阶段回滚,都是执行了本地事务。比如XA的一阶段和二阶段,也都是利用了数据库本身的事务特性

那么这样一样我们是否应该在数据库层面进行挖掘,AT与XA的关系呢?

首先这个时候,我们肯定要从中找相同,与找不同。AT首当其冲,他有个必须品,也就是undolog表,undolog,相 信了解数据库的同学肯定是知道。数据库有六种日志分别是:重做日志(redo log)、回滚日志(undo log)、二进制日志(binlog)、错误日志(errorlog)、 慢查询日志(slow query log)、一般查询日志(general log),中继日志(relay log)

那么数据库的undolog是做什么用的呢?undolog保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC)

可以发现数据库的undolog跟seata at模式的undolog的作用不谋而合,所以可以判断,at模式的undolog就是把本地事务作用中的undolog,利用他的原理,做到了分布式事务中,来保证了分布式事务下的事务一致性。

那么说完了undolog,redolog呢?

Redolog的作用便是防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行 重做,从而达到事务的持久性这一特性。

那么为什么Seata AT模式没有看到redolog的存在?其实很简单,这个redolog被隐藏的很深,也就是AT模式的一阶段提交,让数据库作为我们的redolog,保证一阶段的数据准确落盘。

这个时候是不是会想到LCN事务模式?他的undolog由数据库来保证,缺少了一个redolog的存在。其实大可不必思念LCN事务,解析到这里,如果把AT改为一阶段不提交,二阶段提交时,前镜像便是undolog,后镜像便是redolog,也就是说AT其实就是一个不在数据库层面,按照数据库事务思想和实现原理的方式,做到了分布式中的事务一致性。

这时候讲到这里,XA跟AT的关系应该一幕了然了,准确的说,其实应该说是分布式事务跟数据库本地事务的关系,可以说XA的缺点造成了AT模式的出生,锁在多侧(多个库),资源阻塞,性能差。

而AT就像为了把事务的实现决定权从数据库手中,放到了Seata中,自实现sql解析,自实现undolog(redolog),既然我们没有 办法去直接优化数据库在分布式事务下的问题,那么不如创造一个新的模式,去其糟粕,取其精华。

搭建XA模式

代码结构与AT模式类似,但是需要修改seata代理的配置文件

@Configuration
public class MySeataConfig {

    @Autowired
    DataSourceProperties dataSourceProperties;
    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties) {

        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())) {
            dataSource.setPoolName(dataSourceProperties.getName());
        }
//        //AT 代理
//        return new DataSourceProxy(dataSource);
        //XA代理
        return new DataSourceProxyXA(dataSource);
    }

}

TCC模式

那么对应到 TCC 模式里,也是一样的,Seata 框架把每组 TCC 接口当做一个 Resource,称为 TCC Resource。这套 TCC 接口可以是 RPC,也以是服务内 JVM 调用。在业务启动时,Seata 框架会自动扫描识别到 TCC 接口的调用方和发布方。如果是 RPC 的话,就是 sofa:reference、sofa:service、dubbo:reference、dubbo:service 等。扫描到 TCC 接口的调用方和发布方之后。如果是发布方,会在业务启动时向 TC 注册 TCC Resource,与DataSource Resource 一样,每个资源也会带有一个资源 ID。如果是调用方,Seata 框架会给调用方加上切面,与 AT 模式一样,在运行时,该切面会拦截所有对 TCC 接口的调用。每调用一次 Try 接口,切面会先向 TC 注册一个分支事务,然后才去执行原来的 RPC 调用。当请求链路调用完成后,TC 通过分支事务的资源ID回调到正确的参与者去执行对应 TCC 资源的 Confirm 或 Cancel 方法。在讲完了整个框架模型以后,大家可能会问 TCC 三个接口怎么实现。因为框架本身很简单,主要是扫描 TCC 接口,注册资源,拦截接口调用,注册分支事务,最后回调二阶段接口。最核心的实际上是 TCC 接口的实现逻辑。下面我将根据蚂蚁金服内部多年的实践来为大家分析怎么实现一个完备的 TCC 接口

TCC设计原则

从 TCC 模型的框架可以发现,TCC 模型的核心在于 TCC 接口的设计。用户在接入 TCC 时,大部分工作都集中在如何实现 TCC 服务上。下面我会分享蚂蚁金服内多年的 TCC 应用实践以及在 TCC 设计和实现过程中的注意事项。设计一套 TCC 接口最重要的是什么?主要有两点,第一点,需要将操作分成两阶段完成。TCC(Try-Confirm-Cancel)分布式事务模型相对于 XA 等传统模型,其特征在于它不依赖 RM 对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。TCC 模型认为对于业务系统中一个特定的业务逻辑 ,其对外提供服务时,必须接受一些不确定性,即对业务逻辑初步操作的调用仅是一个临时性操作,调用它的主业务服务保留了后续的取消权。如果主业务服务认为全局事务应该回滚,它会要求取消之前的临时性操作,这就对应从业务服务的取消操作。而当主业务服务认为全局事务应该提交时,它会放弃之前临时性操作的取消权,这对应从业务服务的确认操作。每一个初步操作,最终都会被确认或取消。因此,针对一个具体的业务服务,TCC 分布式事务模型需要业务系统提供三段业务逻辑:1.初步操作 Try:完成所有业务检查,预留必须的业务资源。2.确认操作 Confirm:真正执行的业务逻辑,不做任何业务检查,只使用 Try 阶段预留的业务资源。因此,只要 Try 操作成功,Confirm 必须能成功。另外,Confirm 操作需满足幂等性,保证一笔分布式事务能且只能成功一次。3.取消操作 Cancel:释放 Try 阶段预留的业务资源。同样的,Cancel 操作也需要满足幂等性。 37f91a942e74a0d16b013949881ce4e

TCC实战

  • tcc-account

编写一个业务接口decrease;一个业务成功后事务提交接口commit; 一个业务异常后事务回滚接口rollback

@RestController
@RequestMapping("/account")
@Slf4j
public class AccountController {

    @Autowired
    private IAccountService accountService;
    /**
     * 扣减账户余额
     * @param userId id
     * @param money 金额
     * @return
     */
    @PostMapping("decrease")
    public boolean   decrease(@RequestBody BusinessActionContext actionContext,@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
        return accountService.decrease(actionContext.getXid(), userId,money);
    }
    @RequestMapping("commit")
    public boolean commit(@RequestBody BusinessActionContext actionContext){
        try {
            return accountService.commit(actionContext.getXid());
        }catch (IllegalStateException e){
            log.error("commit error:", e);
            return true;
        }
    }

    @RequestMapping("rollback")
    public boolean rollback(@RequestBody BusinessActionContext actionContext){
        try {
            return accountService.rollback(actionContext.getXid());
        }catch (IllegalStateException e){
            log.error("rollback error:", e);
            return true;
        }
    }

}


  • tcc-storage 接口与tcc-account服务类似

@RestController
@RequestMapping("/storage")
@Slf4j
public class StorageController {
    @Autowired
    private IStorageService storageService;
    /**
     * 扣减库存
     * @param productId 产品id
     * @param count 数量
     * @return
     */
    @PostMapping("decrease")
    public boolean decrease(@RequestBody BusinessActionContext actionContext, @RequestParam("productId") Long productId, @RequestParam("count") Integer count){

        return storageService.decrease(actionContext.getXid(),productId,count);
    }
    @RequestMapping("commit")
    public boolean commit(@RequestBody BusinessActionContext actionContext){
        try {
            return storageService.commit(actionContext.getXid());
        }catch (IllegalStateException e){
            log.error("commit error:", e);
            return true;
        }
    }

    @RequestMapping("rollback")
    public boolean rollback(@RequestBody BusinessActionContext actionContext){
        try {
            return storageService.rollback(actionContext.getXid());
        }catch (IllegalStateException e){
            log.error("rollback error:", e);
            return true;
        }
    }

}
  • tcc-order 编写配置文件
@Configuration
public class MySeataConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource hikariDataSource(){
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "transactionManager")
    @Primary
    public DataSourceTransactionManager transactionManager(@Qualifier("hikariDataSource") DataSource hikariDataSource) {
        return new DataSourceTransactionManager(hikariDataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource hikariDataSource)throws Exception{
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(hikariDataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*Mapper.xml"));
        bean.setTransactionFactory(new SpringManagedTransactionFactory());
        return bean.getObject();
    }

}

OrderApi

LocalTCC
public interface OrderApi {

    @TwoPhaseBusinessAction(name = "orderApi", commitMethod = "commit", rollbackMethod = "rollback")
    boolean saveOrder(BusinessActionContext actionContext, Orders order);

    /**
     * 提交事务
     * @param actionContext save xid
     * @return
     */
    boolean commit(BusinessActionContext actionContext);

    /**
     * 回滚事务
     * @param actionContext save xid
     * @return
     */
    boolean rollback(BusinessActionContext actionContext);
}

StorageApi

@FeignClient(value = "tcc-storage")
@LocalTCC
public interface StorageApi {

    /**
     * 扣减库存
     * @param productId
     * @param count
     * @return
     */
    @PostMapping(value = "/storage/decrease")
    @TwoPhaseBusinessAction(name = "storageApi", commitMethod = "commit", rollbackMethod = "rollback")
    boolean decrease(@RequestBody BusinessActionContext actionContext,@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
    /**
     * 提交事务
     * @param actionContext save xid
     * @return
     */
    @GetMapping(value = "/storage/commit")
    boolean commit(@RequestBody BusinessActionContext actionContext);

    /**
     * 回滚事务
     * @param actionContext save xid
     * @return
     */
    @GetMapping(value = "/storage/rollback")
    boolean rollback(@RequestBody BusinessActionContext actionContext);
}

AccountApi

@FeignClient(value = "tcc-account")
@LocalTCC
public interface AccountApi {

    /**
     * 扣减账户余额
     * @param userId 用户id
     * @param money 金额
     * @return
     */
    @PostMapping("/account/decrease")
    @TwoPhaseBusinessAction(name = "accountApi", commitMethod = "commit1", rollbackMethod = "rollback1")
    boolean decrease(@RequestBody BusinessActionContext actionContext,@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);

    /**
     * Commit boolean.
     *
     * @param actionContext save xid
     * @return the boolean
     */
    @RequestMapping("/account/commit")
    boolean commit1(@RequestBody BusinessActionContext actionContext);

    /**
     * Rollback boolean.
     *
     * @param actionContext save xid
     * @return the boolean
     */
    @RequestMapping("/account/rollback")
    boolean rollback1(@RequestBody BusinessActionContext actionContext);
}

通过 @TwoPhaseBusinessAction 完成Tcc的二阶段提交 OrdersServiceImpl创建订单方法

    @GlobalTransactional()
    public boolean create(Orders order) {
        String xid = RootContext.getXID();
        log.info("------->交易开始");
        BusinessActionContext actionContext = new BusinessActionContext();
        actionContext.setXid(xid);
        //本地方法
        boolean result = orderApi.saveOrder(actionContext, order);
        if(!result){
            throw new RuntimeException("保存订单失败");
        }
        log.info("------->扣减库存开始storage中");
        //远程方法 扣减库存
        result=storageApi.decrease(actionContext,order.getProductId(),order.getCount());
        if(!result){
            throw new RuntimeException("扣减库存失败");
        }

        //远程方法 扣减账户余额
        log.info("------->扣减账户开始account中");
      accountApi.decrease(actionContext,order.getUserId(),order.getPayAmount());
        log.info("------->扣减账户结束account中" + result);
        log.info("------->交易结束");
        //throw new RuntimeException("调用2阶段提交的rollback方法");
        return true;
    }

SAGA模式

Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

关于SAGA的实战正在研究中。。。

参考seata官网:

素材来源网络