相比于上篇文章所聊的 AT 模式,TCC(Try-Confirm-Cancel) 模式就带一点手动的感觉了,它也是两阶段提交的演化,但是和 AT 又不太一样,我们来看下流程。
专注于为中小企业提供做网站、成都网站设计服务,电脑端+手机端+微信端的三站合一,更高效的管理,为中小企业和布克赛尔蒙古免费做网站提供优质的服务。我们立足成都,凝聚了一批互联网行业人才,有力地推动了千余家企业的稳健成长,帮助中小企业通过网站建设实现规模扩充和转变。
官网上有一张 TCC 的流程图,我们来看下:
可以看到,TCC 也是分为两阶段:
第一阶段是 prepare,在这个阶段主要是做资源的检测和预留工作,例如银行转账,这个阶段就先去检查下用户的钱够不够,不够就直接抛异常,够就先给冻结上。
第二阶段是 commit 或 rollback,这个主要是等各个分支事务的一阶段都执行完毕,都执行完毕后各自将自己的情况报告给 TC,TC 一统计,发现各个分支事务都没有异常,那么就通知大家一起提交;如果 TC 发现有分支事务发生异常了,那么就通知大家回滚。
那么小伙伴可能也发现了,上面这个流程中,一共涉及到了三个方法,prepare、commit 以及 rollback,这三个方法都完全是用户自定义的方法,都是需要我们自己来实现的,所以我一开始就说 TCC 是一种手动的模式。
和 AT 相比,大家发现 TCC 这种模式其实是不依赖于底层数据库的事务支持的,也就是说,哪怕你底层数据库不支持事务也没关系,反正 prepare、commit 以及 rollback 三个方法都是开发者自己写的,我们自己将这三个方法对应的流程捋顺就行了。
在上篇文章的中,我们讲 AT 模式,每个数据库都需要有一个 undo log 表,这个表用来记录一条数据更改之前和更改之后的状态(前镜像和后镜像),如果所有分支事务最终都提交成功,那么记录在 undo log 表中的数据就会自动删除;如果有一个分支事务执行失败,导致所有事务都需要回滚,那么就会以 undo log 表中的数据会依据,生成反向补偿语句,利用反向补偿语句将数据复原,执行完成后也会删除 undo log 表中的记录。
在这个流程中,大家看到,undo log 表扮演了非常重要的角色。TCC 和 AT 最大的区别在于,TCC 中的提交和回滚逻辑都是开发者自己写的,而 AT 都是框架自动完成的。
为了方便大家理解,本文我就不重新搞案例了,咱们还用上篇文章那个下订单的案例来演示。
这是一个商品下单的案例,一共有五个服务,我来和大家稍微解释下:
这个案例讲了一个什么事呢?
当用户想要下单的时候,调用了 bussiness 中的接口,bussiness 中的接口又调用了它自己的 service,在 service 中,首先开启了全局分布式事务,然后通过 feign 调用 storage 中的接口去扣库存,然后再通过 feign 调用 order 中的接口去创建订单(order 在创建订单的时候,不仅会创建订单,还会扣除用户账户的余额),在这个过程中,如果有任何一个环节出错了(余额不足、库存不足等导致的问题),就会触发整体的事务回滚。
本案例具体架构如下图:
这个案例就是一个典型的分布式事务问题,storage、order 以及 account 中的事务分属于不同的微服务,但是我们希望他们同时成功或者同时失败。
这个案例的基本架构我这里就不重复搭建了,小伙伴们可以参考上篇文章,这里我们主要来看 TCC 事务如何添加进来。
首先我们将上篇文章中的数据库来重新设计一下,方便我们本文的使用。
账户表增加一个冻结金额的字段,如下:
订单表和前文保持一致,不变。
库存表也增加一个冻结库存数量的字段,如下:
另外,由于我们这里不再使用 AT 模式,所以可以删除之前的 undo_log 表了(可能有小伙伴删除 undo_log 表之后,会报错,那是因为你 TCC 模式使用不对,注意看松哥后面的讲解哦)。
相关的数据库脚本小伙伴们可以在文末下载,这里我就不列出来了。
在 TCC 模式中,我们的 Feign 换一种方式来配置。
小伙伴们都知道,在上篇文章的案例中,我们有一个 common 模块,用来存放一些公共内容(实际上我们只是存储了 RespBean),现在我们把这里涉及到的 OpenFeign 接口也存储进来,一共是三个 OpenFeign 接口,因为还要用到 seata 中的注解,所以我们在 common 中引入 OpenFeign 和 seata 的依赖,如下:
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
2.2.2.RELEASE
org.springframework.cloud
spring-cloud-starter-openfeign
2.2.6.RELEASE
然后在这里定义 OpenFeign 的三个接口,如下:
@LocalTCC
public interface AccountServiceApi {
@PostMapping("/account/deduct/prepare")
@TwoPhaseBusinessAction(name = "accountServiceApi", commitMethod = "commit", rollbackMethod = "rollback")
boolean prepare(@RequestBody BusinessActionContext actionContext, @RequestParam("userId") @BusinessActionContextParameter(paramName = "userId") String userId, @RequestParam("money") @BusinessActionContextParameter(paramName = "money") Double money);
@RequestMapping("/account/deduct/commit")
boolean commit(@RequestBody BusinessActionContext actionContext);
@RequestMapping("/account/deduct/rollback")
boolean rollback(@RequestBody BusinessActionContext actionContext);
}
@LocalTCC
public interface OrderServiceApi {
@PostMapping("/order/create/prepare")
@TwoPhaseBusinessAction(name = "orderServiceApi", commitMethod = "commit", rollbackMethod = "rollback")
boolean prepare(@RequestBody BusinessActionContext actionContext, @RequestParam("userId") @BusinessActionContextParameter(paramName = "userId") String userId, @RequestParam("productId") @BusinessActionContextParameter(paramName = "productId") String productId, @RequestParam("count") @BusinessActionContextParameter(paramName = "count") Integer count);
@RequestMapping("/order/create/commit")
boolean commit(@RequestBody BusinessActionContext actionContext);
@RequestMapping("/order/create/rollback")
boolean rollback(@RequestBody BusinessActionContext actionContext);
}
@LocalTCC
public interface StorageServiceApi {
@PostMapping("/storage/deduct/prepare")
@TwoPhaseBusinessAction(name = "storageServiceApi",commitMethod = "commit",rollbackMethod = "rollback")
boolean deduct(@RequestBody BusinessActionContext actionContext, @RequestParam("productId")@BusinessActionContextParameter(paramName = "productId") String productId, @RequestParam("count") @BusinessActionContextParameter(paramName = "count") Integer count);
@RequestMapping("/storage/deduct/commit")
boolean commit(@RequestBody BusinessActionContext actionContext);
@RequestMapping("/storage/deduct/rollback")
boolean rollback(@RequestBody BusinessActionContext actionContext);
}
这里一共有三个接口,但是只要大家搞懂其中一个,另外两个都很好懂了。我这里就以 AccountServiceApi 为例来和大家讲解吧。
这是 AccountServiceApi,另外两个接口的设计也是大同小异,这里我就不再赘述。
接下来看接口的实现。
首先我们来看看 Account 服务。AccountController 实现 AccountServiceApi。
我们来看下 AccountController 的定义:
@RestController
public class AccountController implements AccountServiceApi {
@Autowired
AccountService accountService;
@Override
public boolean prepare(BusinessActionContext actionContext, String userId, Double money) {
return accountService.prepareDeduct(userId, money);
}
@Override
public boolean commit(BusinessActionContext actionContext) {
return accountService.commitDeduct(actionContext);
}
@Override
public boolean rollback(BusinessActionContext actionContext) {
return accountService.rollbackDeduct(actionContext);
}
}
因为接口的路径都定义在 AccountServiceApi 中了,所以这里只需要简单实现即可,核心的处理逻辑在 AccountService 中,我们来看下 AccountService:
@Service
public class AccountService {
private static final Logger logger = LoggerFactory.getLogger(AccountService.class);
@Autowired
AccountMapper accountMapper;
/**
* 预扣款阶段
* 检查账户余额
*
* @param userId
* @param money
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean prepareDeduct(String userId, Double money) {
Account account = accountMapper.getAccountByUserId(userId);
if (account == null) {
throw new RuntimeException("账户不存在");
}
if (account.getMoney() < money) {
throw new RuntimeException("余额不足,预扣款失败");
}
account.setFreezeMoney(account.getFreezeMoney() + money);
account.setMoney(account.getMoney() - money);
Integer i = accountMapper.updateAccount(account);
logger.info("{} 账户预扣款 {} 元", userId, money);
return i == 1;
}
/**
* 实际扣款阶段
*
* @param actionContext
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean commitDeduct(BusinessActionContext actionContext) {
String userId = (String) actionContext.getActionContext("userId");
Double money = ((BigDecimal) actionContext.getActionContext("money")).doubleValue();
Account account = accountMapper.getAccountByUserId(userId);
if (account.getFreezeMoney() < money) {
throw new RuntimeException("余额不足,扣款失败");
}
account.setFreezeMoney(account.getFreezeMoney() - money);
Integer i = accountMapper.updateAccount(account);
logger.info("{} 账户扣款 {} 元", userId, money);
return i == 1;
}
/**
* 账户回滚阶段
*
* @param actionContext
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean rollbackDeduct(BusinessActionContext actionContext) {
String userId = (String) actionContext.getActionContext("userId");
Double money = ((BigDecimal) actionContext.getActionContext("money")).doubleValue();
Account account = accountMapper.getAccountByUserId(userId);
if (account.getFreezeMoney() >= money) {
account.setMoney(account.getMoney() + money);
account.setFreezeMoney(account.getFreezeMoney() - money);
Integer i = accountMapper.updateAccount(account);
logger.info("{} 账户释放冻结金额 {} 元", userId, money);
return i == 1;
}
logger.info("{} 账户资金已释放",userId);
//说明prepare中抛出异常,未冻结资金
return true;
}
}
好了,这就是从账户扣钱的两阶段操作,数据库操作比较简单,我这里就不列出来了,文末可以下载源码。
再来看订单服务。
由于我们是在 order 中调用 account 完成账户扣款的,所以需要先在 order 中加入 account 的 OpenFeign 调用,如下:
@FeignClient("account")
public interface AccountServiceApiImpl extends AccountServiceApi {
}
这应该没啥好解释的。
接下来我们来看 OrderController:
@RestController
public class OrderController implements OrderServiceApi {
@Autowired
OrderService orderService;
@Override
public boolean prepare(BusinessActionContext actionContext, String userId, String productId, Integer count) {
return orderService.prepareCreateOrder(actionContext,userId, productId, count);
}
@Override
public boolean commit(BusinessActionContext actionContext) {
return orderService.commitOrder(actionContext);
}
@Override
public boolean rollback(BusinessActionContext actionContext) {
return orderService.rollbackOrder(actionContext);
}
}
这个跟 AccountService 也基本一致,实现了 OrderServiceApi 接口,接口地址啥的都定义在 OrderServiceApi 中,这个类重点还是在 OrderService 中,如下:
@Service
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
@Autowired
AccountServiceApi accountServiceApi;
@Autowired
OrderMapper orderMapper;
@Transactional(rollbackFor = Exception.class)
public boolean prepareCreateOrder(BusinessActionContext actionContext, String userId, String productId, Integer count) {
//先去扣款,假设每个产品100块钱
boolean resp = accountServiceApi.prepare(actionContext, userId, count * 100.0);
logger.info("{} 用户购买的 {} 商品共计 {} 件,预下单成功", userId, productId, count);
return resp;
}
@Transactional(rollbackFor = Exception.class)
public boolean commitOrder(BusinessActionContext actionContext) {
String userId = (String) actionContext.getActionContext("userId");
String productId = (String) actionContext.getActionContext("productId");
Integer count = (Integer) actionContext.getActionContext("count");
int i = orderMapper.addOrder(userId, productId, count, count * 100.0);
logger.info("{} 用户购买的 {} 商品共计 {} 件,下单成功", userId, productId, count);
return i==1;
}
@Transactional(rollbackFor = Exception.class)
public boolean rollbackOrder(BusinessActionContext actionContext) {
String userId = (String) actionContext.getActionContext("userId");
String productId = (String) actionContext.getActionContext("productId");
Integer count = (Integer) actionContext.getActionContext("count");
logger.info("{} 用户购买的 {} 商品共计 {} 件,订单回滚成功", userId, productId, count);
return true;
}
}
跟之前的 AccountService 一样,这里也是三个核心方法:
好了,这就是下单的操作。
最后我们再来看看扣库存的操作,这个跟扣款比较像,一起来看下:
@RestController
public class StorageController implements StorageServiceApi {
@Autowired
StorageService storageService;
@Override
public boolean deduct(BusinessActionContext actionContext, String productId, Integer count) {
return storageService.prepareDeduct(productId, count);
}
@Override
public boolean commit(BusinessActionContext actionContext) {
return storageService.commitDeduct(actionContext);
}
@Override
public boolean rollback(BusinessActionContext actionContext) {
return storageService.rollbackDeduct(actionContext);
}
}
核心逻辑在 StorageService 中,如下:
@Service
public class StorageService {
private static final Logger logger = LoggerFactory.getLogger(StorageService.class);
@Autowired
StorageMapper storageMapper;
/**
* 预扣库存
*
* @param productId
* @param count
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean prepareDeduct(String productId, Integer count) {
Storage storage = storageMapper.getStorageByProductId(productId);
if (storage == null) {
throw new RuntimeException("商品不存在");
}
if (storage.getCount() < count) {
throw new RuntimeException("库存不足,预扣库存失败");
}
storage.setFreezeCount(storage.getFreezeCount() + count);
storage.setCount(storage.getCount() - count);
int i = storageMapper.updateStorage(storage);
logger.info("{} 商品库存冻结 {} 个", productId, count);
return i == 1;
}
/**
* 扣库存
*
* @param actionContext
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean commitDeduct(BusinessActionContext actionContext) {
String productId = (String) actionContext.getActionContext("productId");
Integer count = (Integer) actionContext.getActionContext("count");
Storage storage = storageMapper.getStorageByProductId(productId);
if (storage.getFreezeCount() < count) {
throw new RuntimeException("库存不足,扣库存失败");
}
storage.setFreezeCount(storage.getFreezeCount() - count);
int i = storageMapper.updateStorage(storage);
logger.info("{} 商品库存扣除 {} 个", productId, count);
return i == 1;
}
@Transactional(rollbackFor = Exception.class)
public boolean rollbackDeduct(BusinessActionContext actionContext) {
String productId = (String) actionContext.getActionContext("productId");
Integer count <网站题目:听说TCC不支持OpenFeign?这个坑松哥必须给大家填了!
标题网址:http://www.shufengxianlan.com/qtweb/news8/60808.html网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联