Seata——基础(笔记)

文章目录

  • 一、分布式事务
    • 1.1 产生原因
    • 1.2 解决问题的理论基础
      • 1.2.1 CAP定理
      • 1.2.2 BASE理论
      • 1.2.3 CA模式与CP模式
      • 1.2. 4 分布式事务模型
  • 二、Seata框架
    • 2.1 架构
    • 2.2 部署
    • 2.3 构建项目
    • 2.4 集成Seata
    • 2.5 模式
      • 2.5.1 XA模式
        • 2.5.1.1 seata的XA模式
        • 2.5.1.2 使用
      • 2.5.2 AT模式
        • 2.5.2.1 XA与AT
        • 2.5.2.2 脏写
        • 2.5.2.3 全局锁与数据库锁
      • 2.5.3 TCC模式
        • 2.5.3.1 优缺点
        • 2.5.3.2 空回滚与业务悬挂
        • 2.5.3.3 TCC使用
      • 2.5.4 SAGA模式
        • 2.5.4.1 优势与缺点
  • 参考文献

一、分布式事务

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

根据上面文件中自己Config位置的dataID 和 group在nacos中创建对应的配置。
Seata——基础(笔记)_第1张图片

# 数据存储方式
# 数据存储方式
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

在数据库中(上面配置的自己的数据库)导入seata自带的表结构,作为全局事务局部事务的存储位置
Seata——基础(笔记)_第2张图片
Seata——基础(笔记)_第3张图片

随后在bin目录运行程序。

在nacos中可以查阅到,即说明成功。
Seata——基础(笔记)_第4张图片

2.3 构建项目

建立如图数据库格式
account_tbl
Seata——基础(笔记)_第5张图片
order_tbl
Seata——基础(笔记)_第6张图片

storage_tbl
Seata——基础(笔记)_第7张图片

创建项目
Seata——基础(笔记)_第8张图片

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


<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>

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

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();

    }
}


Seata——基础(笔记)_第9张图片

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

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

并且完成主运行类的创建

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);
    }
}

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); } }
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(); } }
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);

}
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);
}


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(); } }
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); } }

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(); } }
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); } }

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

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

Seata——基础(笔记)_第10张图片
在这里插入图片描述

Seata——基础(笔记)_第11张图片
Seata——基础(笔记)_第12张图片

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>

为每一个服务配置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

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

	










可以在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

标记全局事务入口

	@Override
    @GlobalTransactional
    public int create(OrderTbl order) {
		...
	}

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

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源码里面找到官方源码
Seata——基础(笔记)_第13张图片

-- 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';

成功
Seata——基础(笔记)_第14张图片

失败
Seata——基础(笔记)_第15张图片

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
    Seata——基础(笔记)_第16张图片
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);
}


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;
    }
}


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); } }
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(); } }

2.5.4 SAGA模式

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

没有隔离性,最终一致。

2.5.4.1 优势与缺点

优势

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

缺点

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

参考文献

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

你可能感兴趣的:(SpringBoot2(笔记),数据库,运维,分布式)