Windows
什么是分布式事务?为什么它很重要?
在现代软件开发中,随着系统规模的不断扩大和业务复杂度的提升,单一服务或数据库已经难以满足高性能、高可用和扩展性的需求。于是,微服务架构逐渐成为主流方案,而分布式事务正是在这种背景下应运而生的重要技术。简单来说,分布式事务是指在多个数据源(例如多个数据库、服务)之间完成的一组操作,这些操作要么全部成功,要么全部失败,从而确保系统的数据一致性和可靠性。
那么,这项技术到底用来做什么呢?举个简单的例子:假设你正在一个电商平台上进行购物支付,整个流程可能包括扣减库存、生成订单、减少用户余额等多个步骤。这些操作通常分别由不同的服务完成,它们各自管理着独立的数据源。如果在这个过程中某些操作失败了,比如库存被扣除了但订单却没有生成,就会导致数据不一致甚至经济损失。分布式事务的目标就是确保这一系列跨服务的操作要么全部完成,要么全部回滚,避免这种情况的发生。
学习分布式事务不仅能够帮助开发者应对复杂的业务场景,还能为后续深入理解微服务架构、大规模系统设计奠定基础。如果你是刚开始接触后端开发的新手,掌握分布式事务的基本概念和实现方法将会是你迈向专业开发者道路上的重要一步。
环境准备:搭建你的开发环境
在正式进入分布式事务的学习之前,我们需要先准备好一套适合开发的环境。本教程将使用 Spring Boot + Spring Cloud Alibaba + Seata 来演示如何实现分布式事务。下面我们将一步步带大家安装和配置这些工具,并测试环境是否正常运行。
1. 安装 Java 和 Maven
首先,你需要安装 Java 运行环境和 Maven 构建工具:
- Java:建议使用 JDK 1.8 或更高版本。你可以从 Oracle官网 下载安装包。
- Maven:访问 Apache Maven官网,下载对应操作系统的二进制文件并配置环境变量。
安装完成后,在终端输入以下命令验证是否成功:
java -version
mvn -v
如果看到类似如下输出,说明安装正确:
Java version "1.8.0_312"
Apache Maven 3.8.4
2. 安装 IDE
为了方便开发,建议使用 IntelliJ IDEA 社区版或企业版。你可以从 JetBrains官网 下载安装包并安装。
3. 安装 Spring Boot 和 Spring Cloud Alibaba
本项目基于 Spring Boot 框架,并结合 Spring Cloud Alibaba 提供的分布式能力。我们无需手动安装,只需在项目的 pom.xml 文件中添加对应的依赖即可。
4. 安装 Seata 分布式事务框架
Seata 是阿里巴巴开源的分布式事务解决方案,支持 TCC、SAGA、AT 等模式。你可以从 Seata GitHub发布页面 下载最新版本的 Seata Server,并解压到本地。
接下来,配置 conf/file.conf 文件,设置事务日志存储路径,并确保 Seata Server 可以连接你的数据库。
5. 启动 Seata Server
打开终端,进入 Seata 的 bin 目录,执行以下命令启动 Seata Server(默认端口 8091):
.\seata-server.bat -p 8091 -n 1 -h 127.0.0.1
# macOS/Linux
sh seata-server.sh -p 8091 -n 1 -h 127.0.0.1
如果看到启动成功的提示,说明 Seata 已经正常运行。
6. 准备数据库环境
本教程使用 MySQL 作为数据库,你需要提前安装 MySQL 并创建两个数据库用于演示分布式事务的不同服务(如 order_db 和 storage_db)。
7. 测试开发环境
现在我们可以创建一个简单的 Spring Boot 项目,并引入相关的依赖,然后运行一个小示例来确认环境是否正常。
创建 DemoApplication.java 主类,并编写一个简单的 HTTP 接口:
@RestController
@SpringBootApplication
public class DemoApplication {
@GetMapping("/hello")
public String hello() {
return "Hello, Distributed Transaction Environment is Ready!";
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
运行该项目,并在浏览器访问 http://localhost:8080/hello,如果看到响应内容,则说明环境配置成功,可以开始下一步学习!
通过以上步骤,我们完成了 Java 开发环境的搭建、Spring Boot 基础项目的创建,并成功运行了 Seata 分布式事务框架。接下来,我们将继续深入讲解分布式事务的核心概念,并通过完整的项目实战来加深理解。
理解分布式事务的核心概念
在实际开发中,理解分布式事务的核心概念至关重要。为了让新手更容易理解,我们用最通俗的语言解释以下几个关键概念:CAP定理、BASE理论、两阶段提交(2PC)、三阶段提交(3PC)以及常见的分布式事务框架。
CAP定理:一致性、可用性、分区容忍
CAP定理指出,在一个分布式系统中,无法同时满足以下三个特性:
- 一致性(Consistency):所有节点在同一时刻具有相同的数据。
- 可用性(Availability):每个请求都能得到响应,无论是否成功。
- 分区容忍(Partition Tolerance):网络可能出现故障,但系统仍然能够继续运行。
通俗地说,CAP定理告诉我们,分布式系统的设计需要在这三个特性之间做出取舍。由于分区容忍几乎是必须的(因为网络不可能百分之百可靠),所以大多数系统只能在一致性和可用性之间找到一个平衡点。
BASE理论:一种更实用的替代方案
与强调一致性的ACID模型不同,BASE理论是一种更为灵活的指导原则。它的核心思想是“基本可用、柔性状态、最终一致”:
- 基本可用(Basically Available):系统允许暂时的不可用,但整体上仍然是可用的。
- 柔性状态(Soft State):系统中的数据可能会暂时处于不一致的状态。
- 最终一致(Eventually Consistent):经过一定时间后,系统的各个节点最终会达成一致。
BASE理论更适合分布式环境,因为它放宽了一致性的要求,更加注重系统的可用性和扩展性。这使得BASE理论成为许多分布式事务框架的基础。
两阶段提交(2PC):经典的协调方式
两阶段提交(Two-phase Commit,简称2PC)是一个经典的分布式事务协议。它的主要过程分为两个阶段:
- 准备阶段(Prepare Phase):协调者询问所有参与者是否能够提交事务。参与者检查本地资源并返回“就绪”或“未就绪”。
- 提交阶段(Commit Phase):如果所有参与者都准备就绪,协调者下达“提交”指令;如果有任意一方未就绪,则协调者下达“回滚”指令。
2PC的优点在于逻辑简单,能保证强一致性。但缺点也很明显:一是单点故障问题(协调者宕机会影响整个流程),二是同步阻塞问题(所有参与者在等待结果期间都会被阻塞)。
三阶段提交(3PC):对2PC的改进
三阶段提交(Three-phase Commit,简称3PC)是对2PC的进一步优化,目的是解决其阻塞问题和单点故障的风险。它的流程包括:
- CanCommit阶段:协调者询问参与者是否愿意提交事务,而不真正执行。
- PreCommit阶段:如果所有参与者同意,协调者发送“预提交”命令。
- DoCommit阶段:参与者正式提交事务。
3PC的优点在于减少了阻塞时间,并且在协调者宕机时有一定的容错机制。然而,它的实现更加复杂,并且在网络不稳定的情况下仍可能产生不一致的情况。
常见的分布式事务框架
为了简化分布式事务的实现,业界推出了多个成熟的分布式事务框架。其中,Seata 是一个非常流行的选择。Seata提供了多种模式来应对不同的业务场景:
- AT模式(自动事务模式):适用于关系型数据库,通过拦截 SQL 实现事务控制。
- TCC模式(Try-Confirm-Cancel 模式):提供高度定制化的事务控制,适合复杂的业务场景。
- Saga模式:适用于长周期事务,通过补偿机制实现最终一致性。
- XA模式:兼容 XA 标准,支持多数据源之间的全局事务。
这些框架为我们提供了强大的工具,可以快速构建可靠的分布式事务解决方案。掌握这些概念是迈向实践应用的关键一步。
通过以上介绍,你应该对分布式事务的核心概念有了初步的理解。这些理论和方法不仅是分布式系统设计的基础,也为后续的项目实战打下了坚实的知识储备。接下来,我们将结合代码示例,逐步带你进入实际开发阶段!
动手实践:从零搭建一个简单的分布式事务项目
在前面的学习中,我们了解了分布式事务的概念、相关理论以及使用的工具。现在,让我们动手实践一下,跟着教程一步步搭建一个简单的分布式事务项目。本项目将以一个电商平台的下单和支付为例,模拟真实场景中的多个服务协同处理事务的需求。
1. 创建微服务项目结构
我们采用 Spring Boot + Spring Cloud Alibaba + Seata 技术栈,项目结构如下:
distributed-transaction-demo/
├── order-service/ // 订单服务
├── storage-service/ // 库存服务
├── account-service/ // 账户服务
├── seata-config/ // Seata 配置
└── pom.xml // 父工程依赖管理
首先,使用 Spring Initializr 创建三个 Spring Boot 项目:order-service、storage-service 和 account-service,并确保它们都包含以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.6.1</version>
</dependency>
2. 配置 Seata 和 Nacos
我们在上一节已经安装了 Seata Server 并成功启动。现在需要在每个微服务中添加 Seata 的配置,以启用分布式事务功能。
在每个服务的 application.yml 中添加如下 Seata 配置:
seata:
enabled: true
application-id: ${spring.application.name} # 当前服务的应用ID
tx-service-group: my_test_tx_group # 事务分组,确保各服务一致
service:
vgroup-mapping:
my_test_tx_group: default # 映射事务分组到TC集群
grouplist:
default: 127.0.0.1:8091 # Seata Server 地址
config:
type: file # 使用文件配置
registry:
type: file # 使用本地注册中心
3. 编写服务接口和业务逻辑
我们的项目涉及三个核心服务:订单服务、库存服务和账户服务。
(1)库存服务(storage-service)
StorageController.java
@RestController
@RequestMapping("/storage")
public class StorageController {
@Autowired
private StorageService storageService;
// 扣减库存
@PostMapping("/deduct")
public String deduct(@RequestParam("productId") Long productId,
@RequestParam("count") Integer count) {
storageService.deduct(productId, count);
return "库存扣减成功";
}
}
StorageService.java
@Service
public class StorageService {
@Autowired
private StorageMapper storageMapper;
// 扣减库存方法
@Transactional
public void deduct(Long productId, Integer count) {
Storage storage = storageMapper.selectById(productId);
if (storage.getUsed() + count > storage.getTotal()) {
throw new RuntimeException("库存不足");
}
storage.setUsed(storage.getUsed() + count);
storageMapper.updateById(storage);
}
}
(2)账户服务(account-service)
AccountController.java
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private AccountService accountService;
// 扣款
@PostMapping("/deduct")
public String deduct(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money) {
accountService.deduct(userId, money);
return "账户扣款成功";
}
}
AccountService.java
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
// 扣款方法
@Transactional
public void deduct(Long userId, BigDecimal money) {
Account account = accountMapper.selectById(userId);
if (account.getBalance().compareTo(money) < 0) {
throw new RuntimeException("余额不足");
}
account.setBalance(account.getBalance().subtract(money));
accountMapper.updateById(account);
}
}
(3)订单服务(order-service)
OrderController.java
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
// 创建订单
@PostMapping("/create")
public String createOrder(@RequestParam("userId") Long userId,
@RequestParam("productId") Long productId,
@RequestParam("count") Integer count,
@RequestParam("money") BigDecimal money) {
try {
orderService.createOrder(userId, productId, count, money);
return "订单创建成功";
} catch (Exception e) {
return "订单创建失败:" + e.getMessage();
}
}
}
OrderService.java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
// 创建订单,调用库存和账户服务
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder(Long userId, Long productId, Integer count, BigDecimal money) throws Exception {
// 1. 插入订单记录
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setCount(count);
order.setMoney(money);
order.setStatus("待发货");
orderMapper.insert(order);
// 2. 调用库存服务,扣减库存
String storageUrl = "http://localhost:8081/storage/deduct?productId=" + productId + "&count=" + count;
ResponseEntity<String> storageResponse = restTemplate.postForEntity(storageUrl, null, String.class);
if (!"库存扣减成功".equals(storageResponse.getBody())) {
throw new RuntimeException("库存扣减失败");
}
// 3. 调用账户服务,扣款
String accountUrl = "http://localhost:8082/account/deduct?userId=" + userId + "&money=" + money;
ResponseEntity<String> accountResponse = restTemplate.postForEntity(accountUrl, null, String.class);
if (!"账户扣款成功".equals(accountResponse.getBody())) {
throw new RuntimeException("账户扣款失败");
}
}
}
4. 启动项目并测试
依次启动 order-service、storage-service 和 account-service,并确保 Seata Server 正在运行。
访问 http://localhost:8080/order/create?userId=1&productId=1&count=1&money=100 发起订单创建请求。
如果一切正常,你应该会看到如下响应:
{
"status": "success",
"message": "订单创建成功"
}
如果你想测试事务回滚效果,可以在某个服务里抛出异常,例如在 AccountService.deduct() 方法中强制抛出错误:
throw new RuntimeException("模拟账户扣款失败");
此时,整个事务应该会回滚,订单不会被创建,库存也不会被扣减。
至此,我们已经完成了一个完整的分布式事务项目。通过本节的实践,你应该已经掌握了如何使用 Spring Boot、RestTemplate 和 Seata 搭建一个具备完整事务控制能力的微服务系统。下一节我们将针对初学者常遇到的问题进行解答,帮助你更快地理解和应用分布式事务。
新手常见问题及解答
在学习和实践分布式事务的过程中,很多初学者会遇到一些常见问题。下面我们就来整理几个最典型的问题,并给出相应的解决方案。
1. 分布式事务为什么会比本地事务复杂?
问题描述: 在传统单体应用中,使用本地事务(如 MySQL 的 ACID 特性)就能轻松完成事务控制。但一旦拆分成多个服务,事务就变得复杂得多,这是为什么?
解答: 单体应用的所有数据都在同一个数据库中,因此可以通过数据库自身提供的事务机制(如 BEGIN、COMMIT、ROLLBACK)来保证数据一致性。而在微服务架构下,数据分散在多个数据库中,每个服务都有自己的事务边界,这就需要借助额外的事务协调机制(如 Seata、XA 协议等)来统一管理事务。这种跨服务的事务管理,自然比单一服务内的本地事务要复杂得多。
解决方案: 初学时可以选择使用成熟的分布式事务框架(如 Seata),它封装了底层的通信和事务协调逻辑,让开发者可以像使用本地事务一样编写代码。等到熟悉原理之后,再深入研究其工作机制。

评论 0