阿里巴巴于2019年1月入驻springcloud的孵化器后,也相继开源了自己的微服务事务管理工具Seata,对于中大厂的系统,Seata提供了全局的事务处理方案,Seata可以说是现阶段特别优秀的一款事务处理架构啦。 Seata官网

Seata 是什么?

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT(案例也是以AT模式驱动,默认的事务模式)、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。有些专业术语去官网查看,这里只帮助大家做一个快速开发案例,以及我们需要了解的原理。

Seata术语表

TC:事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚(可以简单的理解为Seata服务器,来负责全局的管控,包括事务的提交和回滚,XID为协调识别信息)

TM:事务管理者

定义全局事务的范围:开始全局事务、提交或回滚全局事务。(我们后期在处理业务的时候会在业务方法上标注@GlobalTranscational,它即代表着事务的发起方,就是整个分布式事务的的一个管理者。)

RM:资源管理者

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。(在微服务架构中,我们会在不同的服务中调用接口,然后每一个服务都在全局事务中充当一个分支事务,分支事务会提交事务的提交和回滚报告。)

图一:抽象服务流程图

在这里插入图片描述
流程图讲解:
TM开启分布式事务《TM向TC注册全局事务记录》
RM向TC汇报资源准备状态
TM结束分布式事务,事务一阶段结束《TM通知TC提交/回滚事务》
TC汇总事务信息,决定分布式事务还是提交还是回滚
TC通知所有RM提交/回滚资源,事务二阶段结束

关于TM的事务方案的两个阶段解释:

第一阶段:
Seata会拦截 “业务SQL”,解析SQL语义,在执行SQL前将元数据 保存成“before images” 内,然后执行业务数据更新,然后保存成“afterimage”,最后生成行锁,保证操作的acid特性。这样两份镜像就有了可提交,可回滚的控制条件。
第二阶段:提交/回滚
TC通知所有的TM执行提交还是回滚操作。如果是提交:将before image,after image ,行锁删除既可。如果是回滚:Seata就需要回滚到第一阶段已经执行的“业务SQL”,还原业务数据。回滚的方式是用“before image” 还原业务数据;但是还要首先校验脏读,如果元数据一致,可还原,进行反向补偿,否则人工处理。

图二:以库存,订单,账户具体实现流程图

在这里插入图片描述

天上飞的理念,必定有落地的实现

接下来就是案例,带大家快速走进Seata的世界

第一步环境 Seata服务器安装

官网下载Seata服务器 链接: 下载
在这里插入图片描述
这里推荐使用1.0版本或之后的。

1.改动conf文件夹下的配置:
conf.example.conf:修改自定义事务组名称,事务日志存储模式为db,数据库连接信息
service {
  #transaction service group mapping
  #将测试案例的自定义事务组名改成我们自己的 example_seata
  vgroup_mapping.my_test_tx_group = "example_seata"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}
## transaction log store
store {
  ## store mode: file、db
  #将默认的 存储模式file修改成数据库存储db
  mode = "db"
  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    max-branch-session-size = 16384
    # globe session size , if exceeded throws exceptions
    max-global-session-size = 512
    # file buffer size , if exceeded allocate new buffer
    file-write-buffer-cache-size = 16384
    # when recover batch read size
    session.reload.read_size = 100
    # async, sync
    flush-disk-mode = async
  }
    ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    #修改数据库连接信息
    driver-class-name = "com.mysql.cj.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "public"
    password = "public"
    min-conn = 1
    max-conn = 10
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
修改register.conf说明注册中心,这里我们选择阿里的nacos
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
  nacos {
  serverAddr = "localhost"
  namespace = ""
  cluster = "default"
}
2.创建数据库

branch_table,global_table,lock_table创建数据库表的sql,系统有子带,也可以官网下载。

3.启动nacos和seata

启动成功的标志
在这里插入图片描述

第二步,业务

我们就采用官网的,storage,order,account 的处理流程来创建案例,参考图二执行流程。
这里我们创建三个微服务:订单微服务,库存微服务,账户微服务。
业务逻辑:用户下单 ---->订单系统创建订单,远程调用库存服务减少库存,账户扣钱(openFegin客户端调用)---->订单状态修改。
案例操作跨域了三个数据库,两个远程调用,已经明显存在分布式事务的问题。

1.seataOrder,seataStorage,seataAccount库表创建

具体的建表语句自行创建,非常简单的字段数据。

2.将官网提供的日志回滚记录表在每一个库里面创建一个

undo_log.table

3.微服务Order系统搭建

工程名:Seata_order_8001
pom.xml:

    <dependencies>
        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>seata-all</artifactId>
                    <groupId>io.seata</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>0.9.0</version>
        </dependency>
        <!--feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.37</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

application.yml:

server:
  port: 8001

spring:
  application:
    name: seata-order-service  #服务名
  cloud:
    alibaba:
      seata:
        tx-service-group: fsp_tx_group #全局事务组名称
    nacos:
      discovery:
        server-addr: localhost:8848 #服务注册中心
  datasource: #数据源配置
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_account
    username: public
    password: public

feign: #openfegin 打开
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis: #mapper扫描地址
  mapperLocations: classpath:mapper/*.xml

file.conf ,registry.conf 分别拷贝到resource下,seata加载要使用到。
主启动类:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //排除自带数据源
@EnableDiscoveryClient //服务发现
@EnableFeignClients // fegin接口调用
public class SeataOrderMainApp8001
{
    public static void main(String[] args)
    {
        SpringApplication.run(SeataOrderMainApp8001.class, args);
    }
}

Storage远程接口:


@FeignClient(value = "seata-storage-service")
public interface StorageService
{
    @PostMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

DataSourceProxyConfig

@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

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

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}

mybatis 扫描入口

@Configuration
@MapperScan({"com.atguigu.springcloud.alibaba.dao"})
public class MyBatisConfig {
}

业务类:

@Service
@Slf4j
public class OrderServiceImpl implements OrderService
{
    @Resource
    private OrderDao orderDao;
    @Resource
    private StorageService storageService;
    @Resource
    private AccountService accountService;

    /**
     * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
     * 简单说:下订单->扣库存->减余额->改状态
     */
    @Override
    @GlobalTransactional(name = "example-create-order",rollbackFor = Exception.class)
    public void create(Order order)
    {
        log.info("----->开始新建订单");
        //1 新建订单
        orderDao.create(order);

        //2 扣减库存
        log.info("----->订单微服务开始调用库存,做扣减Count");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("----->订单微服务开始调用库存,做扣减end");

        //3 扣减账户
        log.info("----->订单微服务开始调用账户,做扣减Money");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("----->订单微服务开始调用账户,做扣减end");

        //4 修改订单状态,从零到1,1代表已经完成
        log.info("----->修改订单状态开始");
        orderDao.update(order.getUserId(),0);
        log.info("----->修改订单状态结束");

        log.info("----->下订单结束了,O(∩_∩)O哈哈~");

    }
}

创建storage,account系统省略,和订单系统一样的配置,可自行搭建
@GlobalTranscational(name=“事务管理者对该事务的标识”,rollbackFor=“可回滚的异常类型”)

这个注解开启,说明我们将当前的service方案已经递交给了TM进行管理啦,后续的所有接口调用,设计到SQL操作都是一个分支事务RM要处理的。
我们可以在其他系统中故意加入异常,或者调用延迟信息。系统就会触发回滚。如果没有异常那么一次调用正常走完,库信息响应修改。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐