文章目录
前言一、分布式事务问题二、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实现分布式事务以及踩坑总结》对你有帮助,请点赞、收藏,并留下你的观点哦!