在了解分布式事务之前,让我们通过一个微服务的案例来演示一下分布式事务存在的问题。
首先,我准备了一个微服务的 demo 项目,这个项目的结构如下:
在这个Spring Cloud 项目中一共有三个服务:account-service
账户服务,order-service
订单服务和 storage-service
库存服务。其中order-service
负责下单业务,在下单时会调用订单服务,创建订单并写入数据库。然后订单服务调用账户服务和库存服务:
目前这三个服务涉及到了三种数据库表,它们的结构如下:
这三个服务的核心业务代码如下:
首先是 order-service
:
@Override
@Transactional
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();
}
然后是account-service
:
@Override
@Transactional
public void deduct(String userId, int money) {
log.info("开始扣款");
try {
accountMapper.deduct(userId, money);
} catch (Exception e) {
throw new RuntimeException("扣款失败,可能是余额不足!", e);
}
log.info("扣款成功");
}
最后是storage-service
:
@Transactional
@Override
public void deduct(String commodityCode, int count) {
log.info("开始扣减库存");
try {
storageMapper.deduct(commodityCode, count);
} catch (Exception e) {
throw new RuntimeException("扣减库存失败,可能是库存不足!", e);
}
log.info("扣减库存成功");
}
可以发现,这三个核心的业务代码都加上了 @Transactional
注解来开启事务,下面就通过 Postman 来演示一下这个过程。
在演示之前,先看看数据库中的数据:
首先是正常状态,即余额和库存都充足的情况:
通过 Postman 发送如下请求,此时指定商品的数量为 2,库存充足:
最终成功执行,然后让我们看一看数据库中的数据变化
然后让我们来演示一下当库存不足的情况下,是否会存在问题:
现在设置要扣减的库存为 10,而此时的库存数量为 8,则整个过程应该执行失败:
在上面的微服务中,一个业务跨越多个服务或数据源,每个服务都是一个分支事务,要保证所有分支事务最终状态一致,这样的事务就是分布式事务。
但是,上面的微服务上没有实现分布式事务,而只有分支事务,站在分支事务的角度上来看:
因此,要想保证整个业务的流程都能够正常执行,那么就需要采用分布式事务,即要么所有的分支事务都执行成功,要么都执行失败。
分布式事务是指在分布式系统中,多个独立的事务操作需要协同工作以维护数据的一致性和完整性
。在传统的单机事务中,ACID(原子性、一致性、隔离性、持久性)属性可以轻松满足,但在分布式系统中,由于涉及多个数据存储节点,实现分布式事务变得更加复杂。
关键问题包括如何协调各个参与者的事务、如何处理网络分区和节点故障、如何保证全局数据一致性等。因此,分布式事务需要特殊的技术和协议来管理和维护数据一致性,以确保分布式系统的正确运行。
CAP 定理是分布式系统理论中的一个重要概念,由计算机科学家 Eric Brewer 提出。CAP 定理指出,一个分布式系统的设计不能同时满足以下三个属性:
CAP 定理指出,在分布式系统中,最多只能满足其中的两个属性,而必须牺牲其中一个。这意味着在面对网络分区或故障时,要么保证一致性和可用性,但失去了分区容忍性;要么保证一致性和分区容忍性,但失去了可用性。这对分布式系统的设计和架构提出了重要挑战。
例如:ES 集群出现分区时,故障节点会被剔除集群,数据分片会重新分配到其它节点,保证数据一致。因此是低可用性,高一致性,属于CP 模式。
BASE 理论是对 CAP 定理的一种思考和解决方案,它的名称是对以下三个核心概念的缩写:
1. Basically Available(基本可用性): 这意味着分布式系统在出现故障时仍然能够保持基本的可用性。即使部分组件故障或数据出现不一致,系统仍然可以提供有限的服务,不会变得完全不可用。这允许系统在面临问题时继续对外提供核心功能,确保用户体验。
2. Soft State(软状态): BASE 理论承认在一定时间内,分布式系统的数据可以处于不一致的中间状态。这是因为各个节点在进行数据同步或处理时可能存在延迟、不同步的情况,导致数据不完全一致。允许出现软状态意味着系统可以容忍一段时间内的数据不一致,但要求最终达到一致状态。
3. Eventually Consistent(最终一致性): 最终一致性是 BASE 理论的核心概念,它强调虽然分布式系统不能保证强一致性,但最终所有节点的数据将达到一致状态。系统会采取一系列措施来保证数据最终一致,例如定期数据同步、修复数据不一致等。这种方法可以在面临一致性挑战的同时,保持分布式系统的可用性和性能。
BASE 理论的思想在分布式系统中得到了广泛应用,特别是对于大规模、高可用性的系统。它提供了一种权衡可用性和一致性的方法,允许根据具体需求和应用场景来选择适当的数据一致性模型(如最终一致性),以满足不同业务的要求。这使得分布式系统更具弹性、可伸缩性和容错性,同时在最终实现数据一致性。
因此分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴 CAP 定理和 BASE 理论:
AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
要解决分布式事务的问题,各个子系统之间必须能够感知彼此的事务状态,以确保状态一致性。为实现这一目标,通常需要引入一个事务协调者,它的职责是协调每个事务的参与者(子系统内的事务)。这些子系统内的事务通常被称为分支事务,而涉及多个分支事务的全局事务需要协调者来确保一致性。
以上述微服务为例,引入一个事务协调者后的架构如下:
在这种架构中,事务协调者负责协调每个分支事务的执行。当某一分支事务(例如库存服务)由于库存不足而导致业务失败时,事务协调者可以通知所有分支事务进行回滚,以确保所有事务状态的一致性。
分布式事务模型的关键点包括:
事务协调者(Coordinator): 负责协调全局事务的执行和状态管理。它监控每个分支事务的执行情况,并根据需要发出提交或回滚命令。
分支事务(Branch Transaction): 每个子系统内部的事务,负责执行具体的业务操作。分支事务会根据协调者的指示执行提交或回滚操作。
全局事务(Global Transaction): 涉及多个子系统内的一组分支事务,全局事务由协调者协调管理,以确保所有分支事务的状态一致性。
引入事务协调者后,分布式系统可以更好地处理各个子系统之间的事务一致性问题,保证全局事务的正确执行。这种模型有助于应对网络故障、部分事务失败等情况,以确保分布式系统的数据一致性和完整性。
Seata(前身为Fescar)是一个分布式事务解决方案,最早由蚂蚁金服和阿里巴巴共同开源于2019年1月。Seata 的目标是提供高性能、简单易用的分布式事务服务,旨在为用户提供一站式的分布式解决方案。
Seata的主要功能和特点包括:
分布式事务协调: Seata充当分布式事务的协调者,负责管理和协调多个分支事务,以确保它们的一致性和完整性。
高性能: Seata通过优化事务日志存储和协调算法,提供高性能的分布式事务处理能力。
简单易用: Seata提供了简单的API和配置方式,使开发人员能够轻松地集成和使用分布式事务功能。
支持多种存储后端: Seata支持多种存储后端,包括MySQL、Oracle、PostgreSQL等,以满足不同环境的需求。
分布式事务模式: Seata支持两阶段提交(2PC)和补偿模式,可以根据业务需求选择合适的事务模式。
跨框架和跨语言: Seata可以与各种编程语言和框架集成,包括Java、Spring Cloud、Dubbo等,以及Node.js、Python等非Java语言。
Seata 的官方网站(http://seata.io/)提供了详细的文档、博客和源码分析,为用户提供了丰富的资源和支持,帮助开发人员更好地理解和使用分布式事务解决方案。Seata 的开源性质使得它成为解决分布式事务挑战的重要工具之一,广泛应用于云原生和微服务架构中。
Seata 的架构涉及三个重要的角色,它们分别是:
TC (Transaction Coordinator) - 事务协调者: TC 是 Seata 中的核心组件,负责维护全局事务和分支事务的状态,协调全局事务的提交或回滚。TC通过全局事务的唯一标识来管理多个分支事务,确保它们的状态一致性,并根据协调结果决定是提交还是回滚全局事务。
TM (Transaction Manager) - 事务管理器: TM 定义了全局事务的范围,它负责启动全局事务、提交或回滚全局事务。TM 与 TC 进行通信,以协调全局事务的状态和最终结果。TM可以是应用程序的一部分,用于启动和管理分布式事务。
RM (Resource Manager) - 资源管理器: RM 管理分支事务处理的资源,包括数据库、消息队列、缓存等。RM 与 TC 进行通信以注册分支事务、报告分支事务的状态,并根据 TC 的指示来驱动分支事务的提交或回滚。RM 负责协调资源的一致性,以确保全局事务的一致性。
这三个角色的架构图如下:
这三个角色共同协作,构成了 Seata 的分布式事务管理架构。TC 作为协调者负责全局事务的管理,TM 负责事务的启动和控制,RM 负责资源的管理和一致性保障。通过这种分布式事务管理架构,Seata能够有效地管理分布式事务的状态和保证一致性,为分布式系统提供可靠的事务支持。
Seata 提供了四种不同的分布式事务解决方案,以满足不同业务场景的需求。这些解决方案分别是:
XA 模式(eXtended Architecture): XA模式是一种强一致性分阶段事务模式,它允许分布式事务在多个资源管理器(例如数据库)之间进行协调。在XA模式中,事务协调者(TC)协调多个RM(Resource Manager)来确保事务的一致性。虽然XA模式提供了强一致性,但通常会牺牲一定的可用性,并且需要支持XA协议的资源管理器。XA模式不需要业务侵入。
TCC 模式(Try-Confirm-Cancel): TCC模式是一种最终一致性的分阶段事务模式,它要求业务代码显式地定义“尝试”、“确认”和“取消”三个阶段的操作。TCC模式通过在每个参与者(分支事务)上执行这些操作来实现最终一致性。虽然TCC模式具有一定的业务侵入,但它提供了更灵活的事务控制和适应性。
AT 模式(Automatic Transaction): AT模式是一种最终一致性的分阶段事务模式,它无需业务代码显式地定义阶段操作,而是通过自动代理来管理分支事务。AT模式是Seata的默认模式,它提供了最终一致性的分布式事务解决方案,并且无需业务侵入。AT模式适用于大多数业务场景。
SAGA 模式: SAGA模式是一种长事务模式,它将分布式事务分解成多个连续的步骤,每个步骤都是一个本地事务。SAGA模式适用于需要跨多个微服务的长时间事务。虽然SAGA模式具有业务侵入,但它提供了更高级别的事务控制和业务流程管理。
这四种分布式事务解决方案使 Seata 能够适应不同的应用场景,从强一致性到最终一致性,从无业务侵入到有业务侵入,满足了多样化的分布式事务需求。根据具体的业务要求和架构设计,开发人员可以选择适当的 Seata 模式来实现分布式事务管理。
首先我们要下载 seata-server (TC 服务)压缩包,下载地址:http://seata.io/zh-cn/blog/download.html。这里我选择的版本是 1.4.2
:
修改conf
目录下的registry.conf
文件,这里我们选择将 seata-server
服务注册到 Nacos 中:
registry {
# 注册中心类型:file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "CD"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
在 Nacos 中添加seataServer.properties
配置文件。为了让 TC 服务的集群可以共享配置,所以选择了 Nacos 作为统一配置中心。因此服务端配置文件 seataServer.properties
文件需要添加到 Nacos 中。文件内容如下:
# 数据存储方式,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=passwd
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=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
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
创建数据库表
TC 服务在管理分布式事务时,需要记录事务相关数据到数据库中,需要提前创建好这些表。因此新建一个名为 seata
的数据库,然后创建一张分支事务表和一张全局事务表。这些表主要记录全局事务、分支事务、全局锁信息:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 分支事务表
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` tinyint(4) NULL DEFAULT NULL,
`client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- 全局事务表
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`timeout` int(11) NULL DEFAULT NULL,
`begin_time` bigint(20) NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
启动 TC 服务,进入 seata-server
的 bin
目录,运行其中的 seata-server.bat
即可:
启动成功后,可以发现 seata-server
默认的端口号是 8091, 并且此时也应该注册到 Nacos 注册中心了。打开浏览器,访问 nacos
的控制台,然后进入服务列表页面,可以看到 seata-tc-server
的信息:
首先,我们需要在微服务中引入 seata 依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>1.4.2version>
dependency>
注意,引入的依赖版本要和 seata-server
版本匹配。
修改配置文件,需要修改所有微服务的application.yml
文件,添加下面的配置:
seata:
registry: # TC 服务注册中心的配置,微服务根据这些信息去注册中心获取 tc 服务地址
# 参考 tc 服务的 registry.conf 中的配置
type: nacos
nacos: # tc
server-addr: 127.0.0.1:8848
namespace: ""
group: DEFAULT_GROUP
application: seata-tc-server # tc服务在nacos中的服务名称
tx-service-group: seata-demo # 事务组,根据这个获取 tc 服务的cluster名称
service:
vgroup-mapping: # 事务组与TC服务cluster的映射关系
seata-demo: CD
注意,Seata 客户端获取 TC 服务的集群名称 cluster 是通过 tx-service-group
和 vgroup-mapping
的映射来获取的,即以 tx-service-group
为 key ,以 vgroup-mapping
为 value:
完成了上面的所有配置之后,重启这三个微服务,就可以在 seata-server
的控制台中看到如下的日志: