Seata 分布式事务入门篇之 XA 模式

1.分布式事务问题

1.1.本地事务

本地事务,也就是传统的单机事务。在传统数据库事务中,必须要满足四个原则:

Seata 分布式事务入门篇之 XA 模式

1.2.分布式事务

分布式事务,就是指不是在单个服务或单个数据库架构下,产生的事务,例如:

  • 跨数据源的分布式事务

  • 跨服务的分布式事务

在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。例如电商行业中比较常见的下单付款案例:

  • 创建新订单

  • 扣减商品库存

  • 扣减会员积分

完成上面的操作需要访问三个不同的微服务和三个不同的数据库。

订单的创建、库存的扣减、扣减会员积分在每一个服务和数据库内是一个本地事务,可以保证ACID原则。

但是当我们把三件事情看做一个”业务”,要满足保证“业务”的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务了。此时ACID难以满足,这是分布式事务要解决的问题。

2.理论基础

解决分布式事务问题,需要一些分布式系统的基础知识作为理论指导。

2.1.CAP定理

Seata 分布式事务入门篇之 XA 模式

Consistency(一致性): 用户访问分布式系统中的任意节点,得到的数据必须一致
Availability(可用性): 用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
Partition(分区): 因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
tolerance(容错): 在集群出现分区时,整个系统也要持续对外提供服务

1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。

  • Consistency(一致性)

  • Availability(可用性)

  • Partition tolerance (分区容错性)

它们的第一个字母分别是 C、A、P。

这三个指标不可能同时做到。这个结论就叫做 CAP 定理。

CP : 强一致性,弱可用性(牺牲部分机器的可用性,保证数据一致性)–如zookeeper、es

AP : 强可用性,弱一致性(牺牲一致性,保证可用性)–如Eureka

2.1.1.一致性

Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。

比如现在包含两个节点,其中的初始数据是一致的:

Seata 分布式事务入门篇之 XA 模式

当我们修改其中一个节点的数据时,两者的数据产生了差异,要想保住一致性,就必须实现节点1到节点2的数据同步。

Seata 分布式事务入门篇之 XA 模式

2.1.2.可用性

Availability (可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。

如图,有三个节点的集群,访问任何一个都可以及时得到响应:

Seata 分布式事务入门篇之 XA 模式

当有部分节点因为网络故障或其它原因无法访问时,代表节点不可用,但其他可用节点可以继续提供服务。

Seata 分布式事务入门篇之 XA 模式

2.1.3.分区容错

Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。

Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务

Seata 分布式事务入门篇之 XA 模式

2.1.4.矛盾

在分布式系统中,系统间的网络不能100%保证健康,一定会有故障的时候,而服务有必须对外保证服务。因此分区容错性不可避免。

当节点接收到新的数据变更时,就会出现问题了:

Seata 分布式事务入门篇之 XA 模式

如果此时要保证一致性,就必须等待网络恢复,完成数据同步后,整个集群才对外提供服务,服务处于阻塞状态,不可用。

如果此时要保证可用性,就不能等待网络恢复,那节点1、节点2与节点3之间就会出现数据不一致。

也就是说,在P一定会出现的情况下,A和C之间只能实现一个。这就是CAP定理。

2.2.BASE理论

BASE理论是对CAP的一种解决思路,包含三个思想:

  • Basically Available (基本可用): 分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。

  • Soft State(软状态): 在一定时间内,允许出现中间状态,比如数据临时的不一致状态。

  • Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。

2.3.解决分布式事务的思路

分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论,有两种解决思路:

  • AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。

  • CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。

但不管是哪一种模式,都需要在子系统事务之间互相通讯,协调事务状态,也就是需要一个事务协调者(TC)

Seata 分布式事务入门篇之 XA 模式

这里的子系统事务,称为分支事务,如上图中的订单服务、库存服务都为一个子事务,有关联的各个分支事务在一起称为全局事务

3.Alibaba分布式事务组件Seata

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

Seata 分布式事务入门篇之 XA 模式

官网地址:http://seata.io/

3.1.Seata的架构

Seata事务管理中有三个重要的角色:

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

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

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

整体的架构如图:

Seata 分布式事务入门篇之 XA 模式

Seata基于上述架构提供了四种不同的分布式事务解决方案:

  • XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入

  • TCC模式:最终一致的分阶段事务模式,有业务侵入

  • AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式

  • SAGA模式:长事务模式,有业务侵入

无论哪种方案,都离不开事务协调者,我们下面每一种模式都会讲,比较主要的是AT。

4.部署Seata

4.1.下载解压

首先我们要下载seata-server包,地址: http😕/seata.io/zh-cn/blog/download.html

Seata 分布式事务入门篇之 XA 模式

因为我们是直接在windows上跑,所以下的zip压缩包。在非中文目录解压zip包,其目录结构如下:

Seata 分布式事务入门篇之 XA 模式

4.2.修改seata配置文件

修改conf目录下的registry.conf文件,copy以下配置,如果 nacos 的 serverAddr 不一致 记得修改

registry {
# tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等
type = "nacos"


发布

nacos {
# seata tc 服务注册到 nacos的服务名称,可以自定义 spring.application.name
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "SH"
username = "nacos"
password = "nacos"
}
}

config {
# 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取,这样如果tc是集群,可以共享配置
type = "nacos"
# 配置nacos地址等信息
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}

4.3.在nacos添加配置

为了让tc服务的集群可以共享配置,我们选择了nacos作为统一配置中心。因此服务端配置文件seataServer.properties文件需要在nacos中配好。

Seata 分布式事务入门篇之 XA 模式

配置内容如下:

# 数据存储方式,db代表数据库
store.mode=db
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&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
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
# 事务、日志等配置
server.recovery.committingRetryPeriod=60000
server.recovery.asynCommittingRetryPeriod=60000
server.recovery.rollbackingRetryPeriod=60000
server.recovery.timeoutRetryPeriod=60000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000

# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

这里我们使用的数据存储方式是MySQL,所以这里会有数据库的配置,需要跟你本地的数据库对应上:名称、用户名、密码。

另外还有三个表:global_table、branch_table、lock_table,分别是全局事务表、分支事务表及全局锁信息表,这三个表也需要提前创建好

4.4.创建数据库表

tc服务在管理分布式事务时,需要记录事务相关数据到数据库中,你需要提前创建好这些表。

新建一个名为seata的数据库,执行以下sql语句,这些表主要记录全局事务、分支事务、全局锁信息。这些表我们后面讲AT模式的时候会用到

-- -------------------------------- 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;

4.5.启动nacos注册中心

nacos注册中心直接使用我们之前在本地安装好的就可以了,如果不懂可以去看一下前面的nacos章节,有详细的介绍

Seata 分布式事务入门篇之 XA 模式

4.6.启动TC服务

进入bin目录,运行其中的seata-server.bat即可:

Seata 分布式事务入门篇之 XA 模式

启动成功后,seata-server应该已经注册到nacos注册中心了。

打开浏览器,访问nacos地址:http://localhost:8848,然后进入服务列表页面,可以看到seata-tc-server的信息:

Seata 分布式事务入门篇之 XA 模式

5.微服务集成seata

我们这里使用一个alibaba官网提供的一个demo做演示,有需要的直接下载就行:

https://pan.baidu.com/s/1ODzKZb-9wfPGfzFFsE5kJg?pwd=wspt 

提取m:wspt

也可以用你自己已有的微服务项目做尝试。

Seata 分布式事务入门篇之 XA 模式

大致看一下代码逻辑,主要是三个服务类

1.AccountServiceImpl,扣款业务类

package cn.qingzhou.account.service.impl;

import cn.qingzhou.account.mapper.AccountMapper;
import cn.qingzhou.account.service.AccountService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
* @author qingzhuo
*/

@Slf4j
@Service
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountMapper accountMapper;

@Override
public void deduct(String userId, int money) {
log.info("开始扣款");
try {
accountMapper.deduct(userId, money);
} catch (Exception e) {
throw new RuntimeException("扣款失败,可能是余额不足!", e);
}
log.info("扣款成功");
}
}

2.OrderServiceImpl,订单业务类,创建订单的入口,其他服务的聚合。

GlobalTransactional注解: 也就是seata的全局事务注解,只有在入口这里贴上这个注解全局事务才会生效。

package cn.qingzhou.order.service.impl;

import cn.qingzhou.order.client.AccountClient;
import cn.qingzhou.order.client.StorageClient;
import cn.qingzhou.order.entity.Order;
import cn.qingzhou.order.mapper.OrderMapper;
import cn.qingzhou.order.service.OrderService;
import feign.FeignException;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
* @author qingzhou
*/

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

private final AccountClient accountClient;
private final StorageClient storageClient;
private final OrderMapper orderMapper;

public OrderServiceImpl(AccountClient accountClient, StorageClient storageClient, OrderMapper orderMapper) {
this.accountClient = accountClient;
this.storageClient = storageClient;
this.orderMapper = orderMapper;
}

@GlobalTransactional(rollbackFor = Exception.class, timeoutMills = 60000)
@Override
public Long create(Order order) {
// 创建订单
orderMapper.insert(order);
try {
// 扣用户余额
accountClient.deduct(order.getUserId(), order.getMoney());
// 扣库存
storageClient.deduct(order.getCommodityCode(), order.getCount());
} catch (FeignException e) {
log.error("下单失败,原因:{}", e.contentUTF8(), e);
throw new RuntimeException(e.contentUTF8(), e);
}
return order.getId();
}
}

3.StorageServiceImpl,库存业务类

package cn.qingzhou.storage.service.impl;

import cn.qingzhou.storage.mapper.StorageMapper;
import cn.qingzhou.storage.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
* @author qingzhou
*/

@Slf4j
@Service
public class StorageServiceImpl implements StorageService {

@Autowired
private StorageMapper storageMapper;

@Override
public void deduct(String commodityCode, int count) {
log.info("开始扣减库存");
try {
storageMapper.deduct(commodityCode, count);
} catch (Exception e) {
throw new RuntimeException("扣减库存失败,可能是库存不足!", e);
}
log.info("扣减库存成功");
}
}

5.1.seata依赖

因为三个服务都需要用到,所以分别在三个微服务中引入依赖。这里只做演示,直接引到父pom.xml也可以,如果是实际项目中的话建议还是单独引,因为项目中不一定所有微服务都会用到全局事务。

<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本较低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<!--seata starter 采用1.4.2版本-->
<version>${seata.version}</version>
</dependency>

5.2.配置TC地址

在三个微服务的 application.yml 中,配置TC服务信息,通过注册中心nacos,结合服务名称获取TC地址:nacos的server-addr、username、password记得要对应上。

我们这里先测试XA模式,seata默认为AT模式,AT模式和其余的我下一篇分享~

seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 127.0.0.1:8848 # nacos地址
namespace: "" # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
application: seata-tc-server # seata服务名称
username: nacos
password: nacos
tx-service-group: seata-demo # 事务组名称,根据这个获取tc服务的cluster名称
service:
vgroup-mapping: # 事务组与cluster的映射关系
seata-demo: SH
data-source-proxy-mode: XA # 开启XA模式,强一致性,此配置只需加在orderservice里

微服务如何根据这些配置寻找TC的地址呢?

我们知道注册到Nacos中的微服务,确定一个具体实例需要四个信息:

  • namespace:命名空间

  • group:分组

  • application:服务名

  • cluster:集群名

以上四个信息,在刚才的yaml文件中都能找到:

Seata 分布式事务入门篇之 XA 模式

namespace为空,就是默认的public

结合起来,TC服务的信息就是:public@DEFAULT_GROUP@seata-tc-server@SH,这样就能确定TC服务集群了。然后就可以去Nacos拉取对应的实例信息了。

5.3.数据库表

这里订单、扣款、库存都对应创建一个数据库:

1、db_account

2、db_order

3、db_storage

在三个数据库中,分别执行以下脚本:

------------db_account--------------
DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

------------db_order---------------
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

------------db_storage---------------
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) UNSIGNED DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

storage_tbl表的 count 字段设置为 UNSIGNED 无符号,这个的作用是防止超卖,当count小于1就直接抛异常。

给三个数据库都添加一个日志表,这个表也是在讲AT模式时会用到

DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci 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 INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;

给account表和storage表添加以下测试数据:

Seata 分布式事务入门篇之 XA 模式

Seata 分布式事务入门篇之 XA 模式

5.4.测试全局事务是否生效

Seata 分布式事务入门篇之 XA 模式

启动后 nacos 这里可以看到,服务都是成功注册了的

Seata 分布式事务入门篇之 XA 模式

测试浏览器或postman访问以下url测试:

http://localhost:8082/order?userId=user112233&commodityCode=storage112233&count=1&money=200

因为库存为10,当请求参数库存 count 为1的时候,这时候是足够扣的

Seata 分布式事务入门篇之 XA 模式

余额扣减成功

Seata 分布式事务入门篇之 XA 模式

订单创建成功

Seata 分布式事务入门篇之 XA 模式

库存扣减成功

Seata 分布式事务入门篇之 XA 模式

修改参数 count 为 10

http://localhost:8082/order?userId=user112233&commodityCode=storage112233&count=10&money=200

这时候库存count为9,而请求中count为10,显然是不够扣的

Seata 分布式事务入门篇之 XA 模式

后台报错,这是因为我们数据库设置了 count 字段为 UNSIGNED

Seata 分布式事务入门篇之 XA 模式
再看数据库,这里因为全局事务生效,所以余额的扣减和订单表都回滚成功,故没有变化

Seata 分布式事务入门篇之 XA 模式
Seata 分布式事务入门篇之 XA 模式

今天的分享就到这里,其他模式我在下一篇会分享,如果对你有帮助,记得一键三连~ 你的支持就是我最大的动力~~~~~


原文始发于微信公众号(轻洲技术):Seata 分布式事务入门篇之 XA 模式

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/242014.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!