微服务之Spring cloud alibaba入门——Seata篇

微服务之Spring cloud alibaba入门——Seata篇

一. 官网简介

seata的官网链接

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
拿经典的下订单问题举例,用户下订单需要减商品库存、并且需要减少用户账户余额。若减少库存之后,调用减少用户账户余额方法时产生了错误,那么会使余额扣减失败,但此时库存已经减少,信息明显不正确。在传统的单体应用中,只对应一个数据库,下单时的减库存、减账户余额可以在一个事务内完成,因此可以解决上述问题。但是在分布式的环境下,这几个服务不止对应一个数据库,可能是一个服务对应于一个数据库。那么在这种情况下,原本的数据库事务就不能解决该问题,需要一个解决分布式事务的方案,那么seata就可以实现这样的一种操作。

二. 名词术语

XID: 全局唯一事务ID
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

三. 基本过程

微服务之Spring cloud alibaba入门——Seata篇_第1张图片

具体过程:
1.TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
2. XID在微服务调用链路的上下文中传播
3. RM想TC注册分支事务,将其纳入XID对应全局事务的管辖
4. TM向TC发起针对XID的全局提交或回滚决议
5. TC调度XID下管辖的全局分支事务完成提交或回滚请求

四. seata下载和配置

此处下载版本为1.3.0

seata下载链接

  1. 修改seata的conf目录下的 file.conf 文件

    • 自定义事务组
      将service模块中的 vgroupMapping.my_test_tx_group = "default"修改为vgroupMapping.自定义事务名_tx_group = “default”
    • 修改事务日志存储模式为db
      将store模块下的mode改为 db
    • 修改数据库连接信息
      将store块的db模块的数据库链接信息进行修改,该处是以mysql8.0为例,mysql5版本配置成相应版本的链接信息即可。此处的 seata 是后期在本地MySQL建立的数据库,在此处先这样配置。
      driverClassName = "com.mysql.cj.jdbc.Driver"
      url = "jdbc:mysql://127.0.0.1:3306/seata?serverTimezone=UTC"
      user = "root"
      password = "123456"
  2. 建立本地建立名为seata数据库

  3. 在seata库中建表
    利用自带的db_store.sql建表,低版本在conf目录下就有该文件,高版本可以在github的parent目录下找,也可以直接用以下脚本创建:

    CREATE TABLE IF NOT EXISTS `global_table`
    (
        `xid`                       VARCHAR(128) NOT NULL,
        `transaction_id`            BIGINT,
        `status`                    TINYINT      NOT NULL,
        `application_id`            VARCHAR(32),
        `transaction_service_group` VARCHAR(32),
        `transaction_name`          VARCHAR(128),
        `timeout`                   INT,
        `begin_time`                BIGINT,
        `application_data`          VARCHAR(2000),
        `gmt_create`                DATETIME,
        `gmt_modified`              DATETIME,
        PRIMARY KEY (`xid`),
        KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
        KEY `idx_transaction_id` (`transaction_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8;
    
    -- the table to store BranchSession data
    CREATE TABLE IF NOT EXISTS `branch_table`
    (
        `branch_id`         BIGINT       NOT NULL,
        `xid`               VARCHAR(128) NOT NULL,
        `transaction_id`    BIGINT,
        `resource_group_id` VARCHAR(32),
        `resource_id`       VARCHAR(256),
        `branch_type`       VARCHAR(8),
        `status`            TINYINT,
        `client_id`         VARCHAR(64),
        `application_data`  VARCHAR(2000),
        `gmt_create`        DATETIME(6),
        `gmt_modified`      DATETIME(6),
        PRIMARY KEY (`branch_id`),
        KEY `idx_xid` (`xid`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8;
    
    -- the table to store lock data
    CREATE TABLE IF NOT EXISTS `lock_table`
    (
        `row_key`        VARCHAR(128) NOT NULL,
        `xid`            VARCHAR(96),
        `transaction_id` BIGINT,
        `branch_id`      BIGINT       NOT NULL,
        `resource_id`    VARCHAR(256),
        `table_name`     VARCHAR(32),
        `pk`             VARCHAR(36),
        `gmt_create`     DATETIME,
        `gmt_modified`   DATETIME,
        PRIMARY KEY (`row_key`),
        KEY `idx_branch_id` (`branch_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8;
    
  4. 修改conf目录下的registry.conf文件,对其中的Nacos模块进行修改,将serverAddr修改为 localhost:8848

  5. 替换jar包,将mysql-connector-java包改为8.0版本的jar包,seata默认的jar包为5.7版本的,若本地MySQL为5.7则无需进行该项操作。

  6. 启动Nacos后启动Seata

五. 业务数据库的创建

建立三个数据库分别存储库存、订单、账户,每个数据库一张业务表,其次建立对应的回滚表。
建立数据库和数据表:

  • 建立订单库和订单表
    create database seata_order;

    CREATE TABLE `t_order` (
      `id` bigint(11) NOT NULL AUTO_INCREMENT,
      `user_id` bigint(11) DEFAULT NULL,
      `product_id` bigint(11) DEFAULT NULL,
      `count` int(11) DEFAULT NULL,
      `money` decimal(11,0) DEFAULT NULL,
      `status` int(1) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    
  • 建立库存库和库存表
    create database seata_store;

    CREATE TABLE `t_store` (
      `id` bigint(11) NOT NULL AUTO_INCREMENT,
      `product_id` bigint(11) DEFAULT NULL,
      `total` int(11) DEFAULT NULL,
      `used` int(11) DEFAULT NULL,
      `residue` int(11) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    
    INSERT INTO `t_store` VALUES (1,1,100,50,50);
    
  • 建立账户库和账户表
    create database seata_account;

    CREATE TABLE `t_account` (
      `id` bigint(11) NOT NULL AUTO_INCREMENT,
      `user_id` bigint(11) DEFAULT NULL,
      `total` decimal(10,0) DEFAULT NULL,
      `used` decimal(10,0) DEFAULT NULL,
      `residue` decimal(10,0) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    
    INSERT INTO `t_account` VALUES (1,1,1000,700,300); 
    
  • 三个业务数据库都建立回滚表
    该代码在

    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,
      `ext` varchar(100) DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
六. 业务代码的编写

以下的每一个模块都为一个springboot项目。

  • 账户模块
  1. 引入依赖
    注意点就是引入seata依赖时,和自己下载的版本号匹配。

    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
    dependency>
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-seataartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-openfeignartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-actuatorartifactId>
    dependency>
    <dependency>
        <groupId>org.mybatis.spring.bootgroupId>
        <artifactId>mybatis-spring-boot-starterartifactId>
    dependency>
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druid-spring-boot-starterartifactId>
    dependency>
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-testartifactId>
        <scope>testscope>
    dependency>
    
  2. 修改application.yml配置

    server:
      port: 2003
    spring:
      application:
        name: seata-account-service
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        alibaba:
          seata:
            # 若之前修改成了自己配置的名称,该处则修改为   事务名_tx_group 
            tx-service-group: my_test_tx_group 
      datasource:  # 链接数据库
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/seata_account?serverTimezone=UTC
        username: root
        password: 123456
    feign:
      hystrix:
        enabled: true
    
    logging:
      level:
        io:
          seata: info
    
    mybatis:
      mapper-locations: classpath:mapper/*.xml
      type-aliases-package: com.lk.alibaba.domain
    
  3. 编写实体类

    @Data
    public class Account {
        private Long id;
        private Long user_id;
        private BigDecimal total;
        private BigDecimal used;
        private BigDecimal residue;
    }
    
  4. 编写dao

    @Mapper
    public interface AccountDao {
        //减少账户余额的方法
        void decrease(@Param("user_id") Long user_id, @Param("money") BigDecimal money);
    }
    
  5. 编写mapper.xml

    
    
    <mapper namespace="com.lk.alibaba.dao.AccountDao">
    
        <update id="decrease">
            update t_account
            set used=used+#{money},residue=residue-#{money}
            where user_id=#{user_id}
        update>
    
    mapper>
    
  6. 编写service实现类(AccountService接口省略,只有一个decrease方法,因为设置了睡眠时间为20秒,所以通过feign调用该服务时会抛出异常。本方法作为事务失败的起点。

    @Service
    public class AccountServiceImpl implements AccountService {
        @Resource
        private AccountDao accountDao;
        @Override 
        public void decrease(Long user_id, BigDecimal money) {
            try {
                TimeUnit.SECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            accountDao.decrease(user_id,money);
        }
    }
    
  7. 编写controller

    @RestController
    public class AccountController {
        @Resource
        private AccountService accountService;
    
        @RequestMapping(value = "/account/decrease")
        public String decrease(Long user_id, BigDecimal money){
            accountService.decrease(user_id, money);
            return “账户服务完成”;
        }
    }
    
  8. 编写主启动类

    @SpringBootApplication
    @EnableFeignClients
    @EnableDiscoveryClient
    public class Seata2003Application {
        public static void main(String[] args) {
            SpringApplication.run(Seata2003Application.class,args);
        }
    }
    
  9. 将seata的配置文件file.conf和registry.conf放到resources目录下

  • 库存模块
    订单模块的编写与账号模块的编写基本相同。不同点主要在于application.yml配置中的数据库连接信息(改为seate_store库)、端口号(改为2002)、应用名(改为seata-store-service)。其余的实体类、dao、service、controller基本相同,也是只有一个减库存的方法。

  • 订单模块

  1. 引入依赖
    与账户模块相同

  2. 修改application.yml
    和账户模块相同,不同点主要在于application.yml配置中的数据库连接信息(改为seate_order库)、端口号(改为2001)、应用名(改为seata-order-service)

  3. 创建实体类

    @Data
    public class Order {
        private Long id;
        private Long user_id;
        private Long product_id;
        private Integer count;
        private BigDecimal money;
        private Integer status;
    }
    
  4. 编写dao

    @Mapper
    public interface OrderDao {
        void add(Order order);
        void update(@Param("user_id") Long user_id,@Param("status") Integer status);
    }
    
  5. 编写mapper.xml

    
    
    <mapper namespace="com.lk.alibaba.dao.OrderDao">
        <insert id="add" parameterType="Order">
            insert into t_order(user_id,product_id,count,money,status)
            values(#{user_id},#{product_id},#{count},#{money},0);
        insert>
        <update id="update">
            update t_order set status=1 where user_id=#{user_id} and status=#{status}
        update>
    mapper>
    
  6. 编写service
    将store模块的业务和account模块的业务通过OpenFeign映射过来

    @FeignClient(value = "seata-store-service")
    public interface StoreService {
        @PostMapping(value = "/store/decrease")
        String decrease(@RequestParam("product_id") Long product_id,@RequestParam("count") Integer count);
    }
    
    @FeignClient(value = "seata-account-service")
    public interface AccountService {
        @GetMapping(value = "/account/decrease")
        String decrease(@RequestParam("user_id") Long user_id,@RequestParam("money") BigDecimal money);
    }
    

    编写orderService(省略)的实现类

    @Service
    public class OrderServiceImpl implements OrderService {
        @Resource
        private OrderDao orderDao;
        @Resource
        private StoreService storeService;
        @Resource
        private AccountService accountService;
    
        @Override
        @GlobalTransactional(name = "my-create-order",rollbackFor = Exception.class)
        public void add(Order order) {
            orderDao.add(order);
            storeService.decrease(order.getProduct_id(),order.getCount());
            accountService.decrease(order.getUser_id(),order.getMoney());
            orderDao.update(order.getUser_id(),order.getStatus());
        }
    }
    
  7. 编写controller测试

    @RestController
    public class OrderController {
        @Resource
        private OrderService orderService;
        @GetMapping(value = "/order/add")
        public String create(Order order){
            orderService.add(order);
            return “添加成功”;
        }
    }
    
  8. 编写主启动类

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients
    public class Seata2001Application {
        public static void main(String[] args) {
            SpringApplication.run(Seata2001Application.class,args);
        }
    }
    
  9. 将seata的配置文件file.conf和registry.conf放到resources目录下

七. 分布式事务验证

在orderService中使用到了accountService和storeService,实现分布式事务只需要在orderService的方法上添加@GlobalTransactional(name = "my-create-order",rollbackFor = Exception.class)即可实现分布式事务,其中name可以随便起,rollbackFor表示任何异常都会回滚事务。

浏览器输入http://localhost:2001/order/add?user_id=1&product_id=1&count=10&money=100进行下单操作,此时发生了异常,观察三个业务数据库,会发现数据表的数据都没发生改变。

八. Seata深度理解

该案例中,seata服务器相当于TC,标记了@GlobalTransactional的方法即为TM,TM向TC发起全局事务的请求,该案例中的orderDao和accountService以及storeService对数据库进行操作,相当于是RM。在案例的基础上再理解以下逻辑:

  1. TM开启分布式事务(TM向全局注册事务记录),注解为@GloabalTransaction
  2. 按业务场景,编排数据库、服务等事务内资源(RM向TC汇报资源准备状态)也就是当orderDao和accountService以及storeService的方法执行完以后向TC汇报状态。
  3. TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务),此时的seata数据库中,global表有全局事务记录,branch表有各个分支的记录,lock表有各分支的锁记录,各个业务数据库中的undo表中有回滚记录,记录了before-image和after-image
  4. TC汇总事务信息,决定分布式事务是提交还是回滚
  5. TC通知所有RM提交/回滚资源,回滚之前首先键查是否脏写,查看after-image和当前数据表的数据是否相同,若相同则利用before-image反向补偿进行回滚操作,若不相同产生了脏写则转人工。事务二阶段结束。
九. 下一篇介绍

暂停。
微服务之Spring cloud alibaba入门——Seata篇_第2张图片

你可能感兴趣的:(spring,cloud,alibaba,微服务)