轻量级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…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...
Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件
今天呢,博主的学习进度也是步入了Java Mybatis 框架,目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学,希望能对大家有所帮助,也特别欢迎大家指点不足之处,小生很乐意接受正确的建议&…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...
BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
算法:模拟
1.替换所有的问号 1576. 替换所有的问号 - 力扣(LeetCode) 遍历字符串:通过外层循环逐一检查每个字符。遇到 ? 时处理: 内层循环遍历小写字母(a 到 z)。对每个字母检查是否满足: 与…...
C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、为什么要开发一个日历程序?1. 深入理解时间算法2. 练习面向对象设计3. 学习数据结构应用二、核心算法深度解析…...
Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)
引言 在人工智能飞速发展的今天,大语言模型(Large Language Models, LLMs)已成为技术领域的焦点。从智能写作到代码生成,LLM 的应用场景不断扩展,深刻改变了我们的工作和生活方式。然而,理解这些模型的内部…...
使用SSE解决获取状态不一致问题
使用SSE解决获取状态不一致问题 1. 问题描述2. SSE介绍2.1 SSE 的工作原理2.2 SSE 的事件格式规范2.3 SSE与其他技术对比2.4 SSE 的优缺点 3. 实战代码 1. 问题描述 目前做的一个功能是上传多个文件,这个上传文件是整体功能的一部分,文件在上传的过程中…...
解析两阶段提交与三阶段提交的核心差异及MySQL实现方案
引言 在分布式系统的事务处理中,如何保障跨节点数据操作的一致性始终是核心挑战。经典的两阶段提交协议(2PC)通过准备阶段与提交阶段的协调机制,以同步决策模式确保事务原子性。其改进版本三阶段提交协议(3PC…...
