• Seata——基础(笔记)


    一、分布式事务

    1.1 产生原因

    现在有一个下微服务

    1.创建订单
    2.扣减余额
    3.扣减库存
    订单服务
    DB
    账户服务
    DB2
    库存服务
    DB3

    如果库存服务异常回滚,不会影响订单和账户服务因为他们不知道库存服务出问题了。我们需要解决这个问题,因此有了分布式事务。

    在分布式系统下,一个业务跨越了多个服务或数据源,每个服务都是一个分支事务,要保证所有的分支事务最终状态一致,这样的事务就是分布式事务。

    1.2 解决问题的理论基础

    1.2.1 CAP定理

    分布式系统有三个指标

    • Consistency一致性:用户访问分布式系统中任意节点,得到的数据必须一致
    • Availability可用性:用户访问集群中的任意健康节点,必须能够得到响应,而不是超时或拒绝
    • Partition tolerace分区容错性:
      • 分区:因为网络故障或其他原因,导致分布式系统中的部分节点与其它节点失去连接,形成独立分区
      • 容错:集群出现分区时,整个系统也要持续对外提供服务
      • 网络等问题出现时,我们要保证它能够处理该问题,并继续服务。

    此外,分布式系统无法同时满足这三个指标。

    1. 一致性与可用性(CA):一旦出现了分区,为了保证数据一致性于可用性,我们必须让有问题的节点无法访问。违背了分区容错性
    2. 一致性与分区容错性(CP):为了保证分区容错,我们需要让所有访问该服务的业务进行阻塞,等待其恢复。此时违背了可用性
    3. 可用性与分区容错性(AP):如果我们保证可用性和分区容错性,因为分区之后,数据可能会有滞后,丢失等问题,那么无法保证数据一致性。

    1.2.2 BASE理论

    是对CAP的一中解决思路,包含三个思想:

    • Basically Available(基本可用):分布式系统出现故障时,允许损失一部分可用性,保证核心可用
    • Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态
    • Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致

    1.2.3 CA模式与CP模式

    • AP模式:各个子事务分别执行与提交,允许出现结果不一致,然后采用弥补措施恢复数据,实现最终一致。最终一致思想。
    • CP模式:各个子事务执行后相互等待,同时提交,同时回滚,达成强一致性。但事务等待过程中,处于弱可用状态。强一致性思想。

    1.2. 4 分布式事务模型

    解决分布式事务,必须让各个子系统能够感知到彼此的事务状态,才能保证状态一致,因此需要一个事务协调这来协调每一个事务的参与者(子系统事务)

    • 分支事务:子系统事务
    • 全局事务:有关联的各个分支事务在一起
    1.创建订单
    2.扣减余额
    3.扣减库存
    订单服务
    事务协调者
    账户服务
    库存服务

    二、Seata框架

    2.1 架构

    Seata官网
    官方文档

    • TC(Transcation Coordinator)- 事务协调者:维护全局和分支事务状态,协调全局事务的提交或回滚
    • TM(Transcation Manager)- 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务
    • RM(Resource Manager)- 资源管理器:管理分支事务处理的资源,与TC交谈已注册分支事务和报告分支事务状态,并驱动分支事务的提交或回滚
    1. 微服务的入口方法定义了全局事务的范围:他调用了多少个微服务,就说明有多少个分支事务
    2. TM通过监控微服务入口,可知道有多少个分支事务。当入口方法被执行,TM拦截向TC注册开启全局事务
    3. RM代理微服务,向TC注册当前分支事务,告知属于哪一个全局事务
    4. 执行完成后,RM会报告分支事务状态给TC
    5. TM等到入口方法执行完毕后,提交事务给TC,TC检查分支事务状态。全部成功,则告知RM提交,反之就会滚。
    TM
    RM:分支事务1:微服务
    RM:分支事务2:微服务
    TC

    依据上书模型,Seata提供了四种不同的分布式事务解决方案:

    • XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
    • TCC模式:最终一致性的分阶段事务模式,有业务侵入
    • AT模式:最终一致性的分阶段事务模式,无业务侵入,也是Seata默认的模式
    • SAGA模式:长业务模式,有业务侵入

    2.2 部署

    Seata-安装地址

    进入config中,将application.example.yml自己需要的部分复制粘贴并填写即可。
    笔者此处用的是nacos

    server:
      port: 7091
    
    spring:
      application:
        name: seata-server
    
    logging:
      config: classpath:logback-spring.xml
      file:
        path: ${user.home}/logs/seata
      extend:
        logstash-appender:
          destination: 127.0.0.1:4560
        kafka-appender:
          bootstrap-servers: 127.0.0.1:9092
          topic: logback_to_logstash
    
    console:
      user:
        username: seata
        password: seata
    
    seata:
      config:
        # support: nacos, consul, apollo, zk, etcd3
        type: "nacos"
        nacos:
          server-addr: 127.0.0.1:8848
          namespace:
          group: "SEATA_GROUP"
          username: "nacos"
          password: "nacos"
          data-id: seataServer.properties
      registry:
        # support: nacos, eureka, redis, zk, consul, etcd3, sofa
        type: "nacos"
        nacos:
          application: "seata-server"
          server-addr: 127.0.0.1:8848
          group: "DEFAULT_GROUP"
          namespace: ""
          cluster: "CQ"
          username: "nacos"
          password: "nacos"
      store:
        # support: file 、 db 、 redis
        mode: file
    #  server:
    #    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
      security:
        secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
        tokenValidityInMilliseconds: 1800000
        ignore:
          urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    根据上面文件中自己Config位置的dataID 和 group在nacos中创建对应的配置。
    在这里插入图片描述

    # 数据存储方式
    # 数据存储方式
    store.mode=db
    store.db.datasource=druid
    store.db.dbType=mysql
    store.db.driverClassName=com.mysql.jdbc.Driver
    #mysql8 store.db.driverClassName=com.mysql.cj.jdbc.Driver
    store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
    store.db.user=root
    store.db.password=123456
    store.db.minConn=5
    store.db.maxConn=30
    store.db.globalTable=global_table
    store.db.branchTable=branch_table
    store.db.queryLimit=100
    store.db.lockTable=lock_table
    store.db.maxwait=5000
    # 事务、日志等配置
    server.recovery.committingRetryPeriod=1000
    server.recovery.asynCommittingRetryPeriod=1000
    server.recovery.rollbackingRetryPeriod=1000
    server.recovery.timeoutRetryPeriod=1000
    server.maxCommitRetryTimeout=-1
    server.maxRollbackRetryTimeout=-1
    server.rollbackRetryTimeoutUnlockEnable=false
    server.undo.logSaveDays=7
    server.undo.logDeletePeriod=86400000
    
    # 客户端与服务端传输方式
    transport.serialization=seata
    transport.compressor=none
    #关闭metrics功能,提高性能
    metrics.enabled=false
    metrics.registryType=compact
    metrics.exporterList=prometheus
    metrics.exporterPrometheusPort=9898
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    在数据库中(上面配置的自己的数据库)导入seata自带的表结构,作为全局事务局部事务的存储位置
    在这里插入图片描述
    在这里插入图片描述

    随后在bin目录运行程序。

    在nacos中可以查阅到,即说明成功。
    在这里插入图片描述

    2.3 构建项目

    建立如图数据库格式
    account_tbl
    在这里插入图片描述
    order_tbl
    在这里插入图片描述

    storage_tbl
    在这里插入图片描述

    创建项目
    在这里插入图片描述

    我们接下来在主类导入如下内容,之后就不用再在子类中配置了

    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0modelVersion>
        <parent>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-parentartifactId>
            <version>2.6.5version>
        parent>
        <groupId>org.examplegroupId>
        <artifactId>seata-demoartifactId>
        <packaging>pompackaging>
        <version>1.0-SNAPSHOTversion>
        <modules>
            <module>account-servicemodule>
            <module>order-servicemodule>
            <module>storage-servicemodule>
        modules>
    
        <properties>
            <maven.compiler.source>11maven.compiler.source>
            <maven.compiler.target>11maven.compiler.target>
            <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
            <java.version>1.8java.version>
            <spring-cloud.version>2021.0.3spring-cloud.version>
            <spring-cloud-alibaba.version>2.2.5.RELEASEspring-cloud-alibaba.version>
            <mybatis.version>3.5.2mybatis.version>
            <mybatis.generator.version>3.5.3mybatis.generator.version>
            <swagger.version>1.6.6swagger.version>
        properties>
        <dependencyManagement>
            <dependencies>
                
                <dependency>
                    <groupId>org.springframework.cloudgroupId>
                    <artifactId>spring-cloud-dependenciesartifactId>
                    <version>${spring-cloud.version}version>
                    <type>pomtype>
                    <scope>importscope>
                dependency>
                <dependency>
                    <groupId>com.alibaba.cloudgroupId>
                    <artifactId>spring-cloud-alibaba-dependenciesartifactId>
                    <version>${spring-cloud-alibaba.version}version>
                    <type>pomtype>
                    <scope>importscope>
                dependency>
            dependencies>
        dependencyManagement>
        <dependencies>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
            dependency>
    
            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.cloudgroupId>
                        <artifactId>spring-cloud-starter-netflix-ribbonartifactId>
                    exclusion>
                exclusions>
            dependency>
    
            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
            dependency>
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-bootstrapartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-configuration-processorartifactId>
                <optional>trueoptional>
            dependency>
    
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-openfeignartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-loadbalancerartifactId>
            dependency>
    
            <dependency>
                <groupId>io.github.openfeigngroupId>
                <artifactId>feign-httpclientartifactId>
            dependency>
    
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
            dependency>
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-boot-starterartifactId>
                <version>${mybatis.version}version>
            dependency>
    
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-generatorartifactId>
                <version>${mybatis.generator.version}version>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-freemarkerartifactId>
            dependency>
    
            <dependency>
                <groupId>io.swaggergroupId>
                <artifactId>swagger-annotationsartifactId>
                <version>${swagger.version}version>
            dependency>
        dependencies>
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130

    解析数据库并生成相应代码

    package com.storage;
    
    
    import com.baomidou.mybatisplus.generator.FastAutoGenerator;
    import com.baomidou.mybatisplus.generator.config.OutputFile;
    import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
    
    import java.util.Collections;
    
    public class generator {
        public static void main(String[] args){
            FastAutoGenerator.create("jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=UTF-8&&useSSL=false", "root", "123456")
                    .globalConfig(builder -> {
                        builder.author("yjx23332") // 设置作者
                                .enableSwagger() // 开启 swagger 模式
                                .fileOverride() // 覆盖已生成文件
                                .outputDir("D:\\tool\\seata-demo\\storage-service\\src\\main\\java"); // 指定输出目录
                    })
                    .packageConfig(builder -> {
                        builder.parent("com.storage") // 设置父包名
                                .moduleName("") // 设置父包模块名
                                .pathInfo(Collections.singletonMap(OutputFile.xml, "D:\\tool\\seata-demo\\storage-service\\src\\main\\resources")); // 设置mapperXml生成路径
                    })
                    .strategyConfig(builder -> {
                        builder.addInclude("storage_tbl") // 设置需要生成的表名
                                .addTablePrefix("t_", "c_"); // 设置过滤表前缀
                    })
                    .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                    .execute();
    
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    在这里插入图片描述

    同理全部解析完成后,继续如下操作。
    完成每一个子项的配置

    server:
      port: 8082
    spring:
      application:
        name: order-service
      cloud:
        nacos:
          server-addr: 127.0.0.1:8848
          discovery:
            cluster-name: CQ
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root
        password: 123456
        url: jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=UTF-8&&useSSL=false
    feign:
      httpclient:
        max-connections-per-route: 50
        connection-timeout: 2000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    并且完成主运行类的创建

    package com.order;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    @MapperScan("com.order.mapper")
    @EnableFeignClients
    @EnableTransactionManagement
    public class MainApplication {
        public static void main(String[] args){
            SpringApplication.run(MainApplication.class,args);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    Order服务

    package com.order.controller;
    
    import com.order.entity.OrderTbl;
    import com.order.service.IOrderTblService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 

    * 前端控制器 *

    * * @author yjx23332 * @since 2022-08-24 */
    @RestController @RequestMapping("/orderTbl") public class OrderTblController { @Autowired private IOrderTblService iOrderTblService; @PostMapping public ResponseEntity<Integer> createOrder(OrderTbl order){ int orderId = iOrderTblService.create(order); return ResponseEntity.status(HttpStatus.CREATED).body(orderId); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    package com.order.service.impl;
    
    import com.order.entity.OrderTbl;
    import com.order.mapper.OrderTblMapper;
    import com.order.service.AccountClient;
    import com.order.service.IOrderTblService;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.order.service.StorageClient;
    import feign.FeignException;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    /**
     * 

    * 服务实现类 *

    * * @author yjx23332 * @since 2022-08-24 */
    @Service @Slf4j public class OrderTblServiceImpl extends ServiceImpl<OrderTblMapper, OrderTbl> implements IOrderTblService { @Autowired AccountClient accountClient; @Autowired StorageClient storageClient; @Override @Transactional public int create(@RequestBody OrderTbl order) { getBaseMapper().insert(order); try { accountClient.deduct(order.getUserId(),order.getMoney()); storageClient.deduct(order.getCommodityCode(),order.getCount()); }catch (FeignException e){ log.error("下单失败,原因:{}",e.contentUTF8()); throw new RuntimeException(e.contentUTF8(),e); } return order.getId(); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    package com.order.service;
    
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.stereotype.Service;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PostMapping;
    
    @FeignClient("account-service")
    @Service
    public interface AccountClient {
        @PutMapping("/accountTbl/{userId}/{money}")
        void deduct(@PathVariable("userId")Integer userId, @PathVariable("money")Integer money);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    package com.order.service;
    
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.stereotype.Service;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PutMapping;
    
    @FeignClient("storage-service")
    @Service
    public interface StorageClient {
        @PutMapping("/storageTbl/{code}/{count}")
        void deduct(@PathVariable("code")String code,@PathVariable("count") Integer count);
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    Storage

    package com.storage.controller;
    
    import com.storage.service.IStorageTblService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PutMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 

    * 前端控制器 *

    * * @author yjx23332 * @since 2022-08-24 */
    @RestController @RequestMapping("/storageTbl") public class StorageTblController { @Autowired IStorageTblService iStorageTblService; @PutMapping("/{code}/{count}") public ResponseEntity<Void> deduct(@PathVariable("code") String code, @PathVariable("count") Integer count){ iStorageTblService.deduct(code,count); return ResponseEntity.noContent().build(); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    package com.storage.service.impl;
    
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
    import com.storage.entity.StorageTbl;
    import com.storage.mapper.StorageTblMapper;
    import com.storage.service.IStorageTblService;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    /**
     * 

    * 服务实现类 *

    * * @author yjx23332 * @since 2022-08-24 */
    @Service public class StorageTblServiceImpl extends ServiceImpl<StorageTblMapper, StorageTbl> implements IStorageTblService { @Override @Transactional public void deduct(String code, Integer count) { StorageTbl storageTbl = getBaseMapper().selectOne(new LambdaQueryWrapper<StorageTbl>() .eq(StorageTbl::getCommodityCode,code)); storageTbl.setCount(storageTbl.getCount() - count); if(storageTbl.getCount() < 0){ throw new RuntimeException("库存不足!"); } getBaseMapper().updateById(storageTbl); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    Account

    package com.account.controller;
    
    import com.account.service.IAccountTblService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PutMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 

    * 前端控制器 *

    * * @author yjx23332 * @since 2022-08-24 */
    @RestController @RequestMapping("/accountTbl") public class AccountTblController { @Autowired private IAccountTblService iAccountTblService; @PutMapping("/{userId}/{money}") public ResponseEntity<Void> debuct(@PathVariable("userId") Integer userId, @PathVariable("money") Integer money){ iAccountTblService.deduct(userId,money); return ResponseEntity.noContent().build(); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    package com.account.service.impl;
    
    import com.account.entity.AccountTbl;
    import com.account.mapper.AccountTblMapper;
    import com.account.service.IAccountTblService;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    /**
     * 

    * 服务实现类 *

    * * @author yjx23332 * @since 2022-08-24 */
    @Service public class AccountTblServiceImpl extends ServiceImpl<AccountTblMapper, AccountTbl> implements IAccountTblService { @Override @Transactional public void deduct(Integer userId, Integer money) { AccountTbl accountTbl = getBaseMapper().selectOne(new LambdaQueryWrapper<AccountTbl>() .eq(AccountTbl::getUserId,userId)); accountTbl.setMoney(accountTbl.getMoney() - money); if(accountTbl.getMoney() < 0){ throw new RuntimeException("余额不足!"); } getBaseMapper().updateById(accountTbl); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    接下来我们放一些数据进去,然后测试一下能不能用。

    准备一些数据
    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述

    2.4 集成Seata

    引入Seata依赖

    <properties>
    	...
    	<seata.version>1.4.2seata.version>
    	<alibaba.druid.version>1.2.9alibaba.druid.version>
    properties>
    
    <dependencies>
    	<dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-starter-alibaba-seataartifactId>
                <exclusions>
     
                    <exclusion>
                        <groupId>seata-srping-boot-startergroupId>
                        <artifactId>io.seataartifactId>
                    exclusion>
                exclusions>
            dependency>
            <dependency>
                <groupId>io.seatagroupId>
                <artifactId>seata-spring-boot-starterartifactId>
                <version>${seata.version}version>
            dependency>
    
            <dependency>
                <groupId>com.alibabagroupId>
                <artifactId>druidartifactId>
                <version>${alibaba.druid.version}version>
            dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    为每一个服务配置seata,内容要和之前seata中的信息一致

    seata:
      registry:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          namespace: ""
          group: DEFAULT_GROUP
          application: seata-server
          username: nacos
          password: nacos
      #事务组
      tx-service-group: seata-demo
      service:
        # 事务组与cluster的映射关系
        vgroup-mapping:
          seata-demo: CQ
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    重新运行,如果启动失败请考虑Seata与OpenFeign兼容性问题,
    可以试着注释掉

    	
    
    
    
    
    
    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以在Seata控制台看到相关的信息,注册会比较慢。
    在这里插入图片描述

    2.5 模式

    2.5.1 XA模式

    XA规范是X/Open组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,描述了全局的TM与局部RM之间的接口,几乎所有主流数据库都对XA规范提供了支持、

    该标准基于数据库的能力去完成。

    阶段一:准备阶段
    RM这里是数据库,由TC通知各个数据库执行,执行完毕不提交,而是告知TC

    1.1准备
    1.2就绪
    1.1准备
    1.2就绪
    TC
    RM
    RM1

    如果都成功了,TC通知提交

    2.1提交
    2.2已提交
    2.1提交
    2.2已提交
    TC
    RM
    RM1

    如果有失败

    1.1准备
    1.2就绪
    1.1准备
    1.2失败
    TC
    RM
    RM1

    针对提交的数据库

    2.1回滚
    2.2已回滚
    TC
    RM
    2.5.1.1 seata的XA模式
    1.1开启全局事务
    1.2调用分支
    1.2调用分支
    1.3注册分支事务
    1.3注册分支事务
    1.5报告事务状态
    1.5报告事务状态
    1.4执行事务
    1.4执行事务
    2.1提交或回滚全局事务
    2.2检查分支事务状态
    2.3提交或者回滚
    2.3提交或者回滚
    TM
    TC
    RM
    RM1
    2.5.1.2 使用

    开启模式,每一个RM都需要添加

    seata:
      data-source-proxy-mode: XA
    
    • 1
    • 2

    标记全局事务入口

    	@Override
        @GlobalTransactional
        public int create(OrderTbl order) {
    		...
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    成功提交:
    在这里插入图片描述
    回滚:
    在这里插入图片描述
    在这里插入图片描述

    2.5.2 AT模式

    AT模式同样是分阶段提交事务模型,不过弥补了XA模型中资源锁定周期过长的缺陷

    1.1开启全局事务
    1.2调用分支
    1.2调用分支
    1.3注册分支事务
    1.3注册分支事务
    1.4执行事务并提交
    1.4执行事务并提交
    1.4记录更新前后快照
    1.4记录更新前后快照
    2.1提交或回滚全局事务
    2.2检查分支事务状态
    2.3提交
    2.3提交
    2.4删除或恢复
    2.4删除或恢复
    TM
    TC
    RM
    RM1
    undolog

    使用方式同上,直接把XA改为AT。

    需要创建undo_log
    在这里插入图片描述
    注意

    • lock_table由TC可访问的数据库管理(server)
    • undo_log由RM可访问的数据库管理(client)

    创建用的sql,可以直接在github源码里面找到官方源码
    在这里插入图片描述

    -- for AT mode you must to init this sql for you business database. the seata server not need it.
    CREATE TABLE IF NOT EXISTS `undo_log`
    (
        `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
        `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
        `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
        `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
        `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
        `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
        `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
        UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
    ) ENGINE = InnoDB
      AUTO_INCREMENT = 1
      DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    成功
    在这里插入图片描述

    失败
    在这里插入图片描述

    2.5.2.1 XA与AT
    1. XA一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源
    2. XA模式依赖数据库机制实现回滚,AT模式利用快照实现数据回滚
    3. XA模式强一致;AT模式最终一致
    2.5.2.2 脏写

    提前把锁释放了,期间其他事务可以写数据,但是出错后回滚,将导致其他事务丢失更新。

    1. 事务1,事务2同时要写相同数据。
    2. 事务1先获取锁,执行完成后,提交后释放了资源,但此时分布式事务还没有结束
    3. 事务2获取到锁,修改了数据,也提交了
    4. 事务1的其中一个服务出错,回滚
    5. 事务2修改的数据没了
    • 解决方式:全局锁,由TC记录
      在这里插入图片描述
      在执行sql后会获取全局锁,在提交数据库锁,这样即使局部数据库释放了,但是全局锁没有释放

    • 全局锁的死锁问题
      因为是执行完前先保存快照,执行sql后才获取全局锁,也就是说数据库锁会先被获取。
      当一个事务获取了表的全局锁但他释放了数据库锁,随后另一个事务获取了该表的锁执行sql后,想要获取全局锁,却发现无法获取,进入等待。
      有全局锁的事务需要回滚,但是当进入该表时,无法获取数据库锁,也进入了等待。

    • 解决方法:
      全局锁重试默认30次,每次等待10ms,超时则回滚释放数据库锁

    2.5.2.3 全局锁与数据库锁
    • 数据库锁由数据库管理。
    • 全局锁由TC管理,事务不是由TC管理,也可去操作数据。

    由此可以看出,全局锁粒度更小,因此性能比XA模式好一些。但是,隔离不够彻底。

    • 但是大多数情况都是成功,回滚情况较少。
    • 另外分布式事务并发效果较低,耗时长
    • 业务上也可以去隔离它们

    但是如果发生了会怎么样?即,一个TC管理的一个非TC管理,对同一个表进行操作。
    Seata保存了两个快照,一个是更新前快照,一个更新后快照。恢复时他会对比是否期间是否有操作。如果有问题,此时就可以请求人工操作。

    2.5.3 TCC模式

    以扣费为例,金额会分为冻结金额与可用金额

    • 一阶段:检查余额是否充足 ,充足则冻结金额增加应付金额,可用金额则减去应付金额
    • 二阶段:检查冻结金额减去应付金额
    • 二阶段:如果要回滚,则冻结金额减去应付金额,余额增加应付金额

    最终一致。

    1.1开启全局事务
    1.2调用分支
    1.2调用分支
    1.3注册分支事务
    1.3注册分支事务
    1.5报告事务状态
    1.5报告事务状态
    1.4try资源预留
    1.4try资源预留
    2.1提交或回滚全局事务
    2.2检查分支事务状态
    2.3提交或者回滚
    2.3提交或者回滚
    2.4confirm确认或cancel取消
    2.4confirm确认或cancel取消
    TM
    TC
    RM
    RM1
    2.5.3.1 优缺点

    优点

    1. 一阶段直接提交事务,释放数据库资源,性能好
    2. 相比AT模型,无需生成快照,无需使用全局锁,性能最强
    3. 不依赖数据库事务,而是依赖补偿操作,可用于非事务性数据库

    缺点

    1. 有代码侵入,需要认为编写try,Confirm,Cancel接口
    2. 软状态,事务是最终一致
    3. 需要考虑Confirm与Cancel的失败情况,失败会重试,重试就会引发重复操作,需做好幂等处理

    数学中幂等就是多次运算结果一致
    这里就是,同一个操作不管你操作多少次结果是相同的

    我们可以AT、XT、TCC混着用

    2.5.3.2 空回滚与业务悬挂
    • 空回滚:当一个分支的try阶段阻塞,导致一些分支还没有执行try,结果被要求执行cancel回滚,但这个cancel是空的,因此它们不能回滚。

    • 业务悬挂:当阻塞的分支畅通了后继续执行,但已经事务已经回滚结束了,那么它就永远不可能confirm或者cancel。应当阻止空回滚后的try操作。

    2.5.3.3 TCC使用

    我们需要实现如下表
    state

    • 0:try
    • 1:confirm
    • 2:cancel
      在这里插入图片描述
    package com.account.service;
    
    import io.seata.rm.tcc.api.BusinessActionContext;
    import io.seata.rm.tcc.api.BusinessActionContextParameter;
    import io.seata.rm.tcc.api.LocalTCC;
    import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
    
    @LocalTCC
    public interface TCCService {
        @TwoPhaseBusinessAction(name = "prepare",commitMethod = "confirm",rollbackMethod = "cancel")
        void prepare(@BusinessActionContextParameter(paramName = "userId")Integer userId,
                     @BusinessActionContextParameter(paramName = "money")Integer money);
    
        boolean confirm(BusinessActionContext context);
    
        boolean cancel(BusinessActionContext context);
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    package com.account.service.impl;
    
    import com.account.entity.AccountFreezeTbl;
    import com.account.service.IAccountFreezeTblService;
    import com.account.service.IAccountTblService;
    import com.account.service.TCCService;
    import io.seata.core.context.RootContext;
    import io.seata.rm.tcc.api.BusinessActionContext;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    public class TCCServiceImpl implements TCCService {
        @Autowired
        private IAccountTblService iAccountTblService;
        @Autowired
        private IAccountFreezeTblService iAccountFreezeTblService;
    
        @Override
        @Transactional
        public void prepare(Integer userId, Integer money) {
            //获取上下文中的XID
            String xid = RootContext.getXID();
    
            AccountFreezeTbl freezeTbl =  iAccountFreezeTblService.getById(xid);
    
            //业务悬空判断
            if(freezeTbl != null){
                return ;
            }
    
            //扣钱
            iAccountTblService.deduct(userId,money);
    
            freezeTbl = new AccountFreezeTbl();
            freezeTbl.setUserId(userId);
            freezeTbl.setFreezeMoney(money);
            freezeTbl.setState(0);
            freezeTbl.setXid(xid);
            //存储冻结金额
            iAccountFreezeTblService.save(freezeTbl);
        }
    
        @Override
        public boolean confirm(BusinessActionContext context) {
            String xid = RootContext.getXID();
            int count = iAccountFreezeTblService.getBaseMapper().deleteById(xid);
            return count == 1;
        }
    
        @Override
        public boolean cancel(BusinessActionContext context) {
            String xid = context.getXid();
    
            AccountFreezeTbl accountFreezeTbl = iAccountFreezeTblService.getById(xid);
            //空回滚判断
            if(accountFreezeTbl == null){
                Integer userId = Integer.parseInt(context.getActionContext("userId").toString());
                accountFreezeTbl = new AccountFreezeTbl();
                accountFreezeTbl.setUserId(userId);
                accountFreezeTbl.setFreezeMoney(0);
                accountFreezeTbl.setState(2);
                accountFreezeTbl.setXid(xid);
                int count = iAccountFreezeTblService.getBaseMapper().insert(accountFreezeTbl);
                return count == 1;
            }
            //幂等判断
            if(accountFreezeTbl.getState() == 2){
                return true;
            }
            // 恢复余额
            iAccountTblService.refund(accountFreezeTbl.getUserId(),accountFreezeTbl.getFreezeMoney());
    
            accountFreezeTbl.setFreezeMoney(0);
            accountFreezeTbl.setState(2);
            int count = iAccountFreezeTblService.getBaseMapper().updateById(accountFreezeTbl);
            return count == 1;
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    package com.account.service.impl;
    
    import com.account.entity.AccountTbl;
    import com.account.mapper.AccountTblMapper;
    import com.account.service.IAccountTblService;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    /**
     * 

    * 服务实现类 *

    * * @author yjx23332 * @since 2022-08-24 */
    @Service public class AccountTblServiceImpl extends ServiceImpl<AccountTblMapper, AccountTbl> implements IAccountTblService { @Override @Transactional(rollbackFor = RuntimeException.class) public void deduct(Integer userId, Integer money) { AccountTbl accountTbl = getBaseMapper().selectOne(new LambdaQueryWrapper<AccountTbl>() .eq(AccountTbl::getUserId,userId)); accountTbl.setMoney(accountTbl.getMoney() - money); if(accountTbl.getMoney() < 0){ throw new RuntimeException("余额不足!"); } getBaseMapper().updateById(accountTbl); } @Override @Transactional public void refund(Integer userId, Integer freezeMoney) { AccountTbl accountTbl = getBaseMapper().selectOne(new LambdaQueryWrapper<AccountTbl>() .eq(AccountTbl::getUserId,userId)); accountTbl.setMoney(accountTbl.getMoney() + freezeMoney); getBaseMapper().updateById(accountTbl); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    package com.account.controller;
    
    import com.account.service.IAccountTblService;
    import com.account.service.TCCService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PutMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 

    * 前端控制器 *

    * * @author yjx23332 * @since 2022-08-24 */
    @RestController @RequestMapping("/accountTbl") @Slf4j public class AccountTblController { @Autowired private TCCService tccService; @PutMapping("/{userId}/{money}") public ResponseEntity<Void> debuct(@PathVariable("userId") Integer userId, @PathVariable("money") Integer money){ tccService.prepare(userId,money); return ResponseEntity.noContent().build(); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    2.5.4 SAGA模式

    • 一阶段:直接提交本地事务
    • 二阶段:成功则什么都不做,失败则通过编写补偿业务来回滚

    没有隔离性,最终一致。

    2.5.4.1 优势与缺点

    优势

    • 事务参与者基于事件驱动实现异步调用,吞吐高
    • 一阶段直接提交事务,性能好
    • 不用编写TCC中的三个阶段,实现简单

    缺点

    • 软状态持续时间不确定,时效性差
    • 没有锁,没有事务隔离,会有脏写

    参考文献

    [1]黑马程序员Java微服务
    [2]Seata官网

  • 相关阅读:
    Linux之sched_setscheduler调度策略总结(六十)
    【故障公告】被放出的 Bing 爬虫,又被爬宕机的园子
    简单几个步骤,轻松完成短视频配音工作|别惊讶,让我手把手教你
    javascript中continue关键字和break关键字的区别
    Java面试经验,Java实习生应届生面试笔试题整理
    Java学习笔记(四)
    使用pandas处理excel文件【Demo】
    我在滴滴做开源
    多个PDF发票合并实现一张A4纸打印2张电子/数电发票功能
    美团二面:如何保证Redis与Mysql双写一致性?连续两个面试问到了!
  • 原文地址:https://blog.csdn.net/weixin_46949627/article/details/126485222