目录 一、Seata 分布式解决方案 1.1、TCC 模式 1.1.1、TCC 模式理论 对比 TCC 和 AT 模式的一致性和隔离性 TC 的工作模型 1.2.2、TCC 模式优缺点 1.2.3、TCC 模式注意事项:空回滚 1.2.4、
目录
a)TCC 的 try、confirm、cancel 方法都需要在接口中基于注解来声明
c)对应刚刚上述所描述的实现思路,可以基本实现(未考虑空回滚 和 业务悬挂)
TCC 模式和 AT 模式很相似,第一阶段都是独立事务,执行完了直接提交,不同的是 TCC 模式不用去加锁,也不用生成快照,因此性能上会更好.
TCC 模式的第二阶段是基于人工编码的方式来实现数据恢复的,不像 AT 是自动实现的.
人工编码的方式需要实现三个方法,分别是 try、confirm、cancel.
例如现在我的账户余额是 100 元,现在要扣掉 30 元. 如果分成 try、cancel、confirm 这三个阶段.
对比 TCC 和 AT 模式的一致性和隔离性
一致性:首先第一阶段两个模式都是各自提交各自的事务,因此两种模式都有可能出现提交成功和失败的情况,导致状态不一致,需要通过第二阶段来调整. 也就是说这两种模式都是最终一致性.
隔离性:AT 模式是需要通过加锁实现隔离(在第一、第二阶段持有全局锁),而 TCC 模式下不需要加锁隔离,因为在第一阶段是通过冻结来实现隔离(冻结了一部分金额),就算此时有另一个事务也要冻结金额,那就直接从可用余额中取一部分冻结,所以事务之间都没有任何影响,不需要加锁,那么 TCC 模式的性能就要比 AT 模式好很多了.
TC 的工作模型
第一阶段:
这里大部分都和 AT 很像,一开始都是由 TM 去开启全局事务并注册到 TC 上面,然后 TM 去通知每一个分支事务去执行,然后请求被 RM 拦截,RM 就会先去注册分之十五,然后去执行 try 预留资源,执行完后直接提交,随后向 TC 报告事务的状态(资源预留执行成功了?还是失败了).
第二阶段:
TM 通知 TC 事务结束了,那么 TC 就要对事务的状态做判断了. 如果分支预留资源成功了,就直接执行 confirm 提交即可;如果发现其中任意一个有问题,就要执行 cancel 逻辑.
优点:
性能高:第一阶段执行完直接提交事务,并且既不用生成快照,也不用使用全局锁. 可以认为是所有分布式事务模型中性能最好的.
不依赖数据库:不需要依赖于事务性的数据库,因为是靠预留资源来做代偿的. 也就是说不仅可以使用 Mysql 这种关系型数据库,也可以使用 Redis 这种非关系型数据库去实现 TCC 模式.
缺点:
代码侵入高:try、confirm、cancel 这三个方法需要人工编写.
软状态,最终一致:第一阶段执行完后,直接提交事务.
考虑幂等:将来 confirm 和 cancel 可能会执行失败,Seata 看到失败了就会重试,就可能造成死循环. 因此要考虑各种健壮性.
问题:
在将执行某个分支事务的时候,发现执行分支事务的请求因为某种原因(网络抖动)阻塞住了,一旦阻塞的时间超过了超时时间,就会将超时的错误报告给 TC,然后 TC 就会告诉这个分支事务的 RM:“那你去回滚吧”,此时 RM 就会去执行 cancel 的业务.
这就导致本身你没有执行 try 预留资源,现在却要执行 cancel 去释放预留资源. 比方说 try 的业务就是去冻结 30 元的余额,但是在没有进行 try 之前却要进行释放 30 元冻结余额的业务,这不就出事了吗?
解决方案:
因此这里需要做一个空回滚.
在 try 执行请求因为某种原因阻塞时,可能会导致全局事务超时,从而先触发了 cancel 逻辑,此时根本就没有做资源预留,就不能回滚,并且也不能报错(不然 Seata 会以为 cancel 出问题了,会重试,最后导致死循环). 那么空回滚只需要我们返回一个正常结束即可.
问题:
在执行完空回滚之后,try 逻辑的请求阻塞突然通畅,就会去执行资源预留业务,但是资源预留了之后就没有后续了(已经执行过 cancel 中的空回滚了),既没有 cancel,也没有 confirm,业务只执行了一半. 这就是业务悬挂.
比如说我本来有 100 元余额,执行完空回滚后,try逻辑突然通常,冻结了我 30 元的可用余额,然后也没有后续业务了,就导致我这 30 元有是有,但是却一直用不了.
解决办法:
在执行 try 的时候,先判断一下是否回滚过,如果回滚过了 try 就不能执行了. 同样在执行 cancel 的时候,需要判断一下,try 是不是已经执行了,如果 try 没有执行,就去做一个空回滚.
怎么知道 try 到底有没有执行过呢?这就需要在数据库中在创建一个表,用来记录事务的状态(记录上一步是执行了 try 呢?还是cancel?还是confirm?).
那么实现的思路如下:
语法如下:
@LocalTCCpublic interface TCCService { @TwoPhaseBusinessAction(name = "prepare", commitMethod = "confirm", rollbackMethod = "cancel") void prepare(@BusinessActionContextParameter(paramName = "param") String param); boolean confirm (BusinessActionContext context); boolean cancel (BusinessActionContext context);}
根据上述语法,就可以编写用户余额冻结服务的接口 AccountTCCService ,如下
@LocalTCCpublic 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);}
这里我们已经有了用户金额表,如下:
这里我们还需要创建 用户冻结金额表 ,如下:
CREATE TABLE `account_freeze_tbl` ( `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `freeze_money` int(11) UNSIGNED NULL DEFAULT 0, `state` int(1) NULL DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel', PRIMARY KEY (`xid`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;
对应实体类如下:
@Data@TableName("account_freeze_tbl")public class AccountFreeze { @TableId(type = IdType.INPUT) private String xid; private String userId; private Integer freezeMoney; private Integer state; public static abstract class State { public final static int TRY = 0; public final static int CONFIRM = 1; public final static int CANCEL = 2; }}
AccountTCCService 接口,如下:
@Slf4j@Servicepublic class AccountTCCServiceImpl implements AccountTCCService { @Autowired private AccountMapper accountMapper; @Autowired private AccountFreezeMapper freezeMapper; @Override @Transactional public void deduct(String userId, int money) { //1.获取事务 id String xid = RootContext.getXID(); //2.扣减可用余额 accountMapper.deduct(userId, money); //3.增加冻结金额,并记录当前事务的状态 AccountFreeze freeze = new AccountFreeze(); freeze.setUserId(userId); freeze.setXid(xid); freeze.setFreezeMoney(money); freeze.setState(AccountFreeze.State.TRY); freezeMapper.insert(freeze); } @Override public boolean confirm(BusinessActionContext ctx) { //1.添加事务 id String xid = RootContext.getXID(); //2.根据 id 删除冻结记录 int count = freezeMapper.deleteById(xid); return count == 1; } @Override public boolean cancel(BusinessActionContext ctx) { //1.查询冻结记录 String xid = RootContext.getXID(); AccountFreeze freeze = freezeMapper.selectById(xid); //2.恢复可用余额 accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney()); //3.清理冻结余额,状态修改为 cancel freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); int count = freezeMapper.updateById(freeze); return count == 1; }}
考虑在执行 try 逻辑阻塞超时,执行了 cancel 逻辑,那么就需要考虑空回滚. 主要记录 cancel 状态即可.
@Override public boolean cancel(BusinessActionContext ctx) { //1.查询冻结记录 String xid = RootContext.getXID(); AccountFreeze freeze = freezeMapper.selectById(xid); //a. 空回滚判断 if (freeze == null) { //这里主要记录当前的 cancel 状态 freeze = new AccountFreeze(); //这里能拿到 userId 和 money 是因为在 AccountTCCService 接口中通过 BusinessActionContextParameter 注解注册了 String userId = ctx.getActionContext("userId").toString(); freeze.setUserId(userId); freeze.setXid(xid); freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.TRY); freezeMapper.insert(freeze); } //2.恢复可用余额 accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney()); //3.清理冻结余额,状态修改为 cancel freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); int count = freezeMapper.updateById(freeze); return count == 1; }
第一次超时了,进行空回滚(添加 freeze,设置状态为 cancel),第二次又超时了,freeze 不为空,就会进行恢复金额逻辑. 这就出问题了,不能进行恢复金额操作,因此,这里需要进行判断,如果处理过了,直接返回 true 即可.
@Override public boolean cancel(BusinessActionContext ctx) { //1.查询冻结记录 String xid = RootContext.getXID(); AccountFreeze freeze = freezeMapper.selectById(xid); //a. 空回滚判断 if (freeze == null) { //这里主要记录当前的 cancel 状态 freeze = new AccountFreeze(); //这里能拿到 userId 和 money 是因为在 AccountTCCService 接口中通过 BusinessActionContextParameter 注解注册了 String userId = ctx.getActionContext("userId").toString(); freeze.setUserId(userId); freeze.setXid(xid); freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.TRY); freezeMapper.insert(freeze); } //b.幂等问题:第一次超时了,进行空回滚,第二次又超时了,freeze 不为空,就会进行恢复金额逻辑(这就出问题了). if(freeze.getState() == AccountFreeze.State.CANCEL) { //已经处理过依次 cancel 了,无需重复处理 return true; } //2.恢复可用余额 accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney()); //3.清理冻结余额,状态修改为 cancel freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); int count = freezeMapper.updateById(freeze); return count == 1; }
confirm 为什么不考虑幂等了?
因为 confirm 逻辑是删除冻结记录,底层就是 sql 调用 delete. 因此即使操作多次,也无妨.
处理过 cancel 之后,就没必要再处理 try 了,因此这里只需要判断 freeze 是否存在冻结记录,如果有,拒绝即可.
@Override @Transactional public void deduct(String userId, int money) { //1.获取事务 id String xid = RootContext.getXID(); //a. 业务悬挂问题处理:判断 freeze 中是否有冻结记录,如果有,一定是 cancel 执行过,要拒绝业务 AccountFreeze oldFreeze = freezeMapper.selectById(xid); if(oldFreeze != null) { return; } //2.扣减可用余额 accountMapper.deduct(userId, money); //3.增加冻结金额,并记录当前事务的状态 AccountFreeze freeze = new AccountFreeze(); freeze.setUserId(userId); freeze.setXid(xid); freeze.setFreezeMoney(money); freeze.setState(AccountFreeze.State.TRY); freezeMapper.insert(freeze); }
全代码如下:
@Slf4j@Servicepublic class AccountTCCServiceImpl implements AccountTCCService { @Autowired private AccountMapper accountMapper; @Autowired private AccountFreezeMapper freezeMapper; @Override @Transactional public void deduct(String userId, int money) { //1.获取事务 id String xid = RootContext.getXID(); //a. 业务悬挂问题处理:判断 freeze 中是否有冻结记录,如果有,一定是 cancel 执行过,要拒绝业务 AccountFreeze oldFreeze = freezeMapper.selectById(xid); if(oldFreeze != null) { return; } //2.扣减可用余额 accountMapper.deduct(userId, money); //3.增加冻结金额,并记录当前事务的状态 AccountFreeze freeze = new AccountFreeze(); freeze.setUserId(userId); freeze.setXid(xid); freeze.setFreezeMoney(money); freeze.setState(AccountFreeze.State.TRY); freezeMapper.insert(freeze); } @Override public boolean confirm(BusinessActionContext ctx) { //1.添加事务 id String xid = RootContext.getXID(); //2.根据 id 删除冻结记录 int count = freezeMapper.deleteById(xid); return count == 1; } @Override public boolean cancel(BusinessActionContext ctx) { //1.查询冻结记录 String xid = RootContext.getXID(); AccountFreeze freeze = freezeMapper.selectById(xid); //a. 空回滚判断 if (freeze == null) { //这里主要记录当前的 cancel 状态 freeze = new AccountFreeze(); //这里能拿到 userId 和 money 是因为在 AccountTCCService 接口中通过 BusinessActionContextParameter 注解注册了 String userId = ctx.getActionContext("userId").toString(); freeze.setUserId(userId); freeze.setXid(xid); freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.TRY); freezeMapper.insert(freeze); } //b.幂等问题:第一次超时了,进行空回滚,第二次又超时了,freeze 不为空,就会进行恢复金额逻辑(这就出问题了). if(freeze.getState() == AccountFreeze.State.CANCEL) { //已经处理过依次 cancel 了,无需重复处理 return true; } //2.恢复可用余额 accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney()); //3.清理冻结余额,状态修改为 cancel freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); int count = freezeMapper.updateById(freeze); return count == 1; }}
Saga模式是SEATA提供的长事务解决方案。也分为两个阶段:
第一阶段:
与 AT 一样,直接提交本地事务.
第二阶段:
如果第一阶段大家都成功了,就什么也不做.
如果第一阶段有失败的,那么他会反向做一个补偿逻辑去回滚. 这里确实和 tcc 优点像,但不完全一样,因为 tcc 再第一阶段中不是处理事务,只是做资源预留.
比如 扣余额业务,TCC 就直接冻结了,而 saga 是直接把余额扣掉了,如果 saga 第一阶段出现问题,第二阶段就是把扣掉的余额增加回来,实现回滚逻辑的.
缺点:
没有隔离性:因为一二阶段既没有全局锁,也没有预留资源,所有事务与事务之间可能存在脏写问题.
软状态持续时间不确定:saga 模式是按顺序执行每一个事务,如果有任何一个出现问题,就会立刻反向补偿. 因此这个不一致的时间不确定.
优点:
吞吐能力高:基于事件驱动实现异步调用,也就是一个事务完成了,自己执行下一个事务,无需阻塞等待.
性能高:第一阶段无需上锁,性能高.
实现简单:不用像 TCC 那样编写三个阶段,实现简单.
3、补充说明
Ps:由于这种模式的使用场景极少,因此就不演示了.
来源地址:https://blog.csdn.net/CYK_byte/article/details/133580617
--结束END--
本文标题: SpringCloud Alibaba - Seata 四种分布式事务解决方案(TCC、Saga)+ 实践部署(下)
本文链接: https://www.lsjlt.com/news/424489.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
下载Word文档到电脑,方便收藏和打印~
2024-05-30
2024-05-30
2024-05-30
2024-05-30
2024-05-30
2024-05-30
2024-05-30
2024-05-30
2024-05-30
2024-05-30
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
一口价域名售卖能注册吗?域名是网站的标识,简短且易于记忆,为在线用户提供了访问我们网站的简单路径。一口价是在域名交易中一种常见的模式,而这种通常是针对已经被注册的域名转售给其他人的一种方式。
一口价域名买卖的过程通常包括以下几个步骤:
1.寻找:买家需要在域名售卖平台上找到心仪的一口价域名。平台通常会为每个可售的域名提供详细的描述,包括价格、年龄、流
443px" 443px) https://www.west.cn/docs/wp-content/uploads/2024/04/SEO图片294.jpg https://www.west.cn/docs/wp-content/uploads/2024/04/SEO图片294-768x413.jpg 域名售卖 域名一口价售卖 游戏音频 赋值/切片 框架优势 评估指南 项目规模
0