Seata分布式

一、Seata定义:

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

对业务无侵入:即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入
高性能:减少分布式事务解决方案所带来的性能消耗

官方文档:https://seata.io/zh-cn/index.html

   seata的几种术语:

        TC(Transaction Coordinator):事务协调者。管理全局的分支事务的状态,用于全局性事务的提交和回滚。
        TM(Transaction Manager):事务管理者。用于开启、提交或回滚事务。
        RM(Resource Manager):资源管理器。用于分支事务上的资源管理,向 TC 注册分支事务,上报分支事务的状态,接收 TC 的命令来提交或者回滚分支事务。

seata涉及到三个角色之间的交互,本文通过流程图将AT模式下的基本交互流程梳理一下,为我们以后的解析打下基础。

 

2PC俩阶段提交协议

顾名思义,分为两个阶段: Prepare 和 Commit

缺点:

1.同步阻塞 参与者在等待协调者的指令时,其实是在等待其他参与者的响应,在此过程中,参与者是无法进行其他操作的,也就是阻塞了其运行。倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信息,那么会导致参与者一直阻塞下大。
2单点 在 2PC 中,一切请求都来自协调者,所以协调者的地位是至关重要的,如果协调者岩机,那么就会使参与者一直阻塞并一直占用事务资源

3 数据不一致 Commit 事务过程中 Commit请求/Rollback请求可能因为协调者宕机或协调者与参与者网络问题丢失,那么就导到了部分参与者设有收到 Commit/Rollback请求,而其他参与者则正常收到执行了 Commit/Rollback操作,没有收到请求的参与者则继续阻塞。这时,参与者之间的数据就不再一致了。当参与者执行 CommitRolback 后会协调者发送 Ack,然而协调者不论是否收到所有的参与者的 Adk,该事务也不会再有其他补救施了,协调者能做的也就是等待超时向事务发起者返回一个"我不确定该事务是否成功”。

二、AT 模式

整体机制

AT 模式是一种无侵入的分布式事务解决方案。
阿里seata框架,实现了该模式。
在AT 模式下,用户只需关注自己的“业务 SQL",用户的“业务 SQL"作为一阶段,Seata框 架会自动生成事务的二阶段提交和回滚

两阶段提交协议的演变:

  • 一阶段:Seata 会拦截”业务 SQL”,首先解析 SQL 语义,找到"业务 SQL"要更新的业务数据,在业务数据被更新前,将其保存成before image",然后执行“业务 SQL"更新业务数据,在业务数据更新之后,再将其保存成afterimage",最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

 

 

  • 二阶段:

    • 二阶段如果是提交的话,因为"业务 SQL"在一阶段已经提交至数据库,所以 seata框架只需将一阶段保存的快照数和行领删掉,完成数据清理即可

  • 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL,还原业务数据。回滚方式是用"before image"还原业务数据,但在还原前要首先要校验脏写,对比"数据库当前业务数据 和after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。 

三、TCC模式

1,侵入性比较强,并目得自己实现相关事务控制逻辑
2.在整个过程基本没有锁,性能更强
TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法。


四、搭建Seata TC协调者 

seata的协调者其实就是阿里开源的一个服务,我们只需要下载并且启动它。

下载地址:http://seata.io/zh-cn/blog/download.html

下载完成直接解压之后配置

创建TC所需要的表

        TC运行需要将事务的信息保存在数据库,因此需要创建一些表,找到seata-1.3.0源码的script\server\db这个目录,将会看到以下SQL文件:

 在数据库中直接运行mysql.sql文件

 修改TC的注册中心

找到seata-server-1.3.0\seata\conf这个目录,其中有一个registry.conf文件,其中配置了TC的注册中心和配置中心。

默认的注册中心是file形式,实际使用中肯定不能使用,需要改成Nacos形式,改动的地方如下图:

修改TC的配置中心

TC的配置中心默认使用的也是file形式,当然要是用nacos作为配置中心了。

直接修改registry.conf文件,需要改动的地方如下图:

上述配置修改好之后,在TC启动的时候将会自动读取nacos的配置。 

修改TC的数据库连接信息

TC是需要使用数据库存储事务信息的,那么如何修改相关配置呢?

上一节的内容已经将所有的配置信息都推送到了Nacos中,TC启动时会从Nacos中读取

 修改file.conf文件如下,mode 改为 db模式

当然Seata还支持Redis作为TC的数据库,只需要改动以下配置即可:

store.mode=redis
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.password=123456

修改\seata-server-1.3.0\seata\script\config-center

 配置分组  seata容错机制,配置之后我们也需要在idea中配置

\seata-server-1.3.0\seata\script\config-center

 在\seata-server-1.3.0\seata\script\config-center\nacos下执行将我们的seata配置文件注册到nacos的配置中心  默认注册到本地 8848

 如果要像linux中的nacos注册配置文件  右键使用git push命令打开输入命令即可  小编这里着重演示windows

 

到这里我们的seata TC模式客户端就配置完了,启动TC

按照上述步骤全部配置成功后,则可以启动TC,在seata-server-1.3.0\seata\bin目录下直接点击seata-server.bat(windows)运行。

启动成功后,在Nacos的服务列表中则可以看到TC已经注册进入,如下图:

四、Seata客户端搭建(RM)

上述已经将Seata的服务端(TC)搭建完成了,下面就以电商系统为例介绍一下如何编码实现分布式事务。

  • 仓储服务:对给定的商品扣除仓储数量。
  • 订单服务:根据采购需求创建订单。

仓储服务搭建

订单服务搭建

项目架构

导入依赖:

<dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-jdbc</artifactId>
       </dependency>
       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
           <scope>runtime</scope>
       </dependency>
       <dependency>
           <groupId>org.mybatis.spring.boot</groupId>
           <artifactId>mybatis-spring-boot-starter</artifactId>
           <version>2.1.1</version>
       </dependency>
       <!--nacos服务注册发现-->
       <dependency>
           <groupId>com.alibaba.cloud</groupId>
           <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
       </dependency>
       <!--添加openfeign依赖-->
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-openfeign</artifactId>
       </dependency>
       <!--添加seata的依赖-->
       <dependency>
           <groupId>com.alibaba.cloud</groupId>
           <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
       </dependency>

创建数据库:

controller层对比

@RestController
@RequestMapping("/order")
public class Ordercontroller {

    @Autowired
    OrderService orderService;

    @RequestMapping("/add")
    public String add(){
        Order order = new Order();
        order.setProduct_id(9);
        order.setStatus(0);
        order.setTotal_amount(100);
        orderService.create(order);
        return "下单成功";
    }
}
@RestController
@RequestMapping("/stock")
public class StockController {

    @Autowired
    StockService stockService;

    @RequestMapping("/reduct")
    public String add(@RequestParam("productId") Integer productId){
      stockService.reduct(productId);
      return "扣减库存";
    }

}

entity对比

package com.xinzhi.order.entity;

import org.springframework.stereotype.Component;

@Component
public class Order {
    private Integer id;
    private Integer product_id;
    private Integer total_amount;
    private Integer status;

    public Order() {
    }

    public Order(Integer id, Integer product_id, Integer total_amount, Integer status) {
        this.id = id;
        this.product_id = product_id;
        this.total_amount = total_amount;
        this.status = status;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getProduct_id() {
        return product_id;
    }

    public void setProduct_id(Integer product_id) {
        this.product_id = product_id;
    }

    public Integer getTotal_amount() {
        return total_amount;
    }

    public void setTotal_amount(Integer total_amount) {
        this.total_amount = total_amount;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }
}
package com.xinzhi.stock.entity;

import org.springframework.stereotype.Component;

@Component
public class Stock {
    private Integer id;
    private Integer product_id;
    private Integer total_amount;
    private Integer status;

    public Stock() {
    }

    public Stock(Integer id, Integer product_id, Integer total_amount, Integer status) {
        this.id = id;
        this.product_id = product_id;
        this.total_amount = total_amount;
        this.status = status;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getProduct_id() {
        return product_id;
    }

    public void setProduct_id(Integer product_id) {
        this.product_id = product_id;
    }

    public Integer getTotal_amount() {
        return total_amount;
    }

    public void setTotal_amount(Integer total_amount) {
        this.total_amount = total_amount;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }
}

mapper层对比

package com.xinzhi.order.mapper;

import com.xinzhi.order.entity.Order;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

@Repository
@Mapper
public interface OrderMapper {

    @Insert("insert into seata_order(id,product_id,total_amount,status) values(0,#{product_id},#{total_amount},#{status})")
    int insert(Order order);
}
package com.xinzhi.stock.mapper;

import com.xinzhi.stock.entity.Stock;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;

@Repository
@Mapper
public interface StockMapper {

    @Update("update seata_stock set count = count-1 where product_id = #{product_id} ")
    int reduct(@Param("product_id") Integer productId);
}

 service层对比

package com.xinzhi.order.service.impl;

import com.xinzhi.order.entity.Order;
import com.xinzhi.order.feign.Stock_seata;
import com.xinzhi.order.mapper.OrderMapper;
import com.xinzhi.order.service.OrderService;
import io.seata.spring.annotation.GlobalTransactional;
import org.apache.skywalking.apm.toolkit.trace.Tag;
import org.apache.skywalking.apm.toolkit.trace.Tags;
import org.apache.skywalking.apm.toolkit.trace.Trace;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

@Service
@GlobalTransactional
public class OrderServiceImpl implements OrderService {
    @Autowired
    OrderMapper orderMapper;

    @Autowired
    Stock_seata stock_seata;
    @Transactional
    @Override
    public Order create(Order order) {
        int insert = orderMapper.insert(order);
        Integer product_id = order.getProduct_id();
        System.out.println(product_id);
        String add = stock_seata.add(product_id);
        System.out.println(add);
        int a = 1/0;
        return null;
    }
package com.xinzhi.stock.service.impl;

import com.xinzhi.stock.entity.Stock;
import com.xinzhi.stock.mapper.StockMapper;
import com.xinzhi.stock.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class StockServiceImpl implements StockService {
    @Autowired
    StockMapper stockMapper;


    @Override
    public void reduct(Integer productId) {
        System.out.println("更新商品"+productId);
        stockMapper.reduct(productId);
        System.out.println("aaa");
//        int jian = stockMapper.jian(9);
//        System.out.println(jian);
    }
}

application.yml对比

server:
  port: 8036
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://127.0.0.1:3306/seata_order?characterEncoding=utf8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
  application:
    name: order-seata
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      discovery:
        username: nacos
        password: nacos
    alibaba:
      seata:
        tx-service-group: guangzhou #配置事务分组

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.xinzhi.order.entity
seata:
  registry:
    #配置seata的注册中心,告诉seata client 怎么去访问seata server
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      discovery:
        #        server-addr: 192.168.195.128:8847
        username: nacos
        password: nacos
        namespace: public
      group: SEATA_GROUP  #seata server 所在的组,默认就是SEATA_GROUP
  #配置seata的配置中心,可以读取关于seata client的一些配置
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      discovery:
        username: nacos
        password: nacos
        group: SEATA_GROUP
server:
  port: 8037
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://127.0.0.1:3306/seata?characterEncoding=utf8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
  application:
    name: stock-seata
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      discovery:
        username: nacos
        password: nacos
    alibaba:
      seata:
        tx-service-group: guangzhou #配置事务分组
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.xinzhi.stock.entity
seata:
  registry:
    #配置seata的注册中心,告诉seata client 怎么去访问seata server
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      discovery:
        #        server-addr: 192.168.195.128:8847
        username: nacos
        password: nacos
        namespace: public
      group: SEATA_GROUP  #seata server 所在的组,默认就是SEATA_GROUP
  #配置seata的配置中心,可以读取关于seata client的一些配置
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      username: nacos
      password: nacos
      group: SEATA_GROUP

order-seata服务创建feign接口来调用库存服务

package com.xinzhi.order.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "stock-seata",path = "/stock")
public interface Stock_seata {

    @RequestMapping("/reduct")
    String add(@RequestParam("productId") Integer productId);
}

项目启动当订单服务调用库存服务时,会报异常,此时可查看数据是否修改,注意是要加开启事务的全局注解的

  此时,看下库存的数据有没有扣减,很高兴,库存没有扣减成功,说明事务已经回滚了,分布式事务成功了。

总结
Seata客户端创建很简单,需要注意以下几点内容:

seata客户端的版本需要和服务端保持一致
每个服务的数据库都要创建一个undo_log回滚日志表


客户端指定的事务分组名称要和Nacos相同,比如service.vgroupMapping.seata-account-tx-group=default
前缀:service.vgroupMapping.
后缀:{自定义}

五、AT模式原理分析


AT模式最大的优点就是对业务代码无侵入,一切都像在写单体业务逻辑一样。

TC相关的三张表:

global_table:全局事务表,每当有一个全局事务发起后,就会在该表中记录全局事务的ID
branch_table:分支事务表,记录每一个分支事务的ID,分支事务操作的哪个数据库等信息
lock_table:全局锁

一阶段步骤

  1. TM:seata-order.create()方法执行时,由于该方法具有@GlobalTranscational标志,该TM会向TC发起全局事务,生成XID(全局锁)
  2. RM:orderMapper.insert(order) 写表UNDO_LOG记录回滚日志(Branch ID),通知TC操作结果
  3. RM:stock_seata.add(product_id)写表UNDO_LOG记录回滚日志(Branch ID),通知TC操作结果

RM写表的过程,Seata 会拦截业务SQL,首先解析 SQL 语义,在业务数据被更新前,将其保存成before image(前置镜像),然后执行业务SQL,在业务数据更新之后,再将其保存成after image(后置镜像),最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

二阶段步骤

因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

正常:TM执行成功,通知TC全局提交,TC此时通知所有的RM提交成功,删除UNDO_LOG回滚日志

 

异常:TM(分支事务资源管理器)执行失败,通知TC(事务协调者)全局回滚,TC此时通知所有的RM进行回滚,根据UNDO_LOG反向操作,使用before image还原业务数据,删除UNDO_LOG,但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

 

 AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写业务 SQL,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值