1.什么是TCC
2. Seata TCC 模式
2.1. TCC模式接口定义
/**
* @author qingzhou
*
* 通过 @LocalTCC 这个注解,RM 初始化的时候会向 TC 注册一个分支事务。
*/
@LocalTCC
public interface OrderService {
@TwoPhaseBusinessAction(name = "prepareSaveOrder", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
Order prepareSaveOrder(OrderVo orderVo, @BusinessActionContextParameter(paramName = "orderId") Long orderId);
boolean commit(BusinessActionContext actionContext);
boolean rollback(BusinessActionContext actionContext);
}
2.2. Seata 1.5.1 版本与旧版配置区别
2.2.1.数据库表
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
CREATE TABLE `tcc_fence_log` (
`xid` varchar(128) NOT NULL COMMENT 'global id',
`branch_id` bigint(20) NOT NULL COMMENT 'branch id',
`action_name` varchar(64) NOT NULL COMMENT 'action name',
`status` tinyint(4) NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
`gmt_create` datetime(3) NOT NULL COMMENT 'create time',
`gmt_modified` datetime(3) NOT NULL COMMENT 'update time',
PRIMARY KEY (`xid`,`branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2.2.2.seata服务端配置文件
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: seata
password: seata
seata:
config:
# support: nacos 、 consul 、 apollo 、 zk 、 etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: 7e838c12-8554-4231-82d5-6d93573ddf32
group: SEATA_GROUP
username:
password:
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key: ""
#secret-key: ""
data-id: seataServer.properties
registry:
# support: nacos 、 eureka 、 redis 、 zk 、 consul 、 etcd3 、 sofa
type: nacos
preferred-networks: 30.240.*
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace:
cluster: default
username:
password:
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key: ""
#secret-key: ""
store:
# support: file 、 db 、 redis
mode: db
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
2.2.3.nacos 上的seataServer.properties
#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
#Transport configuration, for client and server
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none
#Transaction routing rules configuration, only for the client
service.vgroupMapping.default_tx_group=default
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
#Transaction rule configuration, only for the client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
#For TCC transaction mode
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h
#Log rule configuration, for client and server
log.exceptionRate=100
#Transaction storage configuration, only for the server. The file, DB, and redis configuration values are optional.
store.mode=db
store.lock.mode=file
store.session.mode=file
#Used for password encryption
store.publicKey=
#If `store.mode,store.lock.mode,store.session.mode` are not equal to `file`, you can remove the configuration block.
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
#These configurations are required if the `store mode` is `redis`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `redis`, you can remove the configuration block.
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.sentinel.masterName=
store.redis.sentinel.sentinelHosts=
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100
#Transaction rule configuration, only for the server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
server.xaerNotaRetryTimeout=60000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
server:
port: 8028
spring:
application:
name: tcc-order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_tcc_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: false
wall:
config:
multi-statement-allow: true
logging:
level:
com.tuling: debug
seata:
application-id: ${spring.application.name}
# seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
tx-service-group: default_tx_group
registry:
# 指定nacos作为注册中心
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
config:
# 指定nacos作为配置中心
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: 7e838c12-8554-4231-82d5-6d93573ddf32
group: SEATA_GROUP
data-id: seataServer.properties
#暴露actuator端点
management:
endpoints:
web:
exposure:
include: '*'
2.3.案例
2.3.1.BusinessServiceImpl聚合业务类
package com.qingzhou.tccorderservice.service.impl;
import com.qingzhou.datasource.entity.Order;
import com.qingzhou.tccorderservice.feign.AccountFeignService;
import com.qingzhou.tccorderservice.feign.StorageFeignService;
import com.qingzhou.tccorderservice.service.BussinessService;
import com.qingzhou.tccorderservice.service.OrderService;
import com.qingzhou.tccorderservice.util.UUIDGenerator;
import com.qingzhou.tccorderservice.vo.OrderVo;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author qingzhou
*/
@Service
@Slf4j
public class BusinessServiceImpl implements BussinessService {
@Autowired
private AccountFeignService accountFeignService;
@Autowired
private StorageFeignService storageFeignService;
@Autowired
private OrderService orderService;
@Override
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
public Order saveOrder(OrderVo orderVo) {
log.info("=============用户下单=================");
log.info("当前 XID: {}", RootContext.getXID());
//获取全局唯一订单号
Long orderId = UUIDGenerator.generateUUID();
//阶段一: 创建订单
Order order = orderService.prepareSaveOrder(orderVo,orderId);
//扣减库存
storageFeignService.deduct(orderVo.getCommodityCode(), orderVo.getCount());
//扣减余额
accountFeignService.debit(orderVo.getUserId(), orderVo.getMoney());
return order;
}
}
2.3.2.OrderService订单服务
package com.qingzhou.tccorderservice.service;
import com.qingzhou.datasource.entity.Order;
import com.qingzhou.tccorderservice.vo.OrderVo;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
/**
* @author qingzhou
*
* 通过 @LocalTCC 这个注解,RM 初始化的时候会向 TC 注册一个分支事务。
*/
@LocalTCC
public interface OrderService {
/**
* TCC的try方法:保存订单信息,状态为支付中
*
* 定义两阶段提交,在try阶段通过@TwoPhaseBusinessAction注解定义了分支事务的 resourceId,commit和 cancel 方法
* name = 该tcc的bean名称,全局唯一
* commitMethod = commit 为二阶段确认方法
* rollbackMethod = rollback 为二阶段取消方法
* BusinessActionContextParameter注解 传递参数到二阶段中
* useTCCFence seata1.5.1的新特性,用于解决TCC幂等,悬挂,空回滚问题,需增加日志表tcc_fence_log
*/
@TwoPhaseBusinessAction(name = "prepareSaveOrder", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
Order prepareSaveOrder(OrderVo orderVo, @BusinessActionContextParameter(paramName = "orderId") Long orderId);
/**
*
* TCC的confirm方法:订单状态改为支付成功
*
* 二阶段确认方法可以另命名,但要保证与commitMethod一致
* context可以传递try方法的参数
*
* @param actionContext
* @return
*/
boolean commit(BusinessActionContext actionContext);
/**
* TCC的cancel方法:订单状态改为支付失败
* 二阶段取消方法可以另命名,但要保证与rollbackMethod一致
*
* @param actionContext
* @return
*/
boolean rollback(BusinessActionContext actionContext);
}
package com.qingzhou.tccorderservice.service.impl;
import com.qingzhou.datasource.entity.Order;
import com.qingzhou.datasource.entity.OrderStatus;
import com.qingzhou.datasource.mapper.OrderMapper;
import com.qingzhou.tccorderservice.feign.AccountFeignService;
import com.qingzhou.tccorderservice.feign.StorageFeignService;
import com.qingzhou.tccorderservice.service.OrderService;
import com.qingzhou.tccorderservice.vo.OrderVo;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author qingzhou
*/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountFeignService accountFeignService;
@Autowired
private StorageFeignService storageFeignService;
@Autowired
private OrderService orderService;
@Override
@Transactional(rollbackFor=Exception.class)
public Order prepareSaveOrder(OrderVo orderVo,
@BusinessActionContextParameter(paramName = "orderId") Long orderId) {
// 保存订单
Order order = new Order();
order.setId(orderId);
order.setUserId(orderVo.getUserId());
order.setCommodityCode(orderVo.getCommodityCode());
order.setCount(orderVo.getCount());
order.setMoney(orderVo.getMoney());
order.setStatus(OrderStatus.INIT.getValue());
Integer saveOrderRecord = orderMapper.insert(order);
log.info("保存订单{}", saveOrderRecord > 0 ? "成功" : "失败");
return order;
}
@Override
public boolean commit(BusinessActionContext actionContext) {
// 获取订单id
long orderId = Long.parseLong(actionContext.getActionContext("orderId").toString());
//更新订单状态为支付成功
Integer updateOrderRecord = orderMapper.updateOrderStatus(orderId,OrderStatus.SUCCESS.getValue());
log.info("更新订单id:{} {}", orderId, updateOrderRecord > 0 ? "成功" : "失败");
return true;
}
@Override
public boolean rollback(BusinessActionContext actionContext) {
//获取订单id
long orderId = Long.parseLong(actionContext.getActionContext("orderId").toString());
//更新订单状态为支付失败
Integer updateOrderRecord = orderMapper.updateOrderStatus(orderId,OrderStatus.FAIL.getValue());
log.info("更新订单id:{} {}", orderId, updateOrderRecord > 0 ? "成功" : "失败");
return true;
}
}
2.3.3.StorageService库存服务
package com.qingzhou.tccstorageservice.service;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
/**
* @author qingzhou
*
* 通过 @LocalTCC 这个注解,RM 初始化的时候会向 TC 注册一个分支事务。
*/
@LocalTCC
public interface StorageService {
/**
* Try: 库存-扣减数量,冻结库存+扣减数量
*
* 定义两阶段提交,在try阶段通过@TwoPhaseBusinessAction注解定义了分支事务的 resourceId,commit和 cancel 方法
* name = 该tcc的bean名称,全局唯一
* commitMethod = commit 为二阶段确认方法
* rollbackMethod = rollback 为二阶段取消方法
* BusinessActionContextParameter注解 传递参数到二阶段中
*
* @param commodityCode 商品编号
* @param count 扣减数量
* @return
*/
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
boolean deduct(@BusinessActionContextParameter(paramName = "commodityCode") String commodityCode,
@BusinessActionContextParameter(paramName = "count") int count);
/**
*
* Confirm: 冻结库存-扣减数量
* 二阶段确认方法可以另命名,但要保证与commitMethod一致
* context可以传递try方法的参数
*
* @param actionContext
* @return
*/
boolean commit(BusinessActionContext actionContext);
/**
* Cancel: 库存+扣减数量,冻结库存-扣减数量
* 二阶段取消方法可以另命名,但要保证与rollbackMethod一致
*
* @param actionContext
* @return
*/
boolean rollback(BusinessActionContext actionContext);
}
package com.qingzhou.tccstorageservice.service.impl;
import com.qingzhou.datasource.entity.Storage;
import com.qingzhou.datasource.mapper.StorageMapper;
import com.qingzhou.tccstorageservice.service.StorageService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author qingzhou
*/
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Autowired
private StorageMapper storageMapper;
@Transactional
@Override
public boolean deduct(String commodityCode, int count){
log.info("=============冻结库存=================");
log.info("当前 XID: {}", RootContext.getXID());
// 检查库存
checkStock(commodityCode,count);
log.info("开始冻结 {} 库存", commodityCode);
//冻结库存
Integer record = storageMapper.freezeStorage(commodityCode,count);
log.info("冻结 {} 库存结果:{}", commodityCode, record > 0 ? "操作成功" : "扣减库存失败");
return true;
}
@Override
public boolean commit(BusinessActionContext actionContext) {
log.info("=============扣减冻结库存=================");
String commodityCode = actionContext.getActionContext("commodityCode").toString();
int count = (int) actionContext.getActionContext("count");
//扣减冻结库存
storageMapper.reduceFreezeStorage(commodityCode,count);
return true;
}
@Override
public boolean rollback(BusinessActionContext actionContext) {
log.info("=============解冻库存=================");
String commodityCode = actionContext.getActionContext("commodityCode").toString();
int count = (int) actionContext.getActionContext("count");
//扣减冻结库存
storageMapper.unfreezeStorage(commodityCode,count);
return true;
}
private void checkStock(String commodityCode, int count){
log.info("检查 {} 库存", commodityCode);
Storage storage = storageMapper.findByCommodityCode(commodityCode);
if (storage.getCount() < count) {
log.warn("{} 库存不足,当前库存:{}", commodityCode, count);
throw new RuntimeException("库存不足");
}
}
}
2.3.4.AccountService账户服务
package com.qingzhou.tccaccountservice.service;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
/**
* @author qingzhou
*
* 通过 @LocalTCC 这个注解,RM 初始化的时候会向 TC 注册一个分支事务。
*/
@LocalTCC
public interface AccountService {
/**
* 用户账户扣款
*
* 定义两阶段提交,在try阶段通过@TwoPhaseBusinessAction注解定义了分支事务的 resourceId,commit和 cancel 方法
* name = 该tcc的bean名称,全局唯一
* commitMethod = commit 为二阶段确认方法
* rollbackMethod = rollback 为二阶段取消方法
*
* @param userId
* @param money 从用户账户中扣除的金额
* @return
*/
@TwoPhaseBusinessAction(name = "debit", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
boolean debit(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money") int money);
/**
* 提交事务,二阶段确认方法可以另命名,但要保证与commitMethod一致
* context可以传递try方法的参数
*
* @param actionContext
* @return
*/
boolean commit(BusinessActionContext actionContext);
/**
* 回滚事务,二阶段取消方法可以另命名,但要保证与rollbackMethod一致
*
* @param actionContext
* @return
*/
boolean rollback(BusinessActionContext actionContext);
}
package com.qingzhou.tccaccountservice.service.impl;
import com.qingzhou.datasource.entity.Account;
import com.qingzhou.datasource.mapper.AccountMapper;
import com.qingzhou.tccaccountservice.service.AccountService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author qingzhou
*/
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
/**
* 扣减用户金额
* @param userId
* @param money
*/
@Transactional
@Override
public boolean debit(String userId, int money){
log.info("=============冻结用户账户余额=================");
log.info("当前 XID: {}", RootContext.getXID());
checkBalance(userId, money);
log.info("开始冻结用户 {} 余额", userId);
//冻结金额
Integer record = accountMapper.freezeBalance(userId,money);
log.info("冻结用户 {} 余额结果:{}", userId, record > 0 ? "操作成功" : "扣减余额失败");
return true;
}
@Override
public boolean commit(BusinessActionContext actionContext) {
log.info("=============扣减冻结金额=================");
String userId = actionContext.getActionContext("userId").toString();
int money = (int) actionContext.getActionContext("money");
//扣减冻结金额
accountMapper.reduceFreezeBalance(userId,money);
return true;
}
@Override
public boolean rollback(BusinessActionContext actionContext) {
log.info("=============解冻金额=================");
String userId = actionContext.getActionContext("userId").toString();
int money = (int) actionContext.getActionContext("money");
//解冻金额
accountMapper.unfreezeBalance(userId,money);
return true;
}
private void checkBalance(String userId, int money){
log.info("检查用户 {} 余额", userId);
Account account = accountMapper.selectByUserId(userId);
if (account.getMoney() < money) {
log.warn("用户 {} 余额不足,当前余额:{}", userId, account.getMoney());
throw new RuntimeException("余额不足");
}
}
}
3.TCC 模式存在的问题
3.1.幂等性问题
3.2.悬挂问题
3.3.空回滚问题
原文始发于微信公众号(轻洲技术):一文搞懂 Seata 分布式事务 TCC 模式及解决空回滚、幂等、悬挂问题
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/165716.html