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

一. 官网简介

seata的官网链接

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

二. 名词术语

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

三. 基本过程

seata还需要搭建kafka吗_seata还需要搭建kafka吗

具体过程:
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"
  1. 建立本地建立名为seata数据库
  2. 在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;
  1. 修改conf目录下的registry.conf文件,对其中的Nacos模块进行修改,将serverAddr修改为 localhost:8848
  2. 替换jar包,将mysql-connector-java包改为8.0版本的jar包,seata默认的jar包为5.7版本的,若本地MySQL为5.7则无需进行该项操作。
  3. 启动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.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<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-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
  1. 修改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
  1. 编写实体类
@Data
public class Account {
    private Long id;
    private Long user_id;
    private BigDecimal total;
    private BigDecimal used;
    private BigDecimal residue;
}
  1. 编写dao
@Mapper
public interface AccountDao {
    //减少账户余额的方法
    void decrease(@Param("user_id") Long user_id, @Param("money") BigDecimal money);
}
  1. 编写mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<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>
  1. 编写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);
    }
}
  1. 编写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 “账户服务完成”;
    }
}
  1. 编写主启动类
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class Seata2003Application {
    public static void main(String[] args) {
        SpringApplication.run(Seata2003Application.class,args);
    }
}
  1. 将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;
}
  1. 编写dao
@Mapper
public interface OrderDao {
    void add(Order order);
    void update(@Param("user_id") Long user_id,@Param("status") Integer status);
}
  1. 编写mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<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>
  1. 编写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());
    }
}
  1. 编写controller测试
@RestController
public class OrderController {
    @Resource
    private OrderService orderService;
    @GetMapping(value = "/order/add")
    public String create(Order order){
        orderService.add(order);
        return “添加成功”;
    }
}
  1. 编写主启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class Seata2001Application {
    public static void main(String[] args) {
        SpringApplication.run(Seata2001Application.class,args);
    }
}
  1. 将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反向补偿进行回滚操作,若不相同产生了脏写则转人工。事务二阶段结束。
九. 下一篇介绍