3. 事务边界定义:事务边界、划分原则与本地/全局事务

在微服务架构中,事务边界是决定数据一致性与系统可用性的核心设计点。错误的边界划分会导致分布式数据不一致、补偿逻辑复杂化甚至系统雪崩。本节从概念、原则到事务类型,系统梳理事务边界的定义与划分方法。

3.1 什么是事务边界

事务边界是指一组数据库操作或业务操作被原子化包裹的起始与结束范围。在边界内,所有操作要么全部成功提交,要么全部回滚撤销。边界之外的操作不受该事务的隔离与原子性保护。

维度 说明
起始点 事务开始(BEGIN / START TRANSACTION)
结束点 提交(COMMIT)或回滚(ROLLBACK)
边界内 所有操作共享同一数据库连接、隔离级别、锁资源
边界外 其他并发事务可见已提交的数据,或等待锁释放
关键认知:事务边界不是代码行数的边界,而是数据一致性范围的边界。一个事务可以跨多个数据库操作,但必须保证这些操作在同一个数据库连接内完成。

3.2 微服务中事务边界的划分原则

单体应用的事务边界通常与 HTTP 请求边界重合。但在微服务中,每个服务拥有独立的数据库,事务边界必须重新划分。以下是业界公认的四大原则:

  1. 服务内强一致,服务间最终一致
    单个微服务内部使用本地事务保证 ACID;跨服务调用使用 Saga 或事件驱动保证最终一致性。
  2. 事务边界与业务聚合边界对齐
    一个事务只修改一个聚合(Aggregate)内的数据。跨聚合的修改应拆分为多个独立事务。
  3. 避免跨服务分布式事务
    不推荐使用两阶段提交(2PC)跨多个微服务,因为会引入同步阻塞、单点故障和性能瓶颈。
  4. 事务边界应尽量短小
    长事务会持有数据库锁过久,降低系统并发能力。微服务中应尽量将事务控制在 100ms 以内。
实践建议:在 NestJS 中,使用 @Transactional() 装饰器或 TypeORM 的 QueryRunner 管理本地事务边界。跨服务调用时,优先采用 Saga 编排模式,而非 XA 协议。
事务边界划分原则对比 ❌ 错误:跨服务长事务 服务A(订单) BEGIN TRANSACTION 远程调用服务B(库存) 远程调用服务C(支付) COMMIT / ROLLBACK ✅ 正确:服务内短事务 + Saga 服务A(订单)本地事务 BEGIN / 插入订单 / COMMIT 服务B(库存)本地事务 BEGIN / 扣减库存 / COMMIT 服务C(支付)本地事务 BEGIN / 扣款 / COMMIT

3.3 本地事务与全局事务

理解本地事务与全局事务的区别,是掌握微服务事务边界的基础。下表从多个维度进行对比:

对比维度 本地事务 全局事务(分布式事务)
范围 单个数据库实例 跨多个数据库 / 消息队列 / 服务
协议 数据库原生 ACID(如 MySQL InnoDB) XA / 2PC / TCC / Saga
隔离性 强隔离(可序列化 / 可重复读) 弱隔离(读未提交 / 快照隔离)
性能 高(锁范围小,无网络开销) 低(同步阻塞,协调器开销)
典型实现 TypeORM QueryRunner、Prisma 事务 Seata、NestJS + RabbitMQ Saga
适用场景 单服务内的 CRUD、聚合内操作 跨服务订单、库存、支付联动
常见误区:不要试图用本地事务包裹跨服务的 HTTP 调用。如果服务B调用失败,服务A的事务回滚只能撤销本地数据,无法撤销服务B已提交的修改。这种场景必须使用 Saga 补偿事务。

3.4 本地事务在 NestJS 中的典型实现

在 NestJS 中,本地事务通常通过 TypeORM 的 QueryRunner 或 @Transactional() 装饰器实现。以下是一个标准的事务边界代码示例:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, QueryRunner, DataSource } from 'typeorm';
import { Order } from './order.entity';

@Injectable()
export class OrderService {
  constructor(
    private dataSource: DataSource,
    @InjectRepository(Order)
    private orderRepo: Repository<Order>,
  ) {}

  async createOrderWithLocalTransaction(orderData: Partial<Order>): Promise<Order> {
    const queryRunner: QueryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // 事务边界内的所有操作
      const order = queryRunner.manager.create(Order, orderData);
      const savedOrder = await queryRunner.manager.save(order);

      // 其他本地操作(如更新库存表、写入日志)
      // await queryRunner.manager.update(...)

      await queryRunner.commitTransaction();
      return savedOrder;
    } catch (error) {
      await queryRunner.rollbackTransaction();
      throw error;
    } finally {
      await queryRunner.release();
    }
  }
}
边界控制要点:
- startTransaction() 与 commitTransaction() 之间的代码构成事务边界。
- 所有数据库操作必须使用 queryRunner.manager,而非 this.orderRepo,否则会脱离事务边界。
- finally 块中必须释放 QueryRunner,避免连接泄漏。
本地事务边界 vs 全局事务边界 本地事务边界(单数据库) BEGIN TRANSACTION INSERT INTO orders ... UPDATE inventory ... COMMIT / ROLLBACK 全局事务边界(跨服务) 协调器:准备阶段 服务A:预扣库存(锁定资源) 服务B:预扣金额(锁定资源) 协调器:提交 / 回滚(2PC)

3.5 事务边界划分的常见陷阱

  • 陷阱1:事务边界包含远程调用
    将 HTTP/RPC 调用放在本地事务内,导致事务长时间持有数据库连接和锁,降低系统吞吐量。
  • 陷阱2:事务边界过大
    一个事务内操作多个聚合根,违反 DDD 聚合设计原则,增加死锁概率。
  • 陷阱3:事务边界过小
    本应原子化的操作被拆成多个独立事务,导致部分成功部分失败,数据不一致。
  • 陷阱4:忽略事务传播行为
    在 NestJS 中,@Transactional() 默认的传播行为是 REQUIRED,如果嵌套调用未正确处理,可能导致事务意外合并或回滚范围错误。
核心总结:事务边界是微服务数据一致性的第一道防线。本地事务负责服务内的强一致,全局事务(Saga / TCC)负责服务间的最终一致。边界划分的核心原则是:高内聚、低耦合、短生命周期。