Seata学习—分布式事务的解决方案

Seata的学习—解决分布式事务方案

前提

使用seata

需要本地支持ACID事务支持的关系型数据库

JAVA应用,通过JDBC连接数据库

在这里插入图片描述

流程

注释1解释:

  1. 分布式事务中,我们会在对应需要增强的方法上增加注解@GlobalTransactionnal注解,发现这个注解之后,TM会发起全局事务处理
  2. TC接收到全局事务处理后会往global_table中插入一条数据,生成一个全局事务ID ,即XID,这个XID会在分支事务中传递,保证所有的分支事务属于同一个全局事务
  3. 之后进入一阶段提交(图中注释1部分),首先RM进程开启事务,会解析sql语句,得到需要更改的表名,sql语句的type(增,删,改,不包含查询操作),条件(where条件),以及修改的数据信息
  4. 之后,通过上述得到的信息生成一条查询的sql语句去查询目标表,得到前镜像,这个镜像是我们后面作为事务回滚的依据
  5. 随后 获取全局锁,通过全局锁和目标表数据的主键id绑定 执行sql语句(这里是考虑到了高并发的场景下,防止一个线程在修改数据时,另一条线程进来修改数据,形成脏写),注意这里不同于之前在spring中的事务处理,这里的sql语句是实实在在执行了
  6. 以上步骤执行后,通过主键id再次查询,得到后镜像
  7. 最重要的一步,将前镜像,后镜像,分支事务id,全局事务id,sql语句type等业务sql所需的信息组合成为一条json数据,插入表UNDO_LOG中
  8. 提交前,向 TC 注册分支:申请操作对象表中,主键值的记录的 全局锁
  9. 上面开启事务修改数据之后,本地事务提交,业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交
  10. 将本地事务提交的结果上报给 TC,至此一阶段提交完毕

注释2解释:

  1. TM的事务边界没有发生任何异常,TM向TC发起全局事务的提交,若TM的事务边界发生异常,TM向TC发起全局事务回滚

全局事务提交

  1. RM收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC(这是出于性能上的考虑,以免线程等待)
  2. 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录
  3. 释放全局锁(无论提交或回滚都要释放全局锁)

全局事务回滚

  1. 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作
  2. 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录
  3. 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理
  4. 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句(由此可见所谓的全局事务回滚就是一条恢复数据的sql语句)
  5. 提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC
  6. 释放全局锁(无论提交或回滚都要释放全局锁)

搭建Seata Server服务

1. 下载Seata Server

下载地址: http://Tags.seata/seata.GitHub

2. 配置Seata Server

在这里插入图片描述

在seata的文件夹中找到conf文件夹,其中都是seata的配置文件

找到如上图所示两个文件: file.conf 和 registry.conf

2.1 file.conf 文件配置

file.conf文件

将储存模式改为我们需要的模式,seata支持的有: file , db , redis,我们这里设置为db(database数据库)

之后在对应的配置处修改自己数据库的信息

2.2 registry.conf 文件配置

这个文件主要分为两个部分:

  1. registry 注册部分: 用于找到seata的服务地址(因为我们的seata也需要运行在nacos上,所以要把seata注册到nacos上,才能发现其他微服务)
  2. config 配置部分: 用于配置seata的配置信息
  • registry 部分配置文件

application设置seata的服务名

serverAddr设置seata的地址(我们是在nacos上运行的seata)

group设置在nacos中的组

namespace设置命名空间

cluster集群设置

username和password是nacos的用户名和密码

在这里插入图片描述

  • config 部分的配置文件

配置seata的内容

serverAddr : seata配置文件的地址(在nacos上)

namespace: seata配置文件所在的命名空间

group: seata配置文件所在的组

username和password: nacos 的用户名密码

dataId : seata配置文件名

在这里插入图片描述

2.3 seataServer.properties配置文件(目前配置文件只支持properties格式的)
  • 下载地址: https://github.com/seata/seata/tree/develop/script/config-center
transport.type=TCP

transport.server=NIO

transport.heartbeat=true

transport.enableClientBatchSendRequest=false

transport.threadFactory.bossThreadPrefix=NettyBoss

transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker

transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler

transport.threadFactory.shareBossWorker=false

transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector

transport.threadFactory.clientSelectorThreadSize=1

transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread

transport.threadFactory.bossThreadSize=1

transport.threadFactory.workerThreadSize=default

transport.shutdown.wait=3

service.vgroupMapping.fengmi_tx_group=default

service.default.grouplist=127.0.0.1:8091

service.enableDegrade=false

service.disableGlobalTransaction=false

client.rm.asyncCommitBufferLimit=10000

client.rm.lock.retryInterval=10

client.rm.lock.retryTimes=30

client.rm.lock.retryPolicyBranchRollbackOnConflict=true

client.rm.reportRetryCount=5

client.rm.tableMetaCheckEnable=false

client.rm.tableMetaCheckerInterval=60000

client.rm.sqlParserType=druid

client.rm.reportSuccessEnable=false

client.rm.sagaBranchRegisterEnable=false

client.tm.commitRetryCount=5

client.tm.rollbackRetryCount=5

client.tm.defaultGlobalTransactionTimeout=60000

client.tm.degradeCheck=false

client.tm.degradeCheckAllowTimes=10

client.tm.degradeCheckPeriod=2000

store.mode=db

store.publicKey=

store.file.dir=file_store/data

store.file.maxBranchSessionSize=16384

store.file.maxGlobalSessionSize=512

store.file.fileWriteBufferCacheSize=16384

store.file.flushDiskMode=async

store.file.sessionReloadReadSize=100

store.db.datasource=druid

store.db.dbType=mysql

store.db.driverClassName=com.mysql.jdbc.Driver

store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
store.db.user=root
store.db.password=778329

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

store.redis.mode=single

store.redis.single.host=127.0.0.1

store.redis.single.port=6379

store.redis.maxConn=10

store.redis.minConn=1

store.redis.maxTotal=100

store.redis.database=0

store.redis.password=

store.redis.queryLimit=100

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

client.undo.dataValidation=true

client.undo.logSerialization=jackson

client.undo.onlyCareUpdateColumns=true

server.undo.logSaveDays=7

server.undo.logDeletePeriod=86400000

client.undo.logTable=undo_log

client.undo.compress.enable=true

client.undo.compress.type=zip

client.undo.compress.threshold=64k

log.exceptionRate=100

transport.serialization=seata

transport.compressor=none

metrics.enabled=false

metrics.registryType=compact

metrics.exporterList=prometheus

metrics.exporterPrometheusPort=9898

properties中注意如下配置:

service.vgroupMapping.fengmi_tx_group=default // seata在不同项目中的隔离以组隔离

store.db.datasource=druid

store.db.dbType=mysql

store.db.driverClassName=com.mysql.jdbc.Driver

store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true // 需要预先在数据库中创建seata的数据库

store.db.user=root

store.db.password=XXXXXX

2.4 Mysql中创建seata数据库

在上面的流程中可以发现,seata管理分布式全局事务,当TM向TC发起全局事务时,需要在一个global_table的表中创建一条记录,产生一个xid,其与我们的事务操作的主键捆绑形成一个全局事务id,这个全局事务id会在各分支事务中传播,所以这些seata所需的表是必须的,官方已提供这些表的格式,下面我们就来看看

seata中的表

  • branch_table : 用于分支事务相关信息记录
  • global_table : 用于全局事务相关信息记录(最重要的就是生成xid,即全局id)
  • lock_table : 全局锁表,用于记录全局锁的信息

在这里插入图片描述

  • 创建表:
-- -------------------------------- 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(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;
2.5 启动Seata Server服务

在这里插入图片描述

  • 本地seata中找到seata文件夹下的bin目录,运行seata-server.bat,可通过cmd指定-p端口和-h地址,但地址必须要与我们的微服务保持一致,否则其他微服务无法注册到seata上(这里最好不要使用127.0.0.1 直接使用局域网真实ip地址)
  • 别忘了seata是在哪运行的,nacos必须先启动!!!

在这里插入图片描述

  • 看到以上画面,seata启动成功,也可以到nacos上看看我们的seata服务是否上线(默认端口8091)

在这里插入图片描述

至此,seata配置启动完毕,seata服务上线

使用Seata解决分布式事务

明确几点:

  1. seata需要配置在我们的微服务中,其本身也是一个微服务
  2. 在流程中的undo_log表,用于记录前镜像,后镜像,作为回滚依据的,要在此时创建,位置在我们操作的数据库中
  3. 在分布式的事务中,所有的微服务都是seata的客户端,seata作为服务端,那么就要告诉我们的微服务seata的地址,这就需要在微服务启动的配置文件中去配置了

1. 创建undo_log表

  • 下载地址: https://github.com/seata/seata/tree/develop/script/client/at/db
-- 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(20)   NOT NULL COMMENT 'branch transaction id',

    `xid`           VARCHAR(100) 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 = utf8 COMMENT ='AT transaction mode undo table';
  • 注意: 此表的位置在我们操作的数据库中,而非上面的seata数据库

在这里插入图片描述

从此表的结构中也可看出他是作为前镜像,后镜像的储存和回滚依据的

brand_id : 分支事务id

xid: 全局事务id

2. 在服务中依赖seata

2.1 pom依赖
<!-- seata -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-seata</artifactId>
    <version>2.2.0.RELEASE</version>
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.4.2</version>
</dependency>

注意seata的版本,我们这里使用的是1.4.2的版本,所以需要在依赖中更换版本,可以从maven中查看版本信息,务必要与自己使用的版本一致

2.2 配置seata信息
seata:
  # 开启seata
  enabled: true
  # properties中设定的组名,多个项目以组名隔离
  tx-service-group: fengmi_tx_group
  # 开启datasource的代理,用于产生前镜像,后镜像
  enable-auto-data-source-proxy: true
  config:
    type: nacos
    nacos:
      # 告知微服务如何配置seata的地址
      server-addr: localhost:8848  # seata配置文件的地址
      username: nacos              # 因为在nacos上要告知nacos的用户名和密码
      password: nacos
      group: DEFAULT_GROUP         # 配置所在的组
      data-id: seataServer.properties # 配置文件名
      namespace: pro                  # 配置文件所在的命名空间
  registry:
    type: nacos
    nacos:
      # 告知微服务seata的服务地址
      application: seata-server   # seata的服务名
      namespace: pro              # seata服务的命名空间
      group: DEFAULT_GROUP        # seata服务的组
      username: nacos
      password: nacos
      server-addr: localhost:8848 # seata服务的地址      

在这里插入图片描述

可以看到,seata与其他的微服务并不在一个组内,在我们之前学习nacos时,只有同组才可以调用,这里不同组为什么还能调用呢?这就必须明确上面的配置中的一项配置

registry.nacos.group: DEFAULT_GROUP 这个配置项,是在告知我们的微服务从哪里去找到seata的服务,也就是虽然不同组,但依然能通过此配置去找到我们的seata,从而注册到seata上

2.3 启动微服务

启动微服务后可以在启动日志或seata中发现我们的微服务已经注册到seata上了

2.3.1 goods微服务
  • 操作的表——goods

在这里插入图片描述

  • pom依赖
<dependencies>
     <!-- 每一个微服务都是一个独立的进程,需要提供接口http访问 -->
     <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>com.alibaba.cloud</groupId>
         <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
     </dependency>
     <!-- 配置中心起步依赖 -->
     <dependency>
         <groupId>com.alibaba.cloud</groupId>
         <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
     </dependency>
     <!-- sentinel 起步依赖 -->
     <dependency>
         <groupId>com.alibaba.cloud</groupId>
         <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
     </dependency>
     <!-- push模式依赖 -->
     <dependency>
         <groupId>com.alibaba.csp</groupId>
         <artifactId>sentinel-datasource-nacos</artifactId>
     </dependency>
     <!-- 链路追踪场景依赖 -->
     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-sleuth</artifactId>
     </dependency>
     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-zipkin</artifactId>
     </dependency>
     <!-- 数据库相关 -->
     <dependency>
         <groupId>com.baomidou</groupId>
         <artifactId>mybatis-plus-boot-starter</artifactId>
         <version>3.4.0</version>
     </dependency>
     <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>
     </dependency>
     <dependency>
         <groupId>com.alibaba</groupId>
         <artifactId>druid-spring-boot-starter</artifactId>
         <version>1.1.10</version>
     </dependency>
     <dependency>
         <groupId>com.qf</groupId>
         <artifactId>cloud-entity</artifactId>
         <version>0.0.1-SNAPSHOT</version>
     </dependency> 
     <!-- openfeign 远程调用依赖 -->
     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-openfeign</artifactId>
     </dependency>
     <dependency>
         <groupId>com.qf</groupId>
         <artifactId>cloud-api</artifactId>
         <version>0.0.1-SNAPSHOT</version>
     </dependency>
     <!-- seata -->
     <dependency>
         <groupId>com.alibaba.cloud</groupId>
         <artifactId>spring-cloud-alibaba-seata</artifactId>
         <version>2.2.0.RELEASE</version>
         <exclusions>
             <exclusion>
                 <groupId>io.seata</groupId>
                 <artifactId>seata-spring-boot-starter</artifactId>
             </exclusion>
         </exclusions>
     </dependency>
     <dependency>
         <groupId>io.seata</groupId>
         <artifactId>seata-spring-boot-starter</artifactId>
         <version>1.4.2</version>
     </dependency>
</dependencies>
  • mapper层
public interface GoodsMapper extends BaseMapper<Goods> {

}
  • service层实现类
@Service
public class GoodsService extends ServiceImpl<GoodsMapper, Goods> implements IGoodsService {
    @Override
    public ResultVo operationForGoodsStock(String goodsId, Integer buyNum) {
        // 非空判断
        if (StringUtils.isEmpty(goodsId)) {
            return new ResultVo(false, "参数不合法");
        }
        // 根据商品id查询商品信息
        Goods goods = this.baseMapper.selectById(goodsId);
        //  非空判断
        if (goods == null) {
            return new ResultVo(false, "商品不存在!!!");
        }
        // 商品存在,对比库存是否匹配
        if (goods.getGoodsStock() < buyNum) {
            return new ResultVo(false, "商品库存不足!!!");
        }
        // 库存满足购买量,操作减少库存
        goods.setGoodsStock(goods.getGoodsStock() - buyNum);
        // 获取总价
        Double totalPrice = goods.getGoodsPrice()* buyNum;
        // 操作到数据库中
        int i = this.baseMapper.updateById(goods);

        return new ResultVo(i > 0 ? true : false, i > 0 ? "商品库存扣减成功" : "商品库存扣减失败",totalPrice);
    }
}
  • 响应实体类ResultVo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultVo<T> {

    @NonNull
    private boolean success;
    @NonNull
    private String msg;

    private T data;

    public ResultVo(@NonNull boolean success, @NonNull String msg) {
        this.success = success;
        this.msg = msg;
    }
}
  • controller层
@RestController
@RequestMapping("goods")
public class GoodsController {
	// 模拟商品库存操作场景
    @RequestMapping("operationStock/{goodsId}/{buyNum}")
    public ResultVo operationStock(@PathVariable String goodsId, @PathVariable Integer buyNum) {
        return goodsService.operationForGoodsStock(goodsId,buyNum);
    }
}    
  • 由于goods微服务需要被其他服务调用,需要做openfeign的接口声明,在统一的模块中进行声明即可

在这里插入图片描述

@FeignClient("cloud-goods")
@RequestMapping("goods")
public interface GoodsApi {
  
    @RequestMapping("operationStock/{goodsId}/{buyNum}")
    public ResultVo operationStock(@PathVariable String goodsId, @PathVariable Integer buyNum);
}

还是那句话:

OpenFeign的接口声明记得分包!!!

2.3.2 order微服务
  • pom依赖与之前的goods一样,这里就不赘叙了
  • 操作表—user_order

在这里插入图片描述

  • mapper层
public interface OrderMapper extends BaseMapper<UserOrder> {
    
}
  • service层实现类
@Service
public class OrderService extends ServiceImpl<OrderMapper, UserOrder> implements IOrderService {
    // 注入openfeign远程接口
    @Autowired
    private GoodsApi goodsApi;

    @Override
    @GlobalTransactional // seata提供全局事务注解
    public ResultVo saveOrder(String goodsId, Integer buyNum) {
        // 非空判断
        if (StringUtils.isEmpty(goodsId)) {
            return new ResultVo(false, "参数不合法");
        }

        // 1. 操作库存
        ResultVo resultVo = goodsApi.operationStock(goodsId, buyNum);


        // 2. 插入订单信息
        UserOrder order = new UserOrder();
        order.setCreateTime(new Date().toString());
        order.setOrderBuynum(buyNum);
        order.setOrderId(UUID.randomUUID().toString());
        order.setOrderAddress("湖北省武汉市XXX");
        order.setOrderUsername("XXX");
        order.setOrderTotalprice((Double) resultVo.getData());

        this.baseMapper.insert(order);

        return new ResultVo(true, "订单下单成功");
    }
}

此处加上的 @GlobalTransactional 正是seata提供的,用于处理分布式全局事务,这个方法即我们TM的边界

  • controller层
@RestController
@RequestMapping("order")
@RefreshScope
public class OrderController {
    // 模拟场景,下订单,思路:
    // 1. 根据商品id去查询商品,查看库存和购买量是否匹配
    // 2. 操作商品表,减去对应库存
    // 3. 订单模块往数据库添加一条记录
    // 上述两个操作要在一个事务中
    @RequestMapping("saveOrder/{goodId}/{buyNum}")
    public ResultVo saveOrder(@PathVariable String goodId, @PathVariable Integer buyNum) {
        return orderService.saveOrder(goodId, buyNum);
    }
}
2.3.3 启动日志
2021-10-17 11:05:34.629  INFO [cloud-goods,,,] 70600 --- [           main] com.alibaba.nacos.client.naming          : new ips(1) service: DEFAULT_GROUP@@seata-server@@default -> [{"instanceId":"192.168.54.108#8091#default#DEFAULT_GROUP@@seata-server","ip":"192.168.54.108","port":8091,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"default","serviceName":"DEFAULT_GROUP@@seata-server","metadata":{},"ipDeleteTimeout":30000,"instanceHeartBeatTimeOut":15000,"instanceHeartBeatInterval":5000,"instanceIdGenerator":"simple"}]
2021-10-17 11:05:34.632  INFO [cloud-goods,,,] 70600 --- [           main] com.alibaba.nacos.client.naming          : current ips:(1) service: DEFAULT_GROUP@@seata-server@@default -> [{"instanceId":"192.168.54.108#8091#default#DEFAULT_GROUP@@seata-server","ip":"192.168.54.108","port":8091,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"default","serviceName":"DEFAULT_GROUP@@seata-server","metadata":{},"ipDeleteTimeout":30000,"instanceHeartBeatTimeOut":15000,"instanceHeartBeatInterval":5000,"instanceIdGenerator":"simple"}]
2021-10-17 11:05:34.636  INFO [cloud-goods,,,] 70600 --- [           main] i.s.c.r.netty.NettyClientChannelManager  : will connect to 192.168.54.108:8091
2021-10-17 11:05:34.637  INFO [cloud-goods,,,] 70600 --- [           main] i.s.c.rpc.netty.RmNettyRemotingClient    : RM will register :jdbc:mysql://127.0.0.1:3306/cloud-demo
2021-10-17 11:05:34.639  INFO [cloud-goods,,,] 70600 --- [           main] i.s.core.rpc.netty.NettyPoolableFactory  : NettyPool create channel to transactionRole:RMROLE,address:192.168.54.108:8091,msg:< RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/cloud-demo', applicationId='cloud-goods', transactionServiceGroup='fengmi_tx_group'} >
2021-10-17 11:05:35.285  INFO [cloud-goods,,,] 70600 --- [           main] i.s.c.rpc.netty.RmNettyRemotingClient    : register RM success. client version:1.4.2, server version:1.4.2,channel:[id: 0xf9b85a1a, L:/192.168.54.108:59048 - R:/192.168.54.108:8091]
2021-10-17 11:05:35.291  INFO [cloud-goods,,,] 70600 --- [           main] i.s.core.rpc.netty.NettyPoolableFactory  : register success, cost 48 ms, version:1.4.2,role:RMROLE,channel:[id: 0xf9b85a1a, L:/192.168.54.108:59048 - R:/192.168.54.108:8091]

截取部分微服务的启动日志,可以看出:我们的微服务找到了seata的服务地址,并且以RM的角色注册到了seata上,那么我们的分布式全局事务就可以实现了

  • 也可以再seata的运行日志中看到我们的客户端上线,及其角色

在这里插入图片描述

2.4 测试

在orderservice中加入异常 int i = 1/0 ; 然后通过查看数据库是否变化,来测试我们的seata是否为我们解决了分布式事务,此处就不做测试展示了

Seata的写隔离

  • 通过官网提供的图解,具体分析Seata的写隔离:

在这里插入图片描述

此图展示的是在并发的情况下,Seata是如何通过写隔离来解决脏写的问题的,流程如下:

  1. 对于原本的数据:id:1 , m:1000; tx1 , tx2 两个全局事务都想将值减少100
  2. 假设 tx1 事务先进入程序,获取到本地锁,他会先进行设置sql 语句,但是如我们最开始的流程中所说的那样,要执行修改sql的前提是要获取全局锁,这时 tx2 事务还未进入,tx1 获取到全局锁,执行了sql , 释放了本地锁
  3. 此时 tx2 事务进入程序,也想要修改数据,于是去获取到本地锁,设置sql语句,但此时的tx1 还未进行全局提交或回滚,全局锁依然在tx1 处, tx2 想要执行sql 语句就必须获取到全局锁,于是 tx2 进入自旋,等待获取全局锁
  4. 之后有两种情况,一种是TX1的事务边界内无异常,全局事务提交,另一种就是TX1的事务边界内有异常,全局事务回滚
  5. 上图展示的是tx1全局事务提交的情况,tx1 的TM发现事务边界内无异常,向TC发起全局事务提交,TC驱动RM进行分支事务的提交
  6. tx1 的RM分支在接收到TC的驱动后,进行分支事务提交,之后释放全局锁,tx2 分支自旋等待 tx1 释放了的全局锁,执行sql语句,进行全局事务提交 ,最后释放本地锁,再释放全局锁,数据改为800,两个全局事务操作都成功

在这里插入图片描述

接上面,

  1. 若 tx1 的全局事务中发生异常,那么 tx1 的TM会向TC发起全局事务回滚,TC驱动RM进行分支回滚,这时 tx1 需要进行回滚,通过branch_id (分支id) 和 xid (全局id)在 undo_log 中获取前镜像数据,生成反向补偿的sql语句, 而sql 语句的执行需要本地锁,此时的本地锁被 tx2 的进程持有,这就会形成: tx1 需要本地锁执行回滚sql语句, tx2 需要全局锁进行提交
  2. 而全局锁是有一个等待超时时间的,细心的小伙伴可以看到global_table 的表中有一个timeout的字段,那个字段就是设置全局锁的超时时间
  3. tx2 等待全局锁必定时间超时,此时 tx2 会放弃全局提交,sql语句并未执行,同时释放本地锁, tx1 重新获取本地锁, 进行全局事务的回滚,最终数据还是1000,未被改变,保证了事务的一致性 ,有效防止了脏写的问题

Seata的读隔离

  • 在我们之前学习的Mysql数据库中,默认的隔离级别是 可重复度读, oracle 的默认隔离级别是 读已提交,而Seata AT模式在本地数据库的隔离级别是读已提交或以上时,全局事务默认隔离级别是 读未提交
  • 看看官方给出的图解:

在这里插入图片描述

此图展示的是: 在高并发的情况下,解决Seata的脏读问题,目前只能通过SELECT FOR UPDATE 的sql语句来让Seata代理,将全局事务的隔离级别升到读已提交

  1. 由图可知 tx1 是一个写事务, tx2 是一个读事务
  2. tx1 进入程序,要改变数据,从1000 改到 900 ,他会先获取本地锁,之后获取全局锁,进行写操作持久化,此时, tx2 进入程序,由于我们的sql 语句后面添加了"FOR UPDATE" ,Seata会捕捉到此条sql,这条sql不会立马执行,而是由Seata进行代理执行
  3. tx1 事务进行了持久化操作之后,全局锁依然在 tx1 处,tx1 释放本地锁, 而Seata代理的sql要执行先获取了本地锁,查询需要全局锁,从而tx2 释放本地锁,等待全局锁
  4. 接下来还是分为两种情况,tx1 的接收到全局提交或全局回滚,进行相应的操作,全局提交就直接提交,之后释放全局锁,tx2 再获取全局锁,读到的就是修改之后的数据
  5. 图中展示了 tx1 发生异常,需要全局回滚的情况,与之前的写隔离不同,由于tx2 是一个读事务,释放了本地锁,这里 tx1 可以获取到 本地锁,进行全局事务的回滚,之后释放全局锁,数据未发生改变
  6. 之后 tx2 获取到全局锁,执行sql ,读取数据,是回滚后的数据,由此,避免了脏读的问题
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值