【Spring Cloud Alibaba】分布式事务 Seata

【Spring Cloud Alibaba】分布式事务 Seata

  • 1 @Transactional注解
  • 2 分布式事务解决方案
  • 3 Seata AT
    • 3.1 Seata AT 概述
    • 3.2 部署
    • 3.3 系统集成
    • 3.4 业务测试

Seata的分布式事务解决方案是业务层面的解决方案,只依赖于单台数据库的事务能力。 Seata框架中一个分布式事务包含3中角色: Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。 Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。

1 @Transactional注解

@Transactional是Spring 事务管理提供的注解,在一个方法中加上了这个注解,那么这个方法就将是有事务的,方法内的操作要么一起提交、要么一起回滚

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

    /**
     * 当在配置文件中有多个 TransactionManager , 可以用该属性指定选择哪个事务管理器。
     */
    @AliasFor("transactionManager")
    String value() default "";

    /**
     * 同上。
     */
    @AliasFor("value")
    String transactionManager() default "";

    /**
     * 事务的传播行为,默认值为 REQUIRED。
     */
    Propagation propagation() default Propagation.REQUIRED;

    /**
     * 事务的隔离规则,默认值采用 DEFAULT。
     */
    Isolation isolation() default Isolation.DEFAULT;

    /**
     * 事务超时时间。
     */
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

    /**
     * 是否只读事务
     */
    boolean readOnly() default false;

    /**
     * 用于指定能够触发事务回滚的异常类型。
     */
    Class<? extends Throwable>[] rollbackFor() default {};

    /**
     * 同上,指定类名。
     */
    String[] rollbackForClassName() default {};

    /**
     * 用于指定不会触发事务回滚的异常类型
     */
    Class<? extends Throwable>[] noRollbackFor() default {};

    /**
     * 同上,指定类名
     */
    String[] noRollbackForClassName() default {};

}

@Transactional注解最常见的应用

  • 可以标注在类、方法和接口(不要这样用)上;且方法上的注解会覆盖类上的注解
  • 标注在方法上,标识开启事务功能,正常则提交、异常则回滚
  • 自行指定rollbackFor属性,让Checked Exception 也能够实现回滚
  • 让TestCase 也能够实现回滚

测试用例@Transactional的使用

/**
 * 

testTransactional01 - 测试事务时会回滚事务 - 执行后会回滚

* @Transactional 如果为提交则不会回滚 * version: 1.0 - 2022/3/8 */ @Transactional @Test public void testTransactional01(){ //数据库存储操作 } /** *

testTransactional02 - 测试事务时会回滚事务 - 指定不回滚 - 即使发生异常也不会回滚

* version: 1.0 - 2022/3/8 */ @Rollback(value = false) @Transactional @Test public void testTransactional02(){ //数据库存储操作 throw new RuntimeException("error"); }

@Transactional注解失效的场景

  • 把注解标注在非public修饰的方法上
  • propagation(传播行为)属性配置错误(不合理)rollbackFor属性设置错误
  • 在同一个类中方法调用,导致事务失效
  • 自己主动去catch,代表『没有出现』异常,导致事务失效
  • 数据库引擎本身就不支持事务(例如MyISAM ),当然也不会生效

同一个类中方法调用,如下列代码调用wrongRollbackFor会导致事务失效

@Transactional
 @Override
 public void wrongRollbackFor() throws Exception {
     //数据上的操作
     throw new RuntimeException("发生异常,测试@Transactional");
 }

 @Override
 public void wrongInnweCall() throws Exception {
     wrongRollbackFor();
 }

2 分布式事务解决方案

分布式事务是来源于微服务的(或类似的场景),服务之间存在着调用,且整个调用链路上存在着多处(分布在不同的微服务上)写数据表的行为,那么,分布式事务就要保证这些操作要么全部成功,要么全部失败

创建订单
扣除余额
扣除库存
物流订单
Order Service
订单数据库
Account Service
账号数据库
Goods Service
商品数据库
Logistics Service
物流数据库

分布式事务可能追求的一致性条件不同(业务特性)

  • 强一致性:任何一次读都能读到某个数据的最近一次写的数据(要求最高)
  • 弱一致性:数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱─致性(绝大多数的业务场景都不允许)
  • 最终━致性:不保证在任意时刻数据都是完整的(状态一致),但是,随时时间的推移(会有个度量),数据总是会达到一致的状态

最常用的分布式事务的解决方案:两阶段提交

  • 两阶段指的是分两步提交﹔存在一个中央协调器负责协调各个分支事务

第一阶段:

中央协调器 本地资源管理器A 本地资源管理器B 是否就绪 就绪 是否就绪 就绪 中央协调器 本地资源管理器A 本地资源管理器B

第二阶段:

中央协调器 本地资源管理器A 本地资源管理器B 提交 成功 提交 成功 中央协调器 本地资源管理器A 本地资源管理器B

最常用的分布式事务的解决方案:本地消息表

  • 该方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行

【Spring Cloud Alibaba】分布式事务 Seata_第1张图片

3 Seata AT

3.1 Seata AT 概述

官方文档

Seata 是什么?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
【Spring Cloud Alibaba】分布式事务 Seata_第2张图片

AT 模式
前提

  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

整体机制
两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。
      【Spring Cloud Alibaba】分布式事务 Seata_第3张图片

Seata术语
TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

3.2 部署

下载 | 官方部署指南

目录结构(采用版本1.4.2)

+---bin
+---conf
|   +---logback
|   \---META-INF
|       \---services
+---lib
|   \---jdbc

修改配置文件,这里使用MySQL作为存储
(注意:修改之前记得先进行备份操作)

## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "db"
  ## rsa decryption public key
  publicKey = ""
  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
    url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"
    user = "root"
    password = "root"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }

  ## redis store property
  redis {
    ## redis mode: single、sentinel
    mode = "single"
    ## single mode property
    single {
      host = "127.0.0.1"
      port = "6379"
    }
    ## sentinel mode property
    sentinel {
      masterName = ""
      ## such as "10.28.235.65:26379,10.28.235.65:26380,10.28.235.65:26381"
      sentinelHosts = ""
    }
    password = ""
    database = "0"
    minConn = 1
    maxConn = 10
    maxTotal = 100
    queryLimit = 100
  }
}

主要修改
mode = “db”
db {
url = “jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true”
user = “root”
password = “root”
}

seata 所需要的数据表

1.4版本mysql数据库语句:https://github.com/seata/seata/blob/1.4.0/script/server/db/mysql.sql

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
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,
    `gmt_modified`      DATETIME,
    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;


采用nacos作为注册中心

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = "SEATA"
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
    aclToken = ""
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = "SEATA"
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
    aclToken = ""
  }
  apollo {
    appId = "seata-server"
    ## apolloConfigService will cover apolloMeta
    apolloMeta = "http://192.168.1.204:8801"
    apolloConfigService = "http://192.168.1.204:8080"
    namespace = "application"
    apolloAccesskeySecret = ""
    cluster = "seata"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
    nodePath = "/seata/seata.properties"
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

这里修改为nacos主要是这些属性
type = “nacos”
注册和配置下的nacos信息均为如下
nacos {
application = “seata-server”
serverAddr = “127.0.0.1:8848”
group = “SEATA_GROUP”
namespace = “SEATA”
cluster = “default”
username = “nacos”
password = “nacos”
}
注意namespace 的值SEATA是在Nacos上创建的命名空间的ID,根据自己环境配置

下载config.txt与nacos-config.sh文件
1.4版本config.txt: 下载
将config.txt放conf同级目录下

# default_tx_group 后续配置会使用到,可以自定义
service.vgroupMapping.default_tx_group=default
#配置数据库
store.mode=db
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root

1.4版本nacos-config.sh:下载
将nacos-config.sh放conf下

输入命令行,将这些配置导入到nacos的seata命名空间中:

sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t SEATA -u nacos -w nacos

SEATA 是之前创建的命名空间ID

命令解析:

  • -h -p 指定nacos的端口地址;
  • -g 指定配置的分组,注意,是配置的分组;
  • -t 指定命名空间id;
  • -u -w指定nacos的用户名和密码,同样,这里开启了nacos注册和配置认证的才需要指定。

这两个文件的作用:
config.txt就是seata各种详细的配置,执行 nacos-config.sh
即可将这些配置导入到nacos,这样就不需要将file.conf和registry.conf放到我们的项目中了,需要什么配置就直接从nacos中读取。

3.3 系统集成

需要在每个微服务数据库创建undo_log表

-- for AT mode you must to init this sql for you business database. the seata server not need it.
-- 注意此处0.7.0+ 增加字段 context
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=1 DEFAULT CHARSET=utf8;

添加依赖

<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-starter-alibaba-seataartifactId>
    <version>2.2.7.RELEASEversion>
dependency>
<dependency>
    <groupId>io.seatagroupId>
    <artifactId>seata-spring-boot-starterartifactId>
    <version>1.4.2version>
dependency>
<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>druid-spring-boot-starterartifactId>
    <version>1.2.6version>
dependency>

启动类添加@EnableAutoDataSourceProxy注解
yaml 添加配置

seata:
  tx-service-group: default_tx_group
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: localhost:8848
      group: SEATA_GROUP
      username: nacos
      password: nacos
      namespace: SEATA
  config:
    type: nacos
    nacos:
      server-addr: localhost:8848
      group: SEATA_GROUP
      username: nacos
      password: nacos
      namespace: SEATA

3.4 业务测试

创建订单业务
【Spring Cloud Alibaba】分布式事务 Seata_第4张图片

/**
 * 创建订单业务接口
 * @return
 */
@GetMapping("createOrder")
 public StoreOrder createOrder(@RequestParam(defaultValue = "1") Integer goodId){
     try {
         log.info("create order by good id : {}",goodId);
         return storeOrderService.createOrder(goodId);
     } catch (Exception e) {
         log.error("create order fail ",e);
         return null;
     }
 }

服务中添加@GlobalTransactional(rollbackFor = Exception.class)分布式微服务事务注解

/**
 * 创建订单 减少库存
 * 仅为演示回滚,非实际业务
 * @param goodId
 * @return
 * @throws Exception
 */
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public StoreOrder createOrder(Integer goodId) throws Exception {


    //远程调用减少库存
    goodFeignClient.deductStock(goodId, 1);

	//模拟订单创建
    StoreOrder order = new StoreOrder();
    order.setOrderNo(UUID.randomUUID().toString().replace("-",""));
    //仅为演示否则此处需要先获取商品信息,这里演示回滚,非实际业务
    order.setPrice(new BigDecimal(100*goodId));
    order.setGoodId(goodId);
    order.setCreateTime(new Date());
    order.setUpdateTime(new Date());
    save(order);
    //模拟发生错误
   throw new Exception("error");
    //return order;
}

需要注意的是1.4.2 中有一个问题
SEATA 1.4.x版本在MySQL8.0中执行undo时报错Cannot construct instance of java.time.LocalDateTime

来自 https://blog.csdn.net/richie696/article/details/116896511
解决方法,其中第三点似乎并不能解决问题,如果是新项目可以使用时间戳解决
【Spring Cloud Alibaba】分布式事务 Seata_第5张图片

测试接口后查看数据库是否正常回滚

Spring Cloud Alibaba 学习笔记项目:Github,学习笔记,仅为组件学习,并没有完整案例项目

你可能感兴趣的:(Spring,Cloud,分布式,java,spring,cloud)