在怀旧情绪可能有些过度的时代,软件开发界引发了一场激烈辩论:微服务模式的大规模采用是否真正带来了预期的收益,还是其收支状况更具不确定性?许多团队开始思考,是时候“回归”到可靠的单体架构,还是答案介于两者之间,以模块化单体的形式存在。
一位分心的架构师
分布式事务:本文旨在准确解决这一主题,展示在这种情况下,Apache Seata 如何与 Spring Boot 的敏捷性相结合,将许多人眼中的噩梦转化为令人惊讶的可管理解决方案。将向你展示 Seata 如何帮助你安睡,无需复杂的架构和回滚策略,或依赖更繁琐的模式如“Outbox 模式”。
一位怀旧且持怀疑态度的架构师
Apache Seata
Apache Seata是一个开源的分布式事务解决方案,目前处于Apache 孵化阶段,在微服务架构下提供高性能且易于使用的分布式事务服务。
主要组件包括:
- 事务协调器(TC):维护全局和分支事务的状态,驱动全局提交或回滚。
- 事务管理器(TM):定义全局事务的范围,开始全局事务,提交或回滚全局事务。
- 资源管理器(RM):管理分支事务工作的资源,与 TC 通信以注册分支事务并报告其状态,驱动分支事务的提交或回滚。
支持的事务模型有:AT、XA (2PC)和SAGA。
XA (2PC)
XA 是 1991 年 X/Open 发布的规范(后来与 The Open Group 合并)。
阶段 1:准备(提交投票)
- TC(Seata 服务器)询问所有参与的RM(例如与数据库交互的微服务)是否准备提交本地事务。
- 每个 RM 执行其操作,写入事务日志以确保持久性,并锁定资源以保证隔离性。
- 如果 RM 准备好,则向 TC 响应“是”,否则响应“否”。
阶段 2:提交或回滚
- 如果所有RM 响应“是”,TC 向所有 RM 发送“提交”命令。它们完成本地事务并释放锁。
- 如果任何RM 响应“否”,或者 TC 检测到超时/故障,TC 发送“回滚”命令。所有 RM 撤销其操作并释放锁。
优缺点(简要):
- 优点:在多个服务之间提供强数据一致性(ACID),类似于一个不可分割的事务。
- 缺点:如果参与者或协调器缓慢或失败,可能导致资源阻塞(高延迟),可能影响可用性和可扩展性。它还依赖于底层数据库/资源支持 XA 标准。
SAGA
SAGA 模式是微服务架构中管理分布式事务的一种广泛采用的方法。与 2PC 不同,Saga 牺牲了即时的强一致性以获得更高的可用性和可扩展性,实现最终一致性。
Saga 是一系列本地事务,其中每个本地事务(在单个微服务中)更新其自己的数据库,然后发布事件或消息以触发序列中的下一个本地事务。
- 无全局锁:关键是,本地事务立即提交,不持有全局锁,允许更高的并发性。
- 失败补偿:如果任何本地事务失败,Saga 不会在传统意义上“回滚”。相反,它执行一系列补偿事务,以语义方式撤销先前完成的本地事务的影响。这些补偿事务是旨在逆转业务影响的新操作。
Saga 可以通过以下方式实现: - 编排:服务发布事件并对其做出反应,导致去中心化的流程。
- 编排:一个中央编排服务协调流程,发送命令并对响应做出反应。
优缺点(简要): - 优点:由于没有长期持有的分布式锁,非常适合高可用性和可扩展性。适用于松散耦合的微服务。
- 缺点:实现最终一致性,意味着数据可能暂时不一致。需要大量的开发工作来实现所有补偿事务并管理复杂的 Saga 逻辑,这也可能使调试更加困难。
AT
AT(自动事务)模式是 Seata 的旗舰解决方案,旨在提供 2PC 的易用性以及 Saga 的非阻塞性质和可扩展性优势。对于大多数使用关系数据库的微服务,它是推荐的默认模式。
阶段 1:本地事务和准备
- 当微服务(RM)在全局事务中执行数据库操作(例如 UPDATE、INSERT、DELETE)时:
- Seata 的智能DataSourceProxy拦截 SQL。
- 它自动创建一个undo\_log(记录修改前的数据状态)。
- SQL 操作在本地数据库上立即执行并提交。
- Seata 然后通过事务协调器(TC)获取修改资源的全局锁。此锁不是传统的数据库锁;它防止其他全局事务同时修改相同的资源,但不阻止读取操作。
- RM 通知 TC 其分支事务已“准备就绪”。
阶段 2:全局提交或回滚
- 全局提交:如果所有分支事务准备成功,TC 指示它们提交。由于本地数据库事务在阶段 1 中已经提交,RM 只需释放其全局锁。
全局回滚:如果任何分支事务失败,或者全局事务需要回滚:
- TC 指示 RM 回滚。
- RM 使用其存储的undo\_log自动补偿对其本地数据库所做的更改,有效地恢复以前的状态。然后它们释放全局锁。
优缺点(简要):
- 优点:为全局事务提供强一致性。提供出色的可用性和可扩展性,因为本地数据库锁仅短暂持有。对开发人员高度透明,需要最少的代码更改。自动回滚简化了错误处理。
- 缺点:主要为关系数据库设计。虽然在数据库级别是非阻塞的,但仍有生成 undo\_logs 和管理全局锁的开销。
一位不耐烦的开发者
与 Spring Boot 的实际演示
设想一个涉及两个不同微服务的场景,每个微服务都有自己专用的自治数据库:
- 信用 API:负责管理用户信用(货币余额)
- 发货 API:专门处理发货购买
协调这两个服务的是一个 BFF(前端后端)。它的作用是协调发货购买操作,这转化为一系列分布式调用: - 通过信用 API 检查和/或扣除用户信用。
- 使用信用通过发货 API 实际购买发货。
关键问题是,如何确保这些跨不同服务和数据库的操作保持事务一致性,确保只有在信用已成功更新的情况下才完成购买,反之亦然?
一位邻居
架构
TM
BFF 将代表事务管理器,即它将定义全局事务。
Java 代码:
@Service
public class BFFManager {
@Autowired
private CreditService creditService;
@Autowired
private ShippingService shippingService;
@GlobalTransactional
public ShippingResult buyShipping(Long userID, BigDecimal cost) {
var wallet = creditService.updateBalance(userID, cost);
var shipping = shippingService.buyShipping(userID, cost);
wallet = creditService.getWallet(userID);
var result = new ShippingResult();
result.setCost(cost);
result.setShippingID(shipping.getId());
result.setCurrentBalance(wallet.getBalance());
return result;
}
}
只需@GlobalTransactional
注解就足够了吗?当然不是,但不需要太多其他东西:
TM 所需的依赖:
Java 代码:
implementation 'org.apache.seata:seata-spring-boot-starter:2.3.0'
implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-seata:2023.0.3.3') {
exclude group: 'org.apache.seata', module: 'seata-spring-boot-starter'
}
spring-cloud-starter-alibaba-seata
除其他功能外,还确保微服务之间的 HTTP 通信始终包含全局事务 ID(XID)。seata-spring-boot-starter
是经典的 Spring Boot 启动器,从属性开始自动配置 Seata 实体(在这种情况下为 TM):
Java 代码:
seata:
enabled: true
data-source-proxy-mode: AT
enable-auto-data-source-proxy: true
application-id: ${spring.application.name}
tx-service-group: default_tx_group
service:
vgroup-mapping:
default_tx_group: default
grouplist:
default: 127.0.0.1:8091
RM
credit-api
和shipping-api
都充当 RM。它们只需要seata-spring-boot-starter
依赖,并具有以下属性:
Java 代码:
seata:
enabled: true
data-source-proxy-mode: AT
enable-auto-data-source-proxy: true
application-id: ${spring.application.name}
tx-service-group: default_tx_group
service:
vgroup-mapping:
default_tx_group: default
grouplist:
default: 127.0.0.1:8091
RM 的数据库必须包含 Seata 的undo\_log表。这里是每种类型数据库的必要脚本。
在 GitHub 上的代码中,表的创建通过创建专用数据库的 docker compose(通过org.springframework.boot:spring-boot-docker-compose)进行管理。
在 RM 中,不需要特定的注解。像往常一样编写面向存储库的代码。如果愿意,我建议继续使用@Transactional
进行本地事务。
TC
TC 由 Seata 的核心seata-server
表示。这需要两个主要配置:
- 注册中心:定义 Seata 将使用的服务注册中心(nacos、eureka、consul、zookeeper、redis、文件)。对于此示例,我选择使用
file
类型。 存储:定义全局事务数据和全局锁的持久性(文件、数据库、redis)。对于此示例,我选择使用
db
类型。
以下是用于初始化服务器的 Docker Compose 设置:
YAML 代码:services: mysql: image: mysql:8.0.33 container_name: mysql-seata environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: seata MYSQL_USER: seata_user MYSQL_PASSWORD: seata_pass ports: - "3317:3306" volumes: - mysql_data:/var/lib/mysql -./docker/seata/mysql.sql:/docker-entrypoint-initdb.d/seata.sql:ro healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-prootpass"] interval: 10s timeout: 5s retries: 5 seata-server: image: apache/seata-server:2.3.0 container_name: seata-server depends_on: mysql: condition: service_healthy environment: - SEATA_CONFIG_NAME=application.yml volumes: - "./docker/seata/resources/application.yml:/seata-server/resources/application.yml" - "./docker/seata/mysql-connector-j-8.0.33.jar:/seata-server/libs/mysql-connector-j-8.0.33.jar" ports: - "7091:7091" - "8091:8091" volumes: mysql_data:
以及配置属性(application.yaml):
YAML 代码:server: port: 7091 spring: application: name: seata-server datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://mysql:3306/seata username: seata_user password: seata_pass logging: config: classpath:logback-spring.xml file: path: ${log.home:${user.home}/logs/seata} console: user: username: seata password: seata seata: security: secretKey: seata tokenValidityInMilliseconds: 1800000 config: type: file registry: type: file store: mode: db db: dbType: mysql driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://mysql:3306/seata user: seata_user password: seata_pass min-conn: 10 max-conn: 100 global-table: global_table branch-table: branch_table lock-table: lock_table distributed-lock-table: distributed_lock vgroup-table: vgroup_table query-limit: 1000 max-wait: 5000
如你所见,专用数据库需要一些表才能运行。所有创建 SQL 脚本都可在此处获得这里。
开始运行!
Java 代码:
@SpringBootTest
public class GlobalTransactionalTest {
@Autowired
private BFFManager bffManager;
@Autowired
private CreditService creditService;
@MockitoBean
private ShippingService shippingService;
@Test
public void globalTransactionalTest_OK() {
var wallet = creditService.getWallet(1L);
var shipping = new Shipping();
shipping.setId(2L);
when(shippingService.buyShipping(1L, new BigDecimal(4))).thenReturn(shipping);
bffManager.buyShipping(1L, new BigDecimal(4));
var newWallet = creditService.getWallet(1L);
assertEquals(new BigDecimal("4.00"), wallet.getBalance().subtract(newWallet.getBalance()));
}
@Test
public void globalTransactionalTest_KO() {
var wallet = creditService.getWallet(1L);
var shipping = new Shipping();
shipping.setId(2L);
when(shippingService.buyShipping(1L, new BigDecimal(4))).thenThrow(new RuntimeException());
try {
bffManager.buyShipping(1L, new BigDecimal(4));
} catch (Exception e) {}
var newWallet = creditService.getWallet(1L);
assertEquals(newWallet.getBalance(), wallet.getBalance());
}
}
完整且可运行的代码可在GitHub上获取。运行组件并告诉我你的想法!
关键替代方案
- Atomikos:XA (2PC)。它仅在访问多个数据库的同一微服务内工作(没有事务协调器,没有 XID 传播)
- SAGA 编排工作流引擎(Camunda,Temporal.io):需要外部编排器和更高的集成工作。
- SAGA(编排事件驱动):最终一致性权衡。
- Outbox 模式:最终一致性。
本文的目的不是推广 Apache Seata 优于其他提到的替代方案,而是强调其易用性。一如既往,应根据具体上下文和系统要求选择合适的工具。
下一集:“单体有多酷?”敬请关注。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。