失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > SpringCloud+Nacos1.4.2+Seata1.3.0实现分布式事务以及踩坑总结

SpringCloud+Nacos1.4.2+Seata1.3.0实现分布式事务以及踩坑总结

时间:2022-07-28 03:24:26

相关推荐

SpringCloud+Nacos1.4.2+Seata1.3.0实现分布式事务以及踩坑总结

文章目录

前言一、分布式事务问题二、Seata简介1,是什么2,能干嘛3,去哪下4,怎么玩三、Seata-Server安装1,官网地址2,下载版本3,解压修改file.conf文件4,数据库新建seata库5,seata库建表6,修改seata-server的registry.conf配置文件7,启动nacos端口88488,启动seata-server9,将配置导入到nacos四、业务数据库准备1,前提条件2,分布式事务业务说明3,创建业务数据库4,按照业务数据库创建业务表5,按照业务数据库创建对应的回滚日志表6,最终效果五、业务微服务准备1,业务需求2,新建订单Order-Module3,新建库存Storage-Module4,新建账户Account-Module六、测试1,数据库初始情况2,正常下单3,超时异常没加@GloblaTransactional4,超时异常添加加@GloblaTransactional七、补充Seata原理1,Seata2,再看TC/TM/RM三大组件3,AT模式如何做到对业务的无侵入(1)是什么(2)一阶段加载(3)二阶段提交(4)二阶段回滚4,debug调试查看5,补充总结总结

前言

一、分布式事务问题

分布式之前:

系统单机单库,不会出现分布式问题,随着业务的发展,系统越来越复杂,数据库也开始变化由一对一逐渐演变一对多,多对多,此时一个单一的系统无法支撑起整个应用,此时,系统逐渐被拆分成一个一个服务,每一个应用组成一个服务,微服务诞生。

分布式之后:

单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源。业务操作需要调用三个服务来完成,此时每个服务内部的数据一致性由本地本地事务来保证,但是全局的数据一致性问题没法保证

示意图如下

总之一句话:一次业务操作需要垮多个数据源或垮多个系统进行远程调用,就会产生分布式事务问题

二、Seata简介

1,是什么

官网地址https://seata.io/zh-cn/

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

2,能干嘛

1,术语

一个典型的分布式事务过程:分布式事务处理过程的 一ID+三组件模型(简称1+3模型)

XID (Transaction ID) : 全局唯一的事务ID

TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

2,处理过程

TM 向TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID

XID 在微服务调用链路的上下文中传播

RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖

TM 向TC 发起针对 XID 的全局提交或回滚决议

TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求

3,去哪下

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

4,怎么玩

单个应用我们只需要使用一个注解 @Transactional

分布式我们也只需要一个注解 @GlobalTransactional

用起来就这么简单,搞定!!!!

ps 往往使用起来越简单,往往越不简单

三、Seata-Server安装

1,官网地址

地址 https://seata.io/zh-cn/index.html

2,下载版本

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

3,解压修改file.conf文件

先备份初始文件再修改

我们这里使用数据库(版本不一致,file.conf也可能不一样)

store修改如下

4,数据库新建seata库

新版本的seata1.0以后没有sql文件需要自行去查找

链接地址 /seata/seata/tree/develop/script

5,seata库建表

-- -------------------------------- The script used when storeMode is 'db' ---------------------------------- the table to store GlobalSession dataCREATE TABLE IF NOT EXISTS `global_table`(`xid` VARCHAR(128) NOT NULL,`transaction_id` BIGINT,`status`TINYINTNOT 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 = InnoDBDEFAULT CHARSET = utf8;-- the table to store BranchSession dataCREATE 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 = InnoDBDEFAULT CHARSET = utf8;-- the table to store lock dataCREATE 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 = InnoDBDEFAULT CHARSET = utf8;

6,修改seata-server的registry.conf配置文件

后面我们会将配置上传到nacos,所以配置中心也需要修改如下

7,启动nacos端口8848

8,启动seata-server

启动时可能报错

(1)会报日志文件找不到的错误

解决办法创建一个logs文件,在文件目录下新建一个seata_gc.log

(2)报内存不够(报文未截图,后续补充下)

解决办法 修改启动参数

将其设置为1024,1024,1024,512即可

9,将配置导入到nacos

(1)修改 config.tx将配置注册到nacos中

下载下来是没有这个文件的所以要自己去下载/seata/seata/tree/1.3.0/script/config-center

启动脚本以及config.txt都有,注意目录结构不要放错了,否则启动不起来,或者自行修改启动脚本

(2)执行将config.txt推送到nacos中

sh nacos-config.sh -h 182.92.219.202 -p 8848 -g SEATA_GROUP -u nacos -w nacos -t 51915a62-d2d6-43d4-8f45-86b159eb90f5

参数配置说明如下

sh ${SEATAPATH}/script/config-center/nacos/nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 5a3c7d6c-f497-4d68-a71a-2e5e3340b3ca -u username -w password-h: host, the default value is localhost.-p: port, the default value is 8848.-g: Configure grouping, the default value is 'SEATA_GROUP'.-t: Tenant information, corresponding to the namespace ID field of Nacos, the default value is ''.-u: username, nacos 1.2.0+ on permission control, the default value is ''.-w: password, nacos 1.2.0+ on permission control, the default value is ''.

四、业务数据库准备

订单-库存-账户 业务数据库准备

1,前提条件

需要先启动Nacos后再启动Seata,两个服务必须先启动起来

2,分布式事务业务说明

参考地址 /seata/seata-samples/tree/master/seata-spring-boot-starter-samples

流程如下:

下订单–>扣库存–>减账户余额

创建三个服务:订单服务、库存服务、账户服务

具体流程如下:

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

流程说明:跨越三个数据库,有两次远程调用,会存在分布式事务问题

3,创建业务数据库

创建三个数据库

CREATE DATABASE 数据库名

存储订单的数据库 seata_order

存储库存的数据库 seata_storage

存储账户信息的数据库 seata_account

4,按照业务数据库创建业务表

存储订单的数据库 seata_order中

CREATE TABLE `t_order` (`id` bigint NOT NULL AUTO_INCREMENT,`user_id` bigint DEFAULT NULL COMMENT '用户id',`product_id` bigint DEFAULT NULL COMMENT '产品id',`count` int DEFAULT NULL COMMENT '数量',`money` decimal(11,0) DEFAULT NULL COMMENT '金额',`status` int DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结',PRIMARY KEY (`id`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表';

存储库存的数据库 seata_storage中

CREATE TABLE `t_storage` (`id` bigint NOT NULL AUTO_INCREMENT,`product_id` bigint DEFAULT NULL COMMENT '产品id',`total` int DEFAULT NULL COMMENT '库存',`used` int DEFAULT NULL COMMENT '已用库存',`residue` int DEFAULT NULL COMMENT '剩余库存',PRIMARY KEY (`id`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='库存表';

存储账户信息的数据库 seata_account中

CREATE TABLE `t_account` (`id` bigint NOT NULL AUTO_INCREMENT,`user_id` bigint DEFAULT NULL COMMENT '用户id',`total` decimal(10,0) DEFAULT NULL COMMENT '总额度',`used` decimal(10,0) DEFAULT NULL COMMENT '已用额度',`residue` decimal(10,0) DEFAULT NULL COMMENT '剩余可用额度',PRIMARY KEY (`id`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户表';

5,按照业务数据库创建对应的回滚日志表

每个库建一个

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

CREATE TABLE `undo_log` (`id` bigint NOT NULL AUTO_INCREMENT,`branch_id` bigint NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8;

6,最终效果

五、业务微服务准备

订单-库存-账户 业务微服务准备

1,业务需求

下订单–>减库存–>扣余额–>改(订单)状态

2,新建订单Order-Module

源码地址 /jn-acheng/spring-cloud-study/tree/master/spring-cloud-seata-order-8101

(1)新建module

spring-cloud-seata-order-8101

版本 (父pom)

(2)POM

只列出核心pom依赖

<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><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></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>

(3)YML

server:port: 8101spring:application:name: seata-orderdatasource:url: jdbc:mysql://192.168.119.50:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTCusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Drivercloud:nacos:discovery:server-addr: 192.168.119.50:8848username: nacospassword: nacosgroup: SEATA_GROUP # 分组# namespace: 193f80d6-57e9-4718-87ff-179335a50ac5 # 命名空间# sh nacos-config.sh -h 192.168.119.50 -p 8848 -g SEATA_GROUP -u nacos -w nacos -t 292c3d9d-b037-49e5-bfc6-1f14d648c743seata:enabled: trueapplication-id: ${spring.application.name}tx-service-group: my_test_tx_group #这里要特别注意和nacos中配置的要保持一直registry:type: nacosnacos:serverAddr: ${spring.cloud.nacos.discovery.server-addr}username: ${spring.cloud.nacos.discovery.username}password: ${spring.cloud.nacos.discovery.password}group: ${spring.cloud.nacos.discovery.group}config:type: nacosnacos:server-addr: ${spring.cloud.nacos.discovery.server-addr}username: ${spring.cloud.nacos.discovery.username}password: ${spring.cloud.nacos.discovery.password}group: ${spring.cloud.nacos.discovery.group}service:vgroup-mapping:my_test_tx_group: defaultmanagement:endpoints:web:exposure:include: '*'mybatis:mapper-locations: classpath:mapper/*.xmlconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

(4)代码

1,主启动类

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

2,订单pojo类

@Datapublic class Order {private Long id;/*** 用户id*/private Long userId ;/*** 产品id*/private Long productId;/*** 数量*/private int count;/*** '金额'*/private BigDecimal money;/*** 订单状态 0:创建中;1:已完结*/private int status;}

3,OrderMapper

@Mapperpublic interface OrderMapper {/*** 创建订单* @param order*/void create(Order order);/*** 修改订单状态* @param id* @param status*/void update(@Param("id") Long id,@Param("status") int status);}

4,OrderMapper.xml

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-////DTD Mapper 3.0//EN" "/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.acheng.mapper.OrderMapper"><resultMap id="BaseResultMap" type="com.acheng.pojo.Order"><id column="id" property="id" jdbcType="BIGINT"></id><result column="user_id" property="userId" jdbcType="BIGINT" /><result column="product_id" property="productId" jdbcType="BIGINT" /><result column="count" property="count" jdbcType="BIGINT" /><result column="money" property="money" jdbcType="DECIMAL" /><result column="status" property="status" jdbcType="INTEGER" /></resultMap><insert id="create" useGeneratedKeys="true" keyProperty="id">insert into t_order (user_id,product_id,count,money,status) values(#{userId},#{productId},#{count},#{money},#{status})</insert><update id="update">update t_order set status=#{status} where id=#{id}</update></mapper>

5,service接口,有三个接口

OrderService

public interface OrderService {void create(Order order);}

StorageService

@FeignClient("seata-storage")public interface StorageService {/*** 扣减库存*/@PostMapping("/storage/decrease")public void decrease(@RequestParam("productId") Long productId, @RequestParam("count")int count);}

AccountService

@FeignClient(value ="seata-account" )public interface AccountService {/*** 扣减金钱*/@PostMapping("/account/decrease")public void decrease(@RequestParam("userId") Long userId,@RequestParam("used") BigDecimal used);}

6,实现类

package com.acheng.service.impl;import com.acheng.mapper.OrderMapper;import com.acheng.pojo.Order;import com.acheng.service.AccountService;import com.acheng.service.OrderService;import com.acheng.service.StorageService;import io.seata.spring.annotation.GlobalTransactional;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;/*** @author LiuCheng* @data /5/13 15:53*/@Service@Slf4j@GlobalTransactional(name = "my_test_tx_group" ,rollbackFor = Exception.class)public class OrderServiceImpl implements OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate StorageService storageService;@Autowiredprivate AccountService accountService;@Overridepublic void create(Order order) {// 创建订单log.info("======= 创建订单 start");orderMapper.create(order);// 扣减库存log.info("======= 扣减库存 start");storageService.decrease(order.getProductId(),order.getCount());log.info("======= 扣减库存 end");// 扣减账户余额log.info("======= 扣减账户余额 start");accountService.decrease(order.getUserId(),order.getMoney());log.info("======= 扣减账户余额 end");// 修改订单状态log.info("======= 修改订单状态 start");int status=1;orderMapper.update(order.getId(),1);log.info("======= 修改订单状态 end");log.info("======= 创建订单 end");}}

7,控制层

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

3,新建库存Storage-Module

偷个懒,不贴代码了 o(╯□╰)o

源码地址 /jn-acheng/spring-cloud-study/tree/master/spring-cloud-seata-storage-8201

4,新建账户Account-Module

再偷个懒,不贴代码了 o(╯□╰)o

源码地址 /jn-acheng/spring-cloud-study/tree/master/spring-cloud-seata-account-8301

六、测试

下订单–>减库存–>扣余额–>改(订单)状态

1,数据库初始情况

2,正常下单

3,超时异常没加@GloblaTransactional

订单状态时0 未支付

库存被扣减了

账户余额扣减了

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

4,超时异常添加加@GloblaTransactional

订单未插入

库存未减少

金额未扣减

七、补充Seata原理

1,Seata

Seata是Fescar的升级版本,1月份蚂蚁金服和阿里巴巴共同开元的分布式解决方案 Seata

全称 Simple Extensible Autonomous Transaction Architecture。

2,再看TC/TM/RM三大组件

分布式事务的执行流程

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

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

(1)是什么

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

整体机制

两阶段提交协议的演变:

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

二阶段:

1,提交异步化,非常快速地完成。

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

(2)一阶段加载

在一阶段,Seata会拦截 “业务SQL”

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

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

1,解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。

2,查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。

select id, name, since from product where name = 'TXC';

3,执行业务 SQL:更新这条记录的 name 为 ‘GTS’。

4,查询后镜像:根据前镜像的结果,通过 主键 定位数据。

select id, name, since from product where id = 1;

5,插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。

{"branchId": 641789253,"undoItems": [{"afterImage": {"rows": [{"fields": [{"name": "id","type": 4,"value": 1}, {"name": "name","type": 12,"value": "GTS"}, {"name": "since","type": 12,"value": ""}]}],"tableName": "product"},"beforeImage": {"rows": [{"fields": [{"name": "id","type": 4,"value": 1}, {"name": "name","type": 12,"value": "TXC"}, {"name": "since","type": 12,"value": ""}]}],"tableName": "product"},"sqlType": "UPDATE"}],"xid": "xid:xxx"}

6,提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。

7,本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。

8,将本地事务提交的结果上报给 TC。

(3)二阶段提交

1,收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。

2,异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

(4)二阶段回滚

1,收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。

2,通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。

3,数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。

4,根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:

update product set name = 'TXC' where id = 1;

5,提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

4,debug调试查看

debug放行后,数据删除

5,补充总结

总结

如果觉得《SpringCloud+Nacos1.4.2+Seata1.3.0实现分布式事务以及踩坑总结》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。