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. 总结

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/77658.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】Rhcsa复习5

一、Linux文件系统权限 1、文件的一般权限 文件权限针对三类对象进行定义&#xff1a; owner 属主&#xff0c;缩写u group 属组&#xff0c; 缩写g other 其他&#xff0c;缩写o 每个文件针对每类访问者定义了三种主要权限&#xff1a; r&#xff1a;read 读 w&…

《Operating System Concepts》阅读笔记:p748-p748

《Operating System Concepts》学习第 64 天&#xff0c;p748-p748 总结&#xff0c;总计 1 页。 一、技术总结 1.Transmission Control Protocol(TCP) 重点是要自己能画出其过程&#xff0c;这里就不赘述了。 二、英语总结(生词&#xff1a;3) transfer, transport, tran…

C语言之图像文件的属性

&#x1f31f; 嗨&#xff0c;我是LucianaiB&#xff01; &#x1f30d; 总有人间一两风&#xff0c;填我十万八千梦。 &#x1f680; 路漫漫其修远兮&#xff0c;吾将上下而求索。 图像文件属性提取系统设计与实现 目录 设计题目设计内容系统分析总体设计详细设计程序实现…

opencv--基础

opencv OpenCV是一个实现数字图像处理和计算机视觉通用算法的开源跨平台库。 链接 opencv中的cv是什么意思 在OpenCV中&#xff0c;"cv" 是 "Computer Vision"&#xff08;计算机视觉&#xff09; 的缩写。 opencv的实现语言 opencv的底层实现代码是使…

Java创建对象的方式

1、通过new关键字创建新对象 用new关键字创建对象是我们在开发中最常用的方式&#xff0c;new关键字会为我们在堆内存中开辟一块空间以存放对象的引用&#xff08;包含对象本身以及内部属性的引用&#xff09;。 2、通过newInstance()方法创建新对象 newInstance()方法本质上是…

构建具备推理与反思能力的高级 Prompt:LLM 智能代理设计指南

在构建强大的 AI 系统&#xff0c;尤其是基于大语言模型&#xff08;LLM&#xff09;的智能代理&#xff08;Agent&#xff09;时&#xff0c;Prompt 设计的质量决定了系统的智能程度。传统 Prompt 通常是简单的问答或填空式指令&#xff0c;而高级任务需要更具结构性、策略性和…

猪行为视频数据集

猪行为数据集包含 23 天(超过 6 周)的日间猪行为视频,这些视频由近乎架空的摄像机拍摄。视频已配准颜色和深度信息。数据以每秒 6 帧的速度捕获,并以 1800 帧(5 分钟)为一批次进行存储。大多数帧显示 8 头猪。 这里可以看到颜色和深度图像的示例: 喂食器位于图片底部中…

C++运算符重载详解

C++ 中的运算符重载允许为用户自定义类型(类或结构体)赋予运算符特定功能,使其操作更直观。以下是运算符重载的关键点: 1. 基本语法 成员函数重载:运算符作为类的成员函数,左操作数为当前对象 (this),右操作数为参数。 class Complex {public:Complex operator+(const …

deep-share开源浏览器扩展,用于分享 DeepSeek 对话,使用户能够将对话内容保存为图片或文本以便轻松分享

一、软件介绍 文末提供程序和源码下载学习 deep-share开源浏览器扩展&#xff0c;用于分享 DeepSeek 对话&#xff0c;使用户能够将对话内容保存为图片或文本以便轻松分享。 二、软件功能 One-click capture of DeepSeek chat content一键捕获 DeepSeek 聊天内容Support sha…

Unity之如何实现RenderStreaming视频推流

文章目录 前言引入 UnityRenderStreaming 的好处教程步骤 1:设置环境步骤 2: 创建项目步骤 3:安装软件包步骤 5:下载示例步骤 6:检查配置环境步骤 7:打开推流场景步骤 8: 准备用于流式传输的WebServer应用程序步骤 9: 运行 示例场景步骤 10:检查视频是否在浏览器中显示…

30天开发操作系统 第26天 -- 为窗口移动提速

前言 昨天我们增加了可同时启动的应用程序的数量&#xff0c;窗口也跟着变多了&#xff0c;整个画面变得热闹起来。 话说&#xff0c;在对比color.hrb和color2.hrb的时候我们需要移动窗口&#xff0c;那个时候笔者感到窗口移动的速度很慢。在真机环境下的速度还算可以接受&…

9.QT-显示类控件|Label|显示不同格式的文本|显示图片|文本对齐|自动换行|缩进|边距|设置伙伴(C++)

Label QLabel 可以⽤来显⽰⽂本和图⽚ 属性说明textQLabel中的⽂本textFormat⽂本的格式.• Qt::PlainText 纯⽂本• Qt::RichText 富⽂本(⽀持html标签)• Qt::MarkdownText markdown格式• Qt::AutoText 根据⽂本内容⾃动决定⽂本格式pixmapQLabel 内部包含的图⽚.scaledCo…

非参数检验题目集

非参数检验题目集 对医学计量资料成组比较&#xff0c;相对参数检验来说&#xff0c;非参数秩和检验的优点是&#xff08; &#xff09; A. 适用范围广 B. 检验效能高 C. 检验结果更准确 D. 充分利用资料信息 E. 不易出现假阴性错误 对于计量资料的比较&#xff0c;在满足参数…

libdxfrw库使用总结

在 Win11VS2022CMake 平台编译 libdxfrw 库的挑战与应对 在当今数字化设计与开发领域&#xff0c;高效处理 CAD 文件格式如 DXF 是众多项目的关键需求。libdxfrw 库作为一种功能强大的工具&#xff0c;能助力开发者精准解析与写入 DXF 文件&#xff0c;使其在众多应用场景中备…

C++学习:六个月从基础到就业——内存管理:RAII原则

C学习&#xff1a;六个月从基础到就业——内存管理&#xff1a;RAII原则 本文是我C学习之旅系列的第十九篇技术文章&#xff0c;也是第二阶段"C进阶特性"的第四篇&#xff0c;主要介绍C中的RAII原则及其在资源管理中的应用。查看完整系列目录了解更多内容。 引言 在…

【愚公系列】《Python网络爬虫从入门到精通》056-Scrapy_Redis分布式爬虫(Scrapy-Redis 模块)

&#x1f31f;【技术大咖愚公搬代码&#xff1a;全栈专家的成长之路&#xff0c;你关注的宝藏博主在这里&#xff01;】&#x1f31f; &#x1f4e3;开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主&#xff01; &#x1f…

PyTorch基础笔记

PyTorch张量 多维数组&#xff1a;张量可以是标量&#xff08;0D&#xff09;、向量&#xff08;1D&#xff09;、矩阵&#xff08;2D&#xff09;或更高维的数据&#xff08;3D&#xff09;。 数据类型&#xff1a;支持多种数据类型&#xff08;如 float32, int64, bool 等&a…

OSCP - Proving Grounds - Sar

主要知识点 路径爆破cronjob 脚本劫持提权 具体步骤 依旧nmap 开始,开放了22和80端口 Nmap scan report for 192.168.192.35 Host is up (0.43s latency). Not shown: 65524 closed tcp ports (reset) PORT STATE SERVICE VERSION 22/tcp open ssh Open…

存储/服务器内存的基本概念简介

为什么写这个文章&#xff1f;今天处理一个powerstore 3000T 控制器&#xff0c;控制器上电后&#xff0c;亮一下灯就很快熄灭了&#xff0c;然后embedded module上和io module不加电&#xff0c;过一整子系统自动就下电了&#xff0c;串口没有任何输出。刚开始判断是主板的问题…

软件开发指南——GUI 开发方案推荐

1. LVGL (Light and Versatile Graphics Library) 适用场景&#xff1a;嵌入式设备、资源受限环境 优势&#xff1a; 专为嵌入式设计的开源 GUI 库&#xff0c;内存占用极小&#xff08;最低仅需 64KB RAM&#xff09;支持触摸屏、硬件加速&#xff08;如 STM32 的 LTDC&…