当前位置: 首页 > news >正文

seate TCC模式案例

场景描述

  1. 用户下单时,需要创建订单并从用户账户中扣除相应的余额。
  2. 如果订单创建成功但余额划扣失败,则需要回滚订单创建操作。
  3. 使用 Seata 的 TCC 模式来保证分布式事务的一致性。

1. 项目结构

假设我们有两个微服务:

  • Order Service:负责创建订单。
  • Account Service:负责扣除用户余额。

此外,还需要一个 Seata Server 来协调分布式事务。


2. 数据库设计

Order 表
CREATE TABLE `orders` (`id` BIGINT AUTO_INCREMENT PRIMARY KEY,`user_id` VARCHAR(32) NOT NULL,`product_id` VARCHAR(32) NOT NULL,`amount` DECIMAL(10, 2) NOT NULL,`status` VARCHAR(16) DEFAULT 'INIT' -- 状态:INIT(初始化)、CONFIRMED(确认)、CANCELLED(取消)
);
Account 表
CREATE TABLE `accounts` (`id` BIGINT AUTO_INCREMENT PRIMARY KEY,`user_id` VARCHAR(32) NOT NULL,`balance` DECIMAL(10, 2) NOT NULL
);

3. Order Service

(1) 定义 TCC 接口

OrderService 中定义 Try、Confirm 和 Cancel 方法。

@LocalTCC
public interface OrderTccService {@TwoPhaseBusinessAction(name = "createOrder", commitMethod = "confirmOrder", rollbackMethod = "cancelOrder")boolean createOrder(BusinessActionContext context, String userId, String productId, BigDecimal amount);boolean confirmOrder(BusinessActionContext context);boolean cancelOrder(BusinessActionContext context);
}
(2) 实现 TCC 方法
@Service
public class OrderTccServiceImpl implements OrderTccService {@Autowiredprivate OrderMapper orderMapper;@Overridepublic boolean createOrder(BusinessActionContext context, String userId, String productId, BigDecimal amount) {// Try 阶段:创建订单,状态为 INITOrder order = new Order();order.setUserId(userId);order.setProductId(productId);order.setAmount(amount);order.setStatus("INIT");orderMapper.insert(order);// 将订单 ID 存入上下文,供 Confirm 和 Cancel 使用context.getActionContext().put("orderId", order.getId());return true;}@Overridepublic boolean confirmOrder(BusinessActionContext context) {// Confirm 阶段:将订单状态更新为 CONFIRMEDLong orderId = (Long) context.getActionContext("orderId");orderMapper.updateStatus(orderId, "CONFIRMED");return true;}@Overridepublic boolean cancelOrder(BusinessActionContext context) {// Cancel 阶段:将订单状态更新为 CANCELLEDLong orderId = (Long) context.getActionContext("orderId");orderMapper.updateStatus(orderId, "CANCELLED");return true;}
}
(3) Mapper 定义
@Mapper
public interface OrderMapper {void insert(Order order);void updateStatus(Long orderId, String status);
}

4. Account Service

(1) 定义 TCC 接口

AccountService 中定义 Try、Confirm 和 Cancel 方法。

@LocalTCC
public interface AccountTccService {@TwoPhaseBusinessAction(name = "deductBalance", commitMethod = "confirmDeduct", rollbackMethod = "cancelDeduct")boolean deductBalance(BusinessActionContext context, String userId, BigDecimal amount);boolean confirmDeduct(BusinessActionContext context);boolean cancelDeduct(BusinessActionContext context);
}
(2) 实现 TCC 方法
@Service
public class AccountTccServiceImpl implements AccountTccService {@Autowiredprivate AccountMapper accountMapper;@Overridepublic boolean deductBalance(BusinessActionContext context, String userId, BigDecimal amount) {// Try 阶段:检查余额是否足够,并冻结相应金额Account account = accountMapper.findByUserId(userId);if (account.getBalance().compareTo(amount) < 0) {throw new RuntimeException("Insufficient balance");}accountMapper.freezeBalance(userId, amount);// 将冻结金额存入上下文,供 Confirm 和 Cancel 使用context.getActionContext().put("userId", userId);context.getActionContext().put("amount", amount);return true;}@Overridepublic boolean confirmDeduct(BusinessActionContext context) {// Confirm 阶段:扣除已冻结的金额String userId = (String) context.getActionContext("userId");BigDecimal amount = (BigDecimal) context.getActionContext("amount");accountMapper.confirmDeduct(userId, amount);return true;}@Overridepublic boolean cancelDeduct(BusinessActionContext context) {// Cancel 阶段:释放已冻结的金额String userId = (String) context.getActionContext("userId");BigDecimal amount = (BigDecimal) context.getActionContext("amount");accountMapper.cancelDeduct(userId, amount);return true;}
}
(3) Mapper 定义
@Mapper
public interface AccountMapper {Account findByUserId(String userId);void freezeBalance(String userId, BigDecimal amount);void confirmDeduct(String userId, BigDecimal amount);void cancelDeduct(String userId, BigDecimal amount);
}

5. 调用方(API Gateway 或其他服务)

在调用方使用 @GlobalTransactional 注解开启全局事务。

@RestController
@RequestMapping("/api/orders")
public class OrderController {@Autowiredprivate OrderTccService orderTccService;@Autowiredprivate AccountTccService accountTccService;@PostMapping("/create")@GlobalTransactionalpublic ResponseEntity<String> createOrder(@RequestBody CreateOrderRequest request) {try {// 创建订单orderTccService.createOrder(null, request.getUserId(), request.getProductId(), request.getAmount());// 扣除余额accountTccService.deductBalance(null, request.getUserId(), request.getAmount());return ResponseEntity.ok("Order created successfully");} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to create order: " + e.getMessage());}}
}

6. 测试流程

  1. 启动 Seata Server。
  2. 启动 Order Service 和 Account Service。
  3. 发送请求到 /api/orders/create 接口,创建订单并扣除余额。
  4. 如果任意一个步骤失败,Seata 会自动触发回滚逻辑。

7. 关键点总结

  1. TCC 模式的核心

    • Try:预留资源。
    • Confirm:确认操作。
    • Cancel:补偿操作。
  2. Spring Cloud 集成

    • 使用 @LocalTCC 和 @TwoPhaseBusinessAction 注解定义 TCC 接口。
    • 使用 @GlobalTransactional 开启全局事务。
  3. 事务一致性

    • 如果任意一步失败,Seata 会自动调用 Cancel 方法进行回滚,确保数据一致。

TCC模式还会存在空回滚,幂等,悬挂等问题 

1. 空回滚

问题描述

  • 定义:在 TCC 模式中,如果 Try 阶段没有执行(例如由于网络超时或服务不可用),但 Cancel 阶段被调用了,则会导致空回滚。
  • 原因
    • Try 请求未到达服务端,或者未成功执行。
    • Seata Server 在协调事务时检测到失败,直接触发了 Cancel 阶段。

解决方案

  • 解决思路:在 Cancel 方法中判断是否需要执行回滚操作。
  • 实现方式
    • 在数据库中增加一个状态字段,用于标记资源是否已经被预留(Try 阶段是否执行过)。
    • 如果状态字段表明资源未被预留,则直接跳过 Cancel 操作。
示例代码
@Override
public boolean cancelDeduct(BusinessActionContext context) {String userId = (String) context.getActionContext("userId");Account account = accountMapper.findByUserId(userId);// 判断是否需要回滚(账户是否有冻结金额)if (account.getFrozenAmount().compareTo(BigDecimal.ZERO) == 0) {return true; // 跳过空回滚}// 执行取消逻辑accountMapper.cancelDeduct(userId, (BigDecimal) context.getActionContext("amount"));return true;
}

2. 幂等性

问题描述

  • 定义:TCC 的 Confirm 或 Cancel 方法可能因为网络重试等原因被多次调用,导致重复操作。
  • 原因
    • Seata Server 可能会多次尝试调用 Confirm 或 Cancel 方法。
    • 客户端或网络层可能引发重复请求。

解决方案

  • 解决思路:确保 Confirm 和 Cancel 方法是幂等的。
  • 实现方式
    • 使用数据库的状态字段来记录操作是否已经完成。
    • 如果某个操作已经完成,则直接返回成功,不再重复执行。
示例代码
@Override
public boolean confirmDeduct(BusinessActionContext context) {String userId = (String) context.getActionContext("userId");Account account = accountMapper.findByUserId(userId);// 判断是否已经确认if ("CONFIRMED".equals(account.getStatus())) {return true; // 已经确认,直接返回}// 执行确认逻辑accountMapper.confirmDeduct(userId, (BigDecimal) context.getActionContext("amount"));accountMapper.updateStatus(userId, "CONFIRMED");return true;
}@Override
public boolean cancelDeduct(BusinessActionContext context) {String userId = (String) context.getActionContext("userId");Account account = accountMapper.findByUserId(userId);// 判断是否已经取消if ("CANCELLED".equals(account.getStatus())) {return true; // 已经取消,直接返回}// 执行取消逻辑accountMapper.cancelDeduct(userId, (BigDecimal) context.getActionContext("amount"));accountMapper.updateStatus(userId, "CANCELLED");return true;
}

3. 悬挂

问题描述

  • 定义:Confirm 或 Cancel 方法比 Try 方法先执行,导致业务逻辑异常。
  • 原因
    • Try 请求在网络传输中延迟,而 Seata Server 认为 Try 失败并提前触发了 Confirm 或 Cancel。
    • Try 请求最终到达服务端时,发现 Confirm 或 Cancel 已经执行。

解决方案

  • 解决思路:通过状态字段和事务上下文信息,避免悬挂问题。
  • 实现方式
    • 在数据库中记录事务的执行状态。
    • 在 Try 方法中检查是否存在对应的 Confirm 或 Cancel 操作。如果有,则直接跳过 Try 操作。
示例代码
@Override
public boolean deductBalance(BusinessActionContext context, String userId, BigDecimal amount) {Account account = accountMapper.findByUserId(userId);// 判断是否已经确认或取消if ("CONFIRMED".equals(account.getStatus()) || "CANCELLED".equals(account.getStatus())) {return true; // 悬挂处理:直接返回}// 执行 Try 逻辑if (account.getBalance().compareTo(amount) < 0) {throw new RuntimeException("Insufficient balance");}accountMapper.freezeBalance(userId, amount);return true;
}

4. 总结

问题原因解决方案
空回滚Try 未执行,但 Cancel 被调用在 Cancel 方法中检查 Try 是否已执行,未执行则跳过。
幂等性Confirm 或 Cancel 方法被多次调用使用状态字段记录操作是否已完成,避免重复执行。
悬挂Confirm 或 Cancel 比 Try 先执行在 Try 方法中检查 Confirm 或 Cancel 是否已执行,已执行则跳过 Try。

通过以上方法,可以有效解决 TCC 模式中的空回滚、幂等性和悬挂问题,从而保证分布式事务的一致性和可靠性。


用字段状态检测以上问题,程序并不健壮,如果在高并发情况下还会出现一些问题,为了程序健壮性,达到强一致,我们还需要引入令牌和分布式锁


1. 状态字段的作用

  • 状态字段 是最基础的幂等性保障方式。
  • 它通过记录操作的状态(如 INITCONFIRMEDCANCELLED)来判断某个操作是否已经完成。
  • 优点:简单直观,易于实现。
  • 缺点:在高并发场景下可能会出现竞争条件(race condition),导致状态更新不一致。

2. 引入令牌机制

为什么需要令牌?

  • 定义:令牌是一种唯一标识符,用于确保每个请求只被执行一次。
  • 在分布式系统中,网络重试可能导致同一个请求被多次发送到服务端。如果服务端无法区分这些重复请求,则会导致重复操作。
  • 适用场景
    • 请求可能因为网络问题被重复发送。
    • 需要严格避免重复操作的场景(如支付、扣款等)。

实现方式

  • 每个请求生成一个唯一的令牌(如 UUID)。
  • 服务端在接收到请求时,先检查该令牌是否已经被处理过。
  • 如果已处理过,则直接返回成功;否则执行业务逻辑并记录该令牌。
示例代码
@Override
public boolean confirmDeduct(BusinessActionContext context) {String token = (String) context.getActionContext("token");if (StringUtils.isEmpty(token)) {throw new RuntimeException("Token is missing");}// 检查令牌是否已经处理过if (deductTokenRepository.existsByToken(token)) {return true; // 幂等性处理:直接返回}// 执行确认逻辑String userId = (String) context.getActionContext("userId");BigDecimal amount = (BigDecimal) context.getActionContext("amount");accountMapper.confirmDeduct(userId, amount);// 记录令牌DeductToken deductToken = new DeductToken();deductToken.setToken(token);deductToken.setStatus("CONFIRMED");deductTokenRepository.save(deductToken);return true;
}
数据库表设计
CREATE TABLE `deduct_token` (`id` BIGINT AUTO_INCREMENT PRIMARY KEY,`token` VARCHAR(64) NOT NULL UNIQUE,`status` VARCHAR(16) NOT NULL,`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

3. 引入分布式锁

为什么需要分布式锁?

  • 定义:分布式锁是一种协调机制,用于保证多个节点对共享资源的操作是互斥的。
  • 在高并发场景下,即使有状态字段或令牌机制,也可能因为多个线程同时访问同一资源而导致数据不一致。
  • 适用场景
    • 多个服务实例同时处理同一个请求。
    • 需要强一致性保障的场景。

实现方式

  • 使用 Redis 或 Zookeeper 实现分布式锁。
  • 在业务逻辑执行前获取锁,在业务逻辑完成后释放锁。
  • 如果无法获取锁,则等待或直接返回失败。
示例代码(基于 Redis)
@Autowired
private RedisTemplate<String, String> redisTemplate;@Override
public boolean confirmDeduct(BusinessActionContext context) {String lockKey = "lock:confirmDeduct:" + context.getXid(); // XID 是全局事务 IDString userId = (String) context.getActionContext("userId");// 尝试获取分布式锁Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, userId, 10, TimeUnit.SECONDS);if (Boolean.FALSE.equals(locked)) {throw new RuntimeException("Failed to acquire lock");}try {// 检查状态字段Account account = accountMapper.findByUserId(userId);if ("CONFIRMED".equals(account.getStatus())) {return true; // 已经确认,直接返回}// 执行确认逻辑accountMapper.confirmDeduct(userId, (BigDecimal) context.getActionContext("amount"));accountMapper.updateStatus(userId, "CONFIRMED");return true;} finally {// 释放分布式锁redisTemplate.delete(lockKey);}
}

4. 综合解决方案

在实际项目中,通常会结合 状态字段令牌机制分布式锁 来实现全面的幂等性保障:

  1. 状态字段
    • 用来记录操作的状态,避免重复执行。
  2. 令牌机制
    • 为每个请求分配唯一标识符,确保每个请求只被执行一次。
  3. 分布式锁
    • 在高并发场景下,使用分布式锁保护共享资源,避免竞争条件。
示例流程
  1. 客户端生成令牌
    • 客户端在发送请求时生成一个唯一的令牌(如 UUID),并将令牌附加到请求中。
  2. 服务端校验令牌
    • 服务端接收到请求后,首先检查令牌是否存在。
    • 如果令牌已存在,则直接返回成功。
  3. 获取分布式锁
    • 如果令牌不存在,则尝试获取分布式锁。
    • 如果锁获取成功,则继续执行业务逻辑;否则返回失败或等待。
  4. 更新状态字段
    • 执行业务逻辑后,更新状态字段以标记操作已完成。
  5. 记录令牌
    • 将令牌保存到数据库中,以便后续重复请求可以直接跳过。

5. 总结

方法适用场景优缺点
状态字段基础的幂等性保障,适用于大多数场景。优点:简单易用;缺点:高并发下可能存在问题。
令牌机制适用于需要严格避免重复操作的场景(如支付、扣款)。优点:能有效防止重复请求;缺点:需要额外存储令牌信息。
分布式锁适用于高并发场景,需要强一致性保障的场景。优点:避免竞争条件;缺点:增加了系统复杂性和性能开销。

通过结合 状态字段令牌机制分布式锁,可以构建一个健壮的幂等性保障机制,从而更好地应对分布式事务中的各种挑战。

相关文章:

  • vue3 toRefs 与 toRef的使用
  • SpringCloud概述和环境搭建
  • Vue3 响应式原理: Proxy 数据劫持详解
  • 命令行参数·环境变量·进程地址空间(linux+C/C++)
  • 【Rust 精进之路之第14篇-结构体 Struct】定义、实例化与方法:封装数据与行为
  • STM32开发过程中碰到的问题总结 - 4
  • C++:详解命名空间
  • Chromium 134 编译指南 Ubuntu篇:环境搭建与源码获取(一)
  • Cesium 地形加载
  • 2025年渗透测试面试题总结-拷打题库07(题目+回答)
  • 性能比拼: Go vs Bun
  • PICO4 Ultra MR开发 空间网格扫描 模型导出及预览
  • 【25软考网工】第二章(8)差错控制、奇偶校验、CRC、海明码
  • DAY6:从执行计划到索引优化的完整指南
  • C语言笔记(鹏哥)上课板书+课件汇总(结构体)-----数据结构常用
  • 【每日八股】复习计算机网络 Day3:TCP 协议的其他相关问题
  • 飞帆中控件数据和 Vue 双向绑定
  • 3.4/Q2,GBD数据库最新文章解读
  • 山东大学软件学院创新项目实训开发日志(20)之中医知识问答自动生成对话标题bug修改
  • 【Rust 精进之路之第13篇-生命周期·进阶】省略规则与静态生命周期 (`‘static`)
  • 人民网评:“中国传递爱而不是关税”
  • 解除近70家煤电厂有毒物质排放限制,特朗普能重振煤炭吗?
  • 鲁比奥称“美或退出俄乌谈判”,欧洲官员:为了施压乌克兰
  • 法官颁布紧急临时禁止令,中国留学生诉美国政府“首战胜利”
  • 黄金投资热,成了“财富焦虑”的贩卖场
  • 长沙岳麓警方通报“女子疑被性侵”:正在进一步侦办