轻量级TCC框架的实现
现有seata、tcc-transaction等tcc框架实现都较为重量级,今天主要带来一种轻量级的实现,大概只用了1200行代码实现。不依赖具体框架grpc、http、dubbo等,只需要业务系统按照标准化实现Try、Commit、Cancel实现接口即可。
已解决悬挂、幂等、空回滚、事务嵌套问题,业务层面无需关注这部分处理。
TCC分为以下几个阶段:
- 执行前置动作 (业务资源的初始化,例如: 创建一个初始化的订单)
- Try (调用外部服务,进行资源的预留)
- 执行本地事务 (需要保证业务是在一个事务内完成)
- Commit\Cancel (根据本地事务的执行的成功与否,进行commit || cancel)

示例
该示例主要用于用户下单的同时,需要扣减用户积分的场景,订单服务和积分服务分别是独立服务部署,它们之间存在分布式事务的问题, 我们通过当前框架展示是如何解决以上问题的。
tcc/src/test/java/com/damon/sample at master · 654894017/tcc · GitHub
步骤1.初始化订单服务数据库表
-- 创建事务表
CREATE TABLE `tcc_main_log_order` (`biz_id` bigint NOT NULL COMMENT '业务id',`status` int NOT NULL DEFAULT '0' COMMENT '状态: 1 创建事务成功 2 回滚成功 3 完成本地事务成功 4 提交事务成功',`version` int NOT NULL DEFAULT '0' COMMENT '版本号',`last_update_time` bigint NOT NULL DEFAULT '0' COMMENT '最后更新时间',`create_time` bigint NOT NULL DEFAULT '0' COMMENT '创建时间',`checked_times` int NOT NULL DEFAULT '0' COMMENT '失败检查次数',PRIMARY KEY (`biz_id`),KEY `idx_status_checked_times_create_time` (`status`,`checked_times`,`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='主事务日志表';-- 创建订单表
CREATE TABLE `tcc_demo_order` (`order_id` bigint NOT NULL,`status` int NOT NULL,`user_id` bigint NOT NULL,`deduction_points` bigint DEFAULT NULL,PRIMARY KEY (`order_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
步骤2.积分服务创建子事务表
-- 创建子事务表
CREATE TABLE `tcc_sub_log_order` (`biz_id` bigint NOT NULL COMMENT '业务id',`sub_biz_id` bigint NOT NULL DEFAULT '0' COMMENT '子业务id',`status` int NOT NULL DEFAULT '0' COMMENT '状态: 1 创建事务成功 2 提交事务成功 3 回滚事务成功',`version` int NOT NULL DEFAULT '0' COMMENT '版本号',`last_update_time` bigint NOT NULL DEFAULT '0' COMMENT '最后更新时间',`create_time` bigint NOT NULL DEFAULT '0' COMMENT '创建时间',PRIMARY KEY (`biz_id`,`sub_biz_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='子事务日志表';-- 创建积分变动日志表
CREATE TABLE `tcc_demo_points_changing_log` (`biz_id` bigint NOT NULL,`user_id` bigint NOT NULL,`change_points` bigint NOT NULL,`change_type` int NOT NULL,`status` int NOT NULL,PRIMARY KEY (`biz_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;-- 创建用户积分表
CREATE TABLE `tcc_demo_user_points` (`user_id` bigint NOT NULL,`points` bigint NOT NULL,PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;-- 初始化用户积分
INSERT INTO `tcc_demo_user_points` (`user_id`, `points`) VALUES (12345678, 999999999989989999);
注意事项
事务表都是以tcc_main_log_xxxx 命名,子事务表都是以tcc_sub_log_xxxx命名,xxxx为业务分类,例如订单下单的业务,事务表命名为tcc_main_log_order, 子事务表命名为tcc_sub_log_order.
步骤3.运行 com.damon.sample.points.PointsApplication
步骤4.运行 com.damon.sample.order.TestRun
下单服务
下单服务继承TccMainService服务
package com.damon.sample.order.app;import cn.hutool.core.util.IdUtil;
import com.damon.sample.order.client.IOrderSubmitAppService;
import com.damon.sample.order.domain.IPointsGateway;
import com.damon.sample.order.domain.Order;
import com.damon.tcc.TccMainService;
import com.damon.tcc.config.TccMainConfig;
import com.damon.tcc.exception.TccLocalTransactionException;
import com.damon.tcc.exception.TccPrepareException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;import java.util.HashMap;
import java.util.Map;@Service
public class OrderSubmitAppService extends TccMainService<Long, Map<String, Boolean>, Order> implements IOrderSubmitAppService {private final JdbcTemplate jdbcTemplate;private final IPointsGateway pointsGateway;@Autowiredpublic OrderSubmitAppService(TccMainConfig config, IPointsGateway pointsGateway) {super(config);this.jdbcTemplate = new JdbcTemplate(config.getDataSource());this.pointsGateway = pointsGateway;}/*** 检查失败的日志,用于纠正事务是否需要回顾还是提交*/public void executeFailedLogCheck() {super.executeFailedLogCheck();}/*** 检查死亡的日志,用于纠正事务是否需要回顾还是提交*/public void executeDeadLogCheck() {super.executeDeadLogCheck();}/*** 执行失败日志检查的时候需要回查请求参数(因为事务日志未记录方法请求参数,所以需要回查一下)** @param bizId 实体对象id(业务id)* @return*/@Overrideprotected Order callbackParameter(Long bizId) {return jdbcTemplate.queryForObject("select * from tcc_demo_order where order_id = ? ", new BeanPropertyRowMapper<>(Order.class), bizId);}/*** 创建订单 (1 预先创建订单 2 执行try动作)** @param userId* @param points* @return*/@Overridepublic Long submitOrder(Long userId, Long points) {Long orderId = IdUtil.getSnowflakeNextId();// 预创建订单jdbcTemplate.update("insert into tcc_demo_order(order_id, user_id, status, deduction_points) values (?, ?, ? ,? )",orderId, userId, 0, points);Order order = new Order(orderId, 0, userId, points);try {return super.process(order);} catch (TccPrepareException e) {throw e;} catch (TccLocalTransactionException e) {throw e;} catch (Exception e) {throw new RuntimeException("系统异常");}}/*** try执行用户积分扣除** @param order* @return*/@Overrideprotected Map<String, Boolean> prepare(Order order) {Boolean result = pointsGateway.tryDeductionPoints(order.getOrderId(), order.getUserId(), order.getDeductionPoints());Map<String, Boolean> map = new HashMap<>();map.put("flag", result);return map;}@Overrideprotected Long executeLocalTransaction(Order object, Map<String, Boolean> map) {int result = jdbcTemplate.update("update tcc_demo_order set status = ? where order_id = ? ", 1, object.getOrderId());if (result == 0) {throw new RuntimeException("无效的订单id : " + object.getOrderId());}return object.getOrderId();}/*** commit积分** @param order*/@Overrideprotected void commit(Order order) {pointsGateway.commitDeductionPoints(order.getOrderId(), order.getUserId(), order.getDeductionPoints());}/*** cancel回滚积分** @param order*/@Overrideprotected void cancel(Order order) {pointsGateway.cancelDeductionPoints(order.getOrderId(), order.getUserId(), order.getDeductionPoints());}
}
积分服务
积分服务继承TccSubService服务
package com.damon.sample.points.app;import com.damon.sample.points.client.IPointsDeductionAppService;
import com.damon.sample.points.client.PointsDeductCmd;
import com.damon.tcc.config.TccSubConfig;
import com.damon.tcc.TccSubService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionSynchronizationManager;@Service
public class PointsDeductionAppService extends TccSubService<Boolean, PointsDeductCmd> implements IPointsDeductionAppService {private final Logger log = LoggerFactory.getLogger(PointsDeductionAppService.class);private final JdbcTemplate jdbcTemplate;@Autowiredpublic PointsDeductionAppService(TccSubConfig config) {super(config);this.jdbcTemplate = new JdbcTemplate(config.getDataSource());}/*** try执行积分扣减* @param parameter* @return*/@Overridepublic boolean attempt(PointsDeductCmd parameter) {return super.prepare(parameter, cmd -> {int result = jdbcTemplate.update("update tcc_demo_user_points set points = points - ? where user_id = ? and points - ? >= 0",cmd.getDeductionPoints(), cmd.getUserId(), cmd.getDeductionPoints());boolean transactionActive = TransactionSynchronizationManager.isActualTransactionActive();if (result == 0) {throw new RuntimeException("用户积分不足 || 用户不存在");}int result2 = jdbcTemplate.update("insert tcc_demo_points_changing_Log (user_id, change_points, change_type, biz_id, status) values(?,?,?,?,?)",cmd.getUserId(), cmd.getDeductionPoints(), 1, cmd.getOrderId(), 0);return true;});}/*** commit提交积分扣减* @param parameter*/@Overridepublic void commit(PointsDeductCmd parameter) {super.commit(parameter, cmd -> {int result = jdbcTemplate.update("update tcc_demo_points_changing_Log set status = 1 where biz_id = ?", cmd.getBizId());if (result == 0) {throw new RuntimeException("无效的业务id,无法积分commit");}});}/*** cancel回滚积分扣减* @param parameter*/@Overridepublic void cancel(PointsDeductCmd parameter) {super.cancel(parameter, cmd -> {int result = jdbcTemplate.update("update tcc_demo_points_changing_Log set status = 2 where biz_id = ?", cmd.getBizId());if (result == 0) {log.error("无效的业务id : {},无法进行积分cancel", cmd.getBizId());return;}int result2 = jdbcTemplate.update("update tcc_demo_user_points set points = points + ? where user_id = ?",cmd.getDeductionPoints(), cmd.getUserId());if (result2 == 0) {throw new RuntimeException("无效的用户id,无法进行积分rollback");}});}}
FAQ
1.关于幂等、悬挂、空回滚如何解决?
主要基于执行先判断的思路来实现的,在子系统执行Cancle的时候,都会先判断tcc_sub_log_xxxx这个表的事务事务已经完成或者取消,如果已完成直接不执行业务,如果未执行则执行Cancel业务,同时更新tcc_sub_log_xxxx的日志状态为已取消。最极端情况举一个示例,假如服务提供者try和cancel同时在执行(try因为网络问题非常滞后的到达业务服务器,这时主服务因为等待超时,调用了子服务cancel动作)。主要分两种情况:
1.假如先执行了cancel,在执行Try则不会执行,因为子事务已经取消(tcc_sub_log_xxxx会增加一条取消日志),在执行try操作时会出现索引冲突异常(tcc_sub_log_xxxx表有biz_id + sub_biz_id唯一索引),子事务的try会回滚。
2.假如先执行了try,在执行cancel则会执行正常取消,属于正常情况。还一种情况就是同时执行了Try、cancel操作,这时候只能依赖tcc_sub_log_xxxx表有biz_id + sub_biz_id唯一索引来解决更新冲突问题。假如Try先执行,cacel就会报错,上游服务重新发起cancel即可。假如先执行了cancle,则try会报错(唯一索引冲突),这时不用处理,子事务的try会回滚。
2.tcc_sub_log_xxxx表事务需要和本地业务在一个数据库事务?
是的,幂等、悬挂、空回滚问题都是基于tcc_sub_log_xxxx的事务日志表进行的,需要保证业务事务和tcc_sub_log_xxxx表的事务在一个数据库事务下。
3.上游系统重放try、commit、cancel怎么处理?
调用方误触发,存在以下几种可能
1.已经commit的事务,调用了cancle,已增加事务是否已commit判断,已commit的事务调用cancel不会执行,同时需要基于tcc_sub_log_xxxx表的version实现的乐观锁来解决更新冲突的问题。
2.已cancel的事务,调用了commit,已增加事务是否已cancel判断,已cancel的事务调用coommit不会执行,同时需要基于tcc_sub_log_xxxx表的version实现的乐观锁来解决更新冲突的问题。
3.已cancel的事务,调用了try,依赖tcc_sub_log_xxxx表biz_id + sub_biz_id唯一索引来解决更新冲突问题。
4.已commit的事务,调用了try,依赖tcc_sub_log_xxxx表biz_id + sub_biz_id唯一索引来解决更新冲突问题。
5.重复try依赖tcc_sub_log_xxxx表biz_id + sub_biz_id唯一索引来解决更新冲突问题。
6.重复commit,已增加事务是否已commit判断,已commit的事务调用commit不会执行,同时需要基于tcc_sub_log_xxxx表的version实现的乐观锁来解决更新冲突的问题。
7.重复cancel,已增加事务是否已cancel判断,已cancel的事务调用cancel不会执行,同时需要基于tcc_sub_log_xxxx表的version实现的乐观锁来解决更新冲突的问题。
GitHub - 654894017/tcc: 编程式tcc框架,已解决悬挂、幂等、空回滚、事务嵌套问题。
相关文章:
轻量级TCC框架的实现
现有seata、tcc-transaction等tcc框架实现都较为重量级,今天主要带来一种轻量级的实现,大概只用了1200行代码实现。不依赖具体框架grpc、http、dubbo等,只需要业务系统按照标准化实现Try、Commit、Cancel实现接口即可。 已解决悬挂、幂等、空…...
共绘智慧升级,看永洪科技助力由由集团起航智慧征途
在数字化洪流汹涌澎湃的当下,企业如何乘风破浪,把握转型升级的黄金机遇,已成为所有企业必须直面的时代命题。由由集团,作为房地产的领航者,始终以前瞻视野引领变革,坚决拥抱数字化浪潮,携手数字…...
小程序开发总结
今年第一次帮别人做小程序。 从开始动手到完成上线,一共耗时两天。AI 让写代码变得简单、高效。 不过,小程序和 Flutter 等大厂开发框架差距实在太大,导致我一开始根本找不到感觉。 第一,IDE 不好用,各种功能杂糅在…...
元脑服务器:浪潮信息引领AI基础设施的创新与发展
根据国际著名研究机构GlobalData于2月19日发布的最新报告,浪潮信息在全球数据中心领域的竞争力评估中表现出色,凭借其在算力算法、开放加速计算和液冷技术等方面的创新,获得了“Leader”评级。在创新、增长力与稳健性两个主要维度上ÿ…...
uniapp+node+mysql接入deepseek实现流式输出
node import express from express; import mysql from mysql2; import cors from cors; import bodyParser from body-parser; import axios from axios; import { WebSocketServer } from ws; // 正确导入 WebSocketServerconst app express();// Middlewares app.use(cors…...
PHP MySQL 创建数据库
PHP MySQL 创建数据库 引言 在网站开发中,数据库是存储和管理数据的核心部分。PHP 和 MySQL 是最常用的网页开发语言和数据库管理系统之一。本文将详细介绍如何在 PHP 中使用 MySQL 创建数据库,并对其操作进行详细讲解。 前提条件 在开始创建数据库之…...
UE4 World, Level, LevelStreaming从入门到深入
前言 在《塞尔达传说:旷野之息》中,玩家攀上初始高塔的瞬间,目光所及的山川湖泊皆可抵达;在《艾尔登法环》中,黄金树的辉光始终悬于地平线之上,指引玩家穿越无缝衔接的史诗战场。这些现代游戏杰作背后的核…...
3月8日实验
拓扑: 需求: 1.学校内部的HTTP客户端可以正常通过域名www.baidu.com访问到白度网络中的HTTP服务器 2.学校网络内部网段基于192.168.1.0/24划分,PC1可以正常访问3.3.3.0/24网段,但是PC2不允许 3.学校内部路由使用静态路由&#…...
IO多路复用实现并发服务器
一.select函数 select 的调用注意事项 在使用 select 函数时,需要注意以下几个关键点: 1. 参数的修改与拷贝 readfds 等参数是结果参数 : select 函数会直接修改传入的 fd_set(如 readfds、writefds 和 exceptfds…...
【漫话机器学习系列】122.相关系数(Correlation Coefficient)
深入理解相关系数(Correlation Coefficient) 1. 引言 在数据分析、统计学和机器学习领域,研究变量之间的关系是至关重要的任务。我们常常想知道:当一个变量变化时,另一个变量是否也会随之变化?如果会&…...
控制系统分类
文章目录 定义与特点1. 自治系统(Autonomous System)与非自治系统(Non-Autonomous System)自治系统非自治系统 2. 线性系统(Linear System)与非线性系统(Nonlinear System)线性系统非…...
文档操作方法得合理使用
博主介绍:✌全网粉丝5W,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战,博主也曾写过优秀论文,查重率极低,在这方面有丰富的经验…...
Python asyncIO 面试题及参考答案 草
目录 如何正确定义一个协程函数?直接调用协程会引发什么问题? 使用 async def 定义的协程与普通函数执行流程有何本质区别? 解释 asyncio.run () 的作用及与手动管理事件循环的差异 为什么协程中必须使用 await 而非 yield 挂起操作? 写出通过 async for 实现异步迭代器…...
计算机网络——交换机
一、什么是交换机? 交换机(Switch)是局域网(LAN)中的核心设备,负责在 数据链路层(OSI第二层)高效转发数据帧。它像一位“智能交通警察”,根据设备的 MAC地址 精准引导数…...
matlab和FPGA联合仿真时读写.txt文件数据的方法
在FPGA开发过程中,往往需要将MATLAB生成的数据作为原始激励灌入FPGA进行仿真。为了验证FPGA计算是否正确,又需要将FPGA计算结果导入MATLAB绘图与MATLAB计算结果对比。 下面是MATLAB“写.txt”、“读.txt”,Verilog“读.txt”、“写.txt”的代…...
解锁DeepSpeek-R1大模型微调:从训练到部署,打造定制化AI会话系统
目录 1. 前言 2.大模型微调概念简述 2.1. 按学习范式分类 2.2. 按参数更新范围分类 2.3. 大模型微调框架简介 3. DeepSpeek R1大模型微调实战 3.1.LLaMA-Factory基础环境安装 3.1大模型下载 3.2. 大模型训练 3.3. 大模型部署 3.4. 微调大模型融合基于SpirngBootVue2…...
【分布式】聊聊分布式id实现方案和生产经验
对于分布式Id来说,在面试过程中也是高频面试题,所以主要针对分布式id实现方案进行详细分析下。 应用场景 对于无论是单机还是分布式系统来说,对于很多场景需要全局唯一ID, 数据库id唯一性日志traceId 可以方便找到日志链&#…...
uniapp或者vue 使用serialport
参考https://blog.csdn.net/ykee126/article/details/90440499 版本是第一位:否则容易编译失败 node 版本 18.14.0 npm 版本 9.3.1 electron 版本 30.0.8 electron-rebuild 版本 3.2.9 serialport 版本 10.0.0 需要python环境 main.js // Modules to control app…...
机器学习12-视觉识别任务
机器学习12-视觉识别任务 分类语义分割滑动窗口滑动窗口的实现思路优点缺点现代替代方法 全卷积(Fully Convolutional Networks, FCN)FCN 的工作原理FCN 的性能优势FCN 的应用案例FCN 的局限性改进方向下采样可学习的上采样:转置卷积 目标检测区域建议Se…...
使用paramiko爆破ssh登录
一.确认是否存在目标主机是否存在root用户 重跑 CVE-2018-15473用户名枚举漏洞 检测: import paramiko from paramiko.ssh_exception import AuthenticationExceptiondef check_user(username, hostname, port):ssh paramiko.SSHClient()ssh.set_missing_host_key…...
Qwen-Image-Edit-2511保姆级教程:零基础学会AI修图,效果惊艳
Qwen-Image-Edit-2511保姆级教程:零基础学会AI修图,效果惊艳 1. 前言:为什么选择Qwen-Image-Edit-2511 如果你还在为Photoshop复杂的操作界面头疼,或者想快速实现专业级的图片编辑效果,那么Qwen-Image-Edit-2511绝对…...
Java 使用国密算法实现数据加密传输
本文是混合加密:前端 SM2 SM4,后端 Spring Boot Hutool 解密的完整示例。 方案的逻辑是: 前端随机生成一个 SM4 key 用 SM4 加密整个业务 JSON 用后端提供的 SM2 公钥 加密这个 SM4 key 后端先用 SM2 私钥 解出 SM4 key 再用 SM4 解出…...
Apache Doris 4.0.4:解锁数据管理新境界
Apache Doris 4.0 作为重要里程碑发布后,社区通过 4.0.1 至 4.0.4 版本快速演进。如今 4.0.4 正式登场,功能更稳定可靠,引领其从实时分析迈向数据管理领域。面向 AI 工作负载的混合搜索能力检索成现代数据平台核心负载,Apache Dor…...
OpenClaw与Qwen3-VL:30B:高效个人AI办公助手实战
OpenClaw与Qwen3-VL:30B:高效个人AI办公助手实战 1. 为什么选择OpenClawQwen3-VL组合 去年冬天,当我第5次因为会议记录整理到凌晨两点时,终于决定寻找自动化解决方案。在尝试了市面上各种RPA工具后,偶然发现了OpenClaw这个开源框…...
导师推荐!2026年最值得用的专业AI论文写作工具
2026年AI论文写作工具已从“单点辅助”升级为智能化学术研究系统,核心评价维度涵盖文献真实性、格式合规性、长文本逻辑、查重降重、AIGC合规等关键指标。本次测评覆盖6款主流工具,测试场景包括中文与英文论文、全流程与专项功能、免费与付费版本&#x…...
突破微信设备限制:WeChatPad如何实现免Root双设备同时在线
突破微信设备限制:WeChatPad如何实现免Root双设备同时在线 【免费下载链接】WeChatPad 强制使用微信平板模式 项目地址: https://gitcode.com/gh_mirrors/we/WeChatPad 你是否曾因微信只能单设备登录而错失重要消息?是否渴望在手机和平板上同时接…...
Obsidian插件终极汉化指南:obsidian-i18n让英文插件秒变中文界面
Obsidian插件终极汉化指南:obsidian-i18n让英文插件秒变中文界面 【免费下载链接】obsidian-i18n 项目地址: https://gitcode.com/gh_mirrors/ob/obsidian-i18n 你是否因为Obsidian插件的英文界面而头疼?面对"Backlink"、"Graph …...
手把手教你用Wireshark抓包分析Opener EIP通信,快速定位ForwardOpen失败原因
深度解析EtherNet/IP通信:用Wireshark诊断ForwardOpen失败的实战指南 当你在MCU上成功移植了Opener协议栈,TCP连接建立正常,却在关键时刻遭遇ForwardOpen失败时,那种挫败感我深有体会。去年在汽车生产线控制系统项目中,…...
【分箱进阶篇】分箱的工程细节:从训练到部署的完整模式
基础篇参考:【分箱基础篇】pandas 分箱双子星:pd.cut 与 pd.qcut 我们在基础篇讲了 pd.cut 和 pd.qcut 各自怎么用。但在实际项目里,分箱不是调一次函数就完事的。通常来说,训练集上算出来的切分点要保存下来,测试集…...
如何快速掌握AI变声神器RVC:面向初学者的完整指南
如何快速掌握AI变声神器RVC:面向初学者的完整指南 【免费下载链接】Retrieval-based-Voice-Conversion-WebUI 语音数据小于等于10分钟也可以用来训练一个优秀的变声模型! 项目地址: https://gitcode.com/GitHub_Trending/re/Retrieval-based-Voice-Con…...
