文章目录
- 前言
- 6 Seata TCC 模式
- 6.1 实现原理
- 6.2 优缺点
- 6.3 空回滚和业务悬挂
- 6.3.1 空回滚
- 6.3.2 业务悬挂
- 6.4 微服务实现TCC模式
- 6.4.1 思路分析
- 6.4.2 声明TCC接口
- 6.4.3 编写实现类
- 6.4.4 Controller类调用TCC接口
- 6.4.5 修改配置文件application.yml
- 6.4.6 重启微服务并测试
- 7 TC服务高可用
- 7.1 高可用架构模型
- 7.2 实现TC服务高可用
- 7.2.1 模拟异地容灾
- 7.2.2 将事务组映射配置到Nacos
- 7.2.3 微服务读取Nacos配置
前言
分布式事务学习笔记(一)分布式事务问题、CAP定理、BASE理论、Seata
分布式事务学习笔记(二)Seata架构、TC服务器部署、微服务集成Seata
分布式事务学习笔记(三)微服务实现Seata XA模式
分布式事务学习笔记(四)微服务实现Stata AT模式、Stata Saga模式介绍
6 Seata TCC 模式
6.1 实现原理
TCC 模式是 Seata 支持的一种由业务方细粒度控制的侵入式分布式事务解决方案,其架构如下图:
TCC 模式包含两个阶段,分别是:
- 阶段一(Try):资源检测与预留阶段
- 阶段二(Confirm):预留资源确认阶段
- 阶段二(Cancel):预留资源释放阶段
以一个扣减用户余额的业务为例。假设账户A原本的余额为100,现在需要扣减30。
- 阶段一(Try):检查余额是否充足,如果充足则冻结金额增加30,可用余额扣减30,总数还是100。完成后事务直接提交,无需等待。
- 阶段二(Confirm):如果是确认提交操作,则冻结金额扣减30,可用余额不变,此时就只剩下可用余额70。
- 阶段二(Cancel):如果是回滚操作,则冻结金额扣减30,可用余额增加30,恢复到初始状态。
6.2 优缺点
TCC的优点:
- 一阶段完成后直接提交事务,释放数据库资源,性能好
- 相比AT模型,无需生成快照,无需使用全局锁,性能更强
- 不依赖数据库事务,而是依赖业务补偿操作,可以用于非事务型数据库,且可以灵活选择业务资源的锁定粒度
TCC的缺点:
- 有代码侵入,需要人为编写try、Confirm和Cancel接口,实现复杂
- 有软状态,事务是最终一致
- 需要考虑Confirm和Cancel的失败情况,实现麻烦
6.3 空回滚和业务悬挂
6.3.1 空回滚
当某分支事务的Try阶段阻塞时,可能导致全局事务超时而触发二阶段的Cancel操作。在未执行Try操作时先执行了Cancel操作,这时Cancel就不能做回滚,就是空回滚。
因此,在执行Cancel操作时,应当判断Try是否已经执行,如果尚未执行,则应该空回滚。
6.3.2 业务悬挂
对于已经空回滚的业务,之前被阻塞的Try操作恢复,继续执行Try,但已经永远不可能继续执行Confirm或Cancel操作,事务一直处于中间状态,这就是业务悬挂。
因此,在执行Try操作时,应当判断Cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的Try操作,避免悬挂。
6.4 微服务实现TCC模式
6.4.1 思路分析
要解决空回滚和业务悬挂问题,就必须要记录当前事务状态是在Try还是Cancel阶段。为此,可以在数据库定义一张表来记录:
CREATE TABLE `t_account_freeze` (
`xid` varchar(128) NOT NULL COMMENT '全局事务id',
`user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
`freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
`state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
业务逻辑分析如下:
- Try业务
- 记录冻结金额和事务状态0到 t_account_freeze 表
- 扣减 t_account 表可用余额
- Confirm业务
- 根据xid删除 t_account_freeze 表的冻结记录
- Cancel业务
- 修改 t_account_freeze 表冻结金额为0,state为2
- 修改 t_account 表,恢复可用金额
- 如何判断是否空回滚?
- Cancel业务中,根据xid查询 t_account_freeze,如果为null则说明Try业务还没做,需要空回滚
- 如何避免业务悬挂?
- Try业务中,根据xid查询 t_account_freeze,如果已经存在则证明Cancel业务已经执行,拒绝执行Try业务
6.4.2 声明TCC接口
在jd-account-service
微服务的com.hsgx.account.service
包下新建一个AccountTCCService
接口:
@LocalTCC
public interface AccountTCCService {
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money")int money);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
6.4.3 编写实现类
在jd-account-service
微服务的com.hsgx.account.service.impl
包下新建一个AccountTCCService
接口的实现类:
@Slf4j
@Service
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper accountFreezeMapper;
@Override
@Transactional
public void deduct(String userId, int money) {
// 1.获取事务xid
String xid = RootContext.getXID();
// 2.扣减可用余额
accountMapper.deduct(userId, money);
// 3.记录冻结金额,事务状态
AccountFreeze freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezeMoney(money);
freeze.setState(0);
freeze.setXid(xid);
accountFreezeMapper.insert(freeze);
}
@Override
public boolean confirm(BusinessActionContext ctx) {
// 1.获取事务xid
String xid = ctx.getXid();
// 2.根据xid删除冻结记录
int count = accountFreezeMapper.deleteById(xid);
return count == 1;
}
@Override
public boolean cancel(BusinessActionContext ctx) {
// 1.查询冻结记录
String xid = ctx.getXid();
AccountFreeze freeze = accountFreezeMapper.selectById(xid);
// 2.恢复可用余额
accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
// 3.将冻结金额清零,状态改为CANCEL
freeze.setFreezeMoney(0);
freeze.setState(2);
int count = accountFreezeMapper.updateById(freeze);
return count == 1;
}
}
6.4.4 Controller类调用TCC接口
修改AccountController
类的deduct
方法,改为调用刚刚新建的TCC接口:
@RestController
@RequestMapping("account")
public class AccountController {
@Autowired
private AccountService accountService;
@Autowired
private AccountTCCService accountTCCService;
@PutMapping("/{userId}/{money}")
public ResponseEntity<Void> deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money){
// accountService.deduct(userId, money);
accountTCCService.deduct(userId, money);
return ResponseEntity.noContent().build();
}
}
6.4.5 修改配置文件application.yml
修改微服务下的配置文件application.yml,注释掉分布式事务的模式:
seata:
# data-source-proxy-mode: AT
6.4.6 重启微服务并测试
假设现在商品库存为10,用户余额为1000。用户使用300金额购买了6件商品:
库存和资金充足,下单成功。此时库存剩余4,余额为700。
此时用户使用300金额再次购买了6件商品,由于库存不足,会下单失败:
查看jd-account-service
微服务的日志,可以看到冻结金额被写入 t_account_freeze 表(Try阶段):
随后,事务回滚,先查询 t_account_freeze 表,在恢复余额:
由此可见,TCC模式的分布式事务生效了。
7 TC服务高可用
TC服务作为分布式事务的核心,一定要保证其高可用,因此需要搭建TC服务集群。
7.1 高可用架构模型
搭建TC服务集群,只需要启动多个TC服务,注册到Nacos即可,相对简单。
同时还支持异地容灾,例如一个TC服务集群在上海,另一个TC服务集群在杭州,当其中一个集群故障时自动切换到另外一个集群。
如上图所示,微服务基于事务组(tx-service-group属性)与TC服务集群的映射关系,来查找当前应该使用哪个集群。
7.2 实现TC服务高可用
7.2.1 模拟异地容灾
计划启动两台TC服务器:
节点名称 | IP地址 | 端口号 | 集群名称 |
---|---|---|---|
Seata1 | 127.0.0.1 | 9091 | SH |
Seata2 | 127.0.0.1 | 9092 | HZ |
修改Seata服务的配置文件,启动9091服务:
将Seata服务目录服务一份,修改配置文件,启动9092服务:
此时可以在Nacos控制台看到这两个节点的服务:
进入“详情”可以看到它们分属两个集群:
7.2.2 将事务组映射配置到Nacos
新建一个配置,将tx-service-group与cluster的映射关系配置到Nacos配置中心:
配置的内容如下:
# 事务组映射关系
service.vgroupMapping.default_tx_group=SH
service.enableDegrade=false
service.disableGlobalTransaction=false
# 与TC服务的通信配置
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
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
# RM配置
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=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
# TM配置
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
# undo日志配置
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
client.log.exceptionRate=100
7.2.3 微服务读取Nacos配置
修改每一个微服务的application.yml文件,让微服务读取Nacos中的client.properties文件:
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username:
password:
group: DEFAULT_GROUP
data-id: client.properties
tx-service-group: default_tx_group # 事务组名称
从启动日志可以看到此时连接的TC服务端口是9091,集群是SH:
如果此时上海节点故障了,只需要在Nacos配置中心修改配置:
修改后,微服务自动切换到9092端口:
可见,TC服务集群的高可用已生效。
…
本节完,更多内容请查阅分类专栏:微服务学习笔记
感兴趣的读者还可以查阅我的另外几个专栏:
- SpringBoot源码解读与原理分析
- MyBatis3源码深度解析
- Redis从入门到精通
- MyBatisPlus详解
- SpringCloud学习笔记