上一篇文章中讲解到了一些关于分布式事务模型2pc的简单介绍。
2pc协议更多是应用于并发访问量不大的跨库场景中。但是由于其使用了大事务的原理,会在性能层面上造成一定的损失。
下边来介绍一下另外一种技术方案:tcc。
在过去的一周时间内,自己利用了一些碎片时间,大概地实践了下tcc这种设计思路在分布式事务应用中的落地实现。
在讲解tcc之前,我们先来说说分布式环境中的一些必备理论基础内容点:
CAP理论
CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足以下3个属性:
一致性(Consistency) :客户端知道一系列的操作都会同时发生(生效)
可用性(Availability) :每个操作都必须以可预期的响应结束
分区容错性(Partition tolerance) :即使出现单个组件无法可用,操作依然可以完成
具体地讲在分布式系统中,在任何数据库设计中,一个Web应用至多只能同时支持上面
的两个属性。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。
BASE理论
简单来讲,base理论包含了:
baseically available(基本可用)
是指分布式环境中,可以允许一定量的异常,保证整体的可用性为主
soft state(软状态)
软状态是指允许出现一定的中间状态(例如说业务场景中的:订单提交中,库存锁定中,转账锁定中 这些经典状态)
eventual consistency(最终一致性)
部分的业务场景中允许程序运作一段时间之后才达到最终状态。
在基于base理论的基础上,业界逐渐开始提出了tcc方案。
实际案例理解tcc
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
Try 阶段主要是对业务系统做检测及资源预留
Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
下边我通过一个自己实践的案例来介绍一个tcc的实践方案:
假设有一个扣除库存的应用场景:
这种链路在分布式环境下如果执行到一半出现了异常该怎么处理呢?
由于没有了本地事务的保护,所以容易出现部分数据丢失,导致业务数据完整性缺失的情况。假设使用了2pc这种强一致性的方案来进行技术实现,又容易导致过多的大事务在并发量增加的时候出现数据库堵塞。拖垮整个应用系统,导致整条业务链路出现不可用情况。
下边我们将业务场景进行复杂化设计:
1.下单的时候对于库存首先需要做一个锁定的操作,对于预先需要扣减的库存不做实际的扣减操作,而是单独用一张表(这里暂时命名为库存锁定表)来记录扣减的信息(商品id,用户id,库存扣减数目)。
(ps:假设插入锁定表失败,进行适量重试,重试多次依旧失败,则记录日志,整个流程终止)
2.插入订单数据,插入成功之后,订单状态不要立即设置为生效状态,而是用一个初始值“提交状态”来显示。
(ps:假设插入订单表失败,进行适量重试,重试多次依旧失败,则记录日志,整个流程终止)
3.扣减库存:从之前提及到的库存锁定表中提取数据信息,进行扣减操作。
(ps:假设更新库存数据失败,进行适量重试,重试多次依旧失败,则记录日志,整个流程终止)
4.扣减成功之后,再去更新订单的状态为“扣减成功”。
(ps:假设更新订单表数据失败,进行适量重试,重试多次依旧失败,则需要借助之前库存锁定表中记录的库存数值,对商品库存进行回滚,将订单状态设置为“提及失败”,前端弹出“请重新下单”类型的提示,记录日志,整个流程终止)
5.接下来便进行异步的发送优惠券操作。从整体的业务流程来分析,扣减库存,下单才是核心关键点,为了尽可能保证其在分布式环境下事务的一致性,所以重点应该放在扣减库存和下单成功这两个步骤中。
其实整个环境走下来,可以看到其中包含有了tcc的设计思想:
try 尝试阶段:这里面不应该有过多的业务逻辑,而是一种尝试的思想,主要用于校验下一些网络是否通畅,参数是否正常之类的操作。
confirm 阶段:这里的环节需要涉及到关于业务逻辑的部分,例如上述中的锁定库存,生成订单,更新库存,修改订单状态。这些阶段都必须要有一个状态值来告诉调用方,执行结果是否正常,如果正常则执行流程可以继续往下。
cancel阶段:该阶段就是为了避免confirm阶段中的任意环节出现了失败的调用操作,而做的回滚行为,例如上述中的:库存回滚,订单状态调整为”提交失败“。
这里我写了一段简单的逻辑代码让各位读者产生更深刻的影响和理解:
public String doTcc(int userId, int goodsId, int stock) { User user = iUserService.queryByUserId(userId); StockFreeze stockFreeze = new StockFreeze(); stockFreeze.setGoodId(goodsId); stockFreeze.setUserId(user.getUserId()); stockFreeze.setStock(stock); stockFreeze.setValid(CommonConstants.StockFreezeEnum.LOCKING.getCode()); boolean confirmStatus = iTccServiceConfirm.confirmGoods(stockFreeze); //锁定库存 if (confirmStatus) { //插入订单 Order order = buildOrder(userId,goodsId,stock, SUBMIT_ING.getCode()); boolean addOrderSuccess = iOrderService.insertOrder(order); if (!addOrderSuccess) { iTccServiceCancel.rollBackOrderInfo(order.getOrderNo()); log.error("插入订单失败,订单信息为{}",order); return "fail"; } //扣减库存 boolean subtractStockStatusSuccess = iGoodsService.updateStock(stock, goodsId); boolean updateOrderResultSuccess = iOrderService.updateOrderStatus(order.getOrderNo(), SUBMIT_SUCCESS.getCode()); //解除库存锁定表中的锁定状态 (必须更新成功,如果更新失败说明可能之前并没有锁定成功记录到数据库,例如说异步写入到数据库,此时并未实际落入库中) boolean updateStockFreezeSuccess = iStockFreezeService.updateValidStatus(userId, goodsId, CommonConstants.StockFreezeEnum.UN_LOCK.getCode()); log.info("updateOrderResultSuccess is {},subtractStockStatusSuccess is {},updateStockFreezeSuccess is {}",updateOrderResultSuccess,subtractStockStatusSuccess,updateStockFreezeSuccess); if (!updateStockFreezeSuccess || !updateOrderResultSuccess || !subtractStockStatusSuccess) { iTccServiceCancel.rollBackOrderInfo(order.getOrderNo()); iTccServiceCancel.rollBackGoodsStock(stockFreeze); iTccServiceCancel.rollBackStockFreeze(userId,goodsId); return "fail"; } //增加优惠券 boolean addCouponSuccess = iUserService.addCoupon(userId); log.info("addCouponSuccess is {}",addCouponSuccess); return "success"; } log.error("操作异常"); return "fail"; } 复制代码
关于tcc的接口设计如下:
public interface ITccServiceConfirm { /** * 确认商品信息 * * @param stockFreeze * @return */ boolean confirmGoods(StockFreeze stockFreeze); } public interface ITccServiceCancel { /** * 回滚商品的库存 * * @param stockFreeze * @return */ int rollBackGoodsStock(StockFreeze stockFreeze); /** * 回滚错误的订单 * * @param orderNo * @return */ int rollBackOrderInfo(String orderNo); /** * 回滚优惠券 * * @param userId * @return */ int rollBackUserCoupon(int userId); /** * 回滚关于商品锁定状态 * * @param userId * @param goodsId * @return */ int rollBackStockFreeze(int userId,int goodsId); } 复制代码
ps: confirm/cancel逻辑可能会被多次调用,因此,需要保证其幂等性。
可以看出,如果业务程序在实践过程中一旦需要使用tcc的思想来进行实践落地,那么需要做的代码开发量是非常多的。
最后小结
对比2pc和tcc两类技术方案的实践,可以发现,tcc在代码量上边要更加地多。但是在并发量较高的场景中,采用tcc的性能会比用2pc这种技术方案高效一些,假若我们业务场景中遇到的是低访问性,而且要求快速上线的情况下,那么此时采用简单的2pc技术方案即可。
在分布式事务方面,目前市面上较为成熟的技术方案有这么几类:seata,bytetcc,tcc-transaction 等等。目前自己在公司中使用的是seata技术框架,能力有限,对于这块了解还不是特别深入,所以暂时还写不了过多的讲解。