Seata分布式事务学习笔记

一、Seata简介、安装

Seate是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务

官网:http://seata-io/zh-cn/

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

1、一个典型的分布式事务过程

  1. 分布式事务处理过程的ID+三组件模型

    1. 全局唯一的事务ID

    2. 三组件概念

      • Transaction Corrdinator(TC)事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚

      • Transaction Manager(TM)事务管理者:定义全局事务的范围,开始全局事务,提交或回滚全局事务

      • Resource Manager(RM)资源管理者:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚

  2. 处理过程

    1. TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
    2. XID在微服务调用链路的上下文中传播
    3. RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
    4. TM向TC发起针对XID的全局提交或回滚决议
    5. TC调度XID下管辖的全部分支事务完成提交或回滚请求

在这里插入图片描述

2、使用方法

  1. 本地@Transactional
  2. 全局@GlobalTransactional

seata的分布式交易解决方案
在这里插入图片描述

3、seata-server安装

1、官网地址

http://seata.io/zh-=ch下载seata-server-0.9.0.zip

2、seata-server-0.9.0.zip解压配置

  1. 先备份原始file.conf文件

  2. 主要修改:自定义事务组名称+事务日志存储模式为DB+数据库连接信息

  3. file.conf

    1. service模块

      service{
      	vgroup_mapping.my_test_tx_group="fsp_tx_group"
      	
      	default.grouplist="127.0.0.1:8091"
      	enableDegrade=false
      	disable=false
      	max.commit.retry.timeout="-1"
      	max.rollback.retry.timeout="-1"
      }
      
    2. store模块

      ## transaction log store
      store{
      	##store mode: file、db
      	mode="db"
      	
      	##file store
      	file{
      		dir = "sessionStore"
      		##分支session大小,如果超过了先尝试压缩lockkey,仍然超出抛出异常
      		max-branch-session-size = 16384
      		##如果超过全局会话大小,则引发异常
      		max-global-session-size = 512
      		##如果超过文件缓冲区大小,请分配新的缓冲区
      		file-write-buffer-cache-size = 16384
      		##恢复批读取大小时
      		session.reload.read_size = 100
      		#async,sync
      	}
      	
      	##database store
      	db{
      		##所使用连接池	
      		datasource = "dbcp"
      		db-type = "mysql"
      		driver-class-name = "com.mysql.jdbc.Driver"
      		url = "jdbc:mysql://127.0.0.1:3306/seata"
      		user = "root"
      		password = "123456"
      		min-conn = 1
      		max-conn = 3
      		global.table = "global_table"
      		branch.table = "branch_table"
      		lock-table = "lock_table"
      		query-limit = 100
      	}
      }
      

3、mysql数据库新建库seata

4、在数据库新建表

建表db_store.sql在\seata-server-0.9.0\seata\conf目录里

5、修改seata-server-0.9.0\seata\conf目录下registry.conf配置文件

registry{
	#file、nacos、eureka、redis、zk、consul、etcd3、sofa
	type = "nacos"
	
	nacos{
		serverAddr = "localhost:8848"
		namespace = ""
		cluster = "default"
	}
}

6、先启动Nacos在启动seata-server

二、seata原理简介

1、说明

0.9不支持集群,1.0以后支持,工作中还是要用1.0以后的版本

2、TC、TM、RM

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

分布式事务执行流程:

  1. TM开启分布式事务(TM向TC注册全局事务记录)
  2. 按业务场景,编排数据库、服务等事务内资源(RM向TC汇报资源准备状态);
  3. TM结束分布式事务,事务一阶段阶段(TM通知TC提交/回滚分布式事务)
  4. TC汇总事务信息,决定分布式事务时提交还是回滚
  5. TC通知所有RM提交/回滚资源,事务二阶段结束

3、AT模式如何做到对业务无侵入

AT模式

前提

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

整体机制

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源

    一阶段提交中,seata会拦截“业务SQL”

    1. 解析SQL语义,找到“业务SQL”要更新的业务数据,在业务数据被更新前,将其保存为“before image”
    2. 执行“业务SQL”更新业务数据,在业务数据更新之后
    3. 其保存成“after image”,最后生成行锁

    以上操作全部在一个数据库事务内完成,这样保证了一阶段提交的原子性。

在这里插入图片描述

  • 二阶段:

    • 提交异步化,非常快速的完成

    • 回滚通过一阶段的回滚日志进行反向补偿

      1. 二阶段提交如果时顺利提交的化

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

      在这里插入图片描述

      1. 二阶段回滚

        二阶段如果时回滚的化,seata就需要回滚一阶段已经执行的“业务sql”,还原业务数据

        回滚方式便是用“before image”还原业务数据,但是还原前首先要验证脏写,对比“数据库当前业务数据”和“after image”,如果两份数据完全一致说明没有脏写,可以还原数据,如果不一致就说明有脏写,出现脏写就需要转人工处理

      在这里插入图片描述

4、debug

在这里插入图片描述

5、补充

在这里插入图片描述

三、测试订单、库存、账户业务数据库准备

1、分布式事务也业务说明

在这里我们会创建三个服务,一个订单服务,一个库存服务一个账户服务>

当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减单商品的库存,在通过远程掉哦那个账户服务来扣减用户账户里面的月,最后在订单服务中修改订单状态为已完成

该操作跨越三个数据库,有两次远程调用,很明显的分布式事务问题

2、创建业务数据库

  1. seate_order:存储订单的数据库

    1. 创建t_order表

      create table 't_order'(
      	'id' bigint(11) not null auto_increment primary key,
          'user_id' bigint(11) default null comment '用户id',
          'product_id' bigint(11) default null comment '产品id',
          'count' int(11) default null comment '数量',
          'money' decimal(11,0) default null comment '金额',
          'status' int(1) default null comment '订单状态:0创建中 1已完结'
      )engine=innodb auto_increment=7 defaule charset=utf8;
      
      select * from t_order;
      
  2. seata_storage:存储库存的数据库

    1. 创建t_storage表

      create table 't_storage'(
      	'id' bigint(11) not null auto_increment primary key,
          'product_id' bigint(11) default null comment '产品id',
          'total' int(11) default null comment '总库存',
          'used' int(11) default null comment '已用库存',
          'residue' int(1) default null comment '剩余库存'
      )engine=innodb auto_increment=2 defaule charset=utf8;
      
      insert into seata_storage.t_storage('id','product_id','total','used','residue') values('1','1','100','0','100');
      
      select * from t_storage;
      
  3. seate_account:存储账户信息的数据库

    1. 创建t_account表

      create table 't_account'(
      	'id' bigint(11) not null auto_increment primary key,
          'user_id' bigint(11) default null comment '用户id',
          'total' int(11) default null comment '总额度',
          'used' int(11) default null comment '已用余额',
          'residue' int(1) default null comment '剩余可用额度'
      )engine=innodb auto_increment=2 defaule charset=utf8;
      
      insert into seate_account.t_account('id','user_id','used','residue') values('1','1','1000','0','1000');
      
      select * from t_account;
      

3、按照上述三库分别建立对应的回滚日志表

订单-库存-账户3个库下都需要建各自的回滚日志表

\seata-server-0.9.0\seata\conf目录下的db_undo_log.sql

drop table 'undo_log';

create table 'undo_log'(
	'id' bigint(20) not null auto_increment,
    'branch_id' bigint(20) not null,
    'xid' varchar(100) not null,
    'context'varchar(128) not null,
    'rollback_info' longblob not null,
    'log_status' int(11) not null,
    'log_created' datetime not null,
    'log_modified' datatime not null,
    'ext' varchar(100) default null,
    primary key('id'),
    unique key 'ux_undo_log' ('xid','branck_id')
)engine=innodb auto_increment=1 default charset=utf8;

3、最终结果展示

在这里插入图片描述

四、订单、库存、账户微服务准备

下订单—>减库存—>扣余额—>改订单状态

1、新建订单Order-Module

seate-order-service2001

1、添加pom配置

<!--nacos-->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
    	<exclusion>
            <groupId>io-seata</groupId>
        	<artifactId>seata-all</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
	<groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>0.9.0</version>
</dependency>

<!--feign-->
<dependency>
	<groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2、添加yml配置

server:
  port: 2001
  
spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        #自定义事务组名称需要与seata-server中的对应
        tx-service-group: fsp_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order
    username: root
    password: 123456

feign:
  hystrix:
    enabled: false

3、file.conf

resources下创建文件file.conf,从seate安装包中赋值配置文件内容

service{
	vgroup_mapping.my_test_tx_group="fsp_tx_group"
	
	default.grouplist="127.0.0.1:8091"
	enableDegrade=false
	disable=false
	max.commit.retry.timeout="-1"
	max.rollback.retry.timeout="-1"
}
## transaction log store
store{
	##store mode: file、db
	mode="db"
	
	##database store
	db{
		##所使用连接池	
		datasource = "dbcp"
		db-type = "mysql"
		driver-class-name = "com.mysql.jdbc.Driver"
		url = "jdbc:mysql://127.0.0.1:3306/seata"
		user = "root"
		password = "123456"
		min-conn = 1
		max-conn = 3
		global.table = "global_table"
		branch.table = "branch_table"
		lock-table = "lock_table"
		query-limit = 100
	}
}

4、registry.conf

resources下创建文件registry.conf,从seata安装包下复制

registry{
	#file、nacos、eureka、redis、zk、consul、etcd3、sofa
	type = "nacos"
	
	nacos{
		serverAddr = "localhost:8848"
		namespace = ""
		cluster = "default"
	}
}

5、创建实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>{
    private Integer code;
    private String message;
    private T data;
    
    public CommonResult(Integer code,String message){
		this(code,message,null);
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order{
    private Long id;
    private Long userId;
    private Long productId;
    private Integer count;
    priate BigDecimal money;
    
    /**
     * 订单状态: 0:创建中 1:已完结
     */
    private Integer status;
}

6、创建dao接口

@Mapper//xml文件省略
public interface OrderDao{
    
    //新建订单
    void create(order order);
    
    //修改订单状态,从0改为1	
    void update(@Param("userId") Long userId,@Param("status") Integer status);
}

7、创建serice接口实现

@FeignClient(vlue="seata-storage-serice")
public interface StorageSerice{
    @PostMapping(value="/storage/decrease")
    CommonResult descrease(@RequestParam("productId")Long productId,
                           @RequestParam("count")Integer count);
}
@FeignClient(vlue="seata-account-serice")
public interface AccountSerice{
    @PostMapping(value="/account/decrease")
    CommonResult descrease(@RequestParam("userId")Long userId,
                           @RequestParam("money")BigDecimal money);
}
@Service
public class OrderSericeImpl implements OrderService{
    @Resource
    private OrderDao orderDao;
    
    @Resource
    private StorageSerice storageService;
    
    @Resource
    private AccountService accountService;
        
    /**
     * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户月->修改订单状态
     */
    @Override
    @GlobalTransactional(nam="fsp-create-order",rollbackFor=Exception.class)
    public void create(Order order){
        log.info("------->1、新建订单");
        orderDao.create(order);
        log.info("------->2、订单服务开始调用库存,做扣减count");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("------->3、订单微服务开始调用账户,做扣减余额");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("------->4、修改订单状态");
        orderDao,update(order.getUserId(),1);
        log.info("------->5、订单结束");
    }
}

8、创建controller

@RestController
public class OrderController{
    @Resource
    private OrderService orderService;
    
    @GetMapping("/order/create")
    public CommonResult create(Order order){
        orderService.create(order);
        return new CommonResult(200,"订单创建完成");
    }
}

9、主启动类

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class SeataOrderMainApp2001{
    public static void main(String[] args){
        SpringApplication.run(SeataOrderMainApp2001.class,args);
    }
}

2、新建库存Storage-Module、账户Account-Module同上,省略

3、测试

在这里插入图片描述

1、正常下单

在这里插入图片描述

2、超时异常,没有加@GlobalTransaction

​ AccountServiceImpl添加超时

  • 当库存和账户进饿扣减后,订单状态没有设置为已经完成,没有从0改为1
  • 而且由于feign的重试机制,账户余额可能被多次扣减

3、超时异常,加了@GlobalTransaction

AccountServiceImpl添加超时,下单后数据库数据没有任何改变,分布式事务生效回滚了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值