分布式事务

分布式事务产生

数据库能实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持操作同一个数据库中的事务。

当我们的单个数据库的性能产生瓶颈的时候,我们可能会对数据库进行分区,分区之后可能不同的库就处于不同的服务器上了。

大型互联网项目往往是由一系列分布式系统构成的,开发语言平台和技术栈也相对比较杂,尤其是在SOA和微服务架构盛行的今天,一个看起来简单的功能,内部可能需要调用多个“服务”并操作多个数据库或分片来实现,情况往往会复杂很多。

一个业务要跨多个数据库,但是这些操作又需要在一个事务中完成,这种事务即为分布式事务。

分布式事务理论

当出现一个事务要操作多数据库的时候(分布式事务),单个数据库的ACID已经不能适应这种情况了,而在这种ACID的集群环境下,再想保证集群的ACID几乎是很难达到,或者即使能达到那么效率和性能会大幅下降,最为关键的是再很难扩展新的分区了,这个时候如果再追求集群的ACID会导致我们的系统变得很差,这时我们就需要引入一个新的理论原则来适应这种集群的情况,就是CAP原则。

CAP原则是指WEB服务无法同时满足以下3个属性(即最多同时满足2个)

  • C 一致性(Consistency):客户端知道一系列的操作都会同时发生(生效)
  • A 可用性(Availablity):每个操作都必须以可预期的响应结束
  • P 分区容错性(Partition tolerance):即使出现单个组件无法可用,操作依然可以完成

具体地讲,在分布式系统中,在任何数据库设计中,一个Web应用至多只能同时支持上面的两个属性。显然,任何横向扩展从策略都要依赖于数据分区。因此,设计人员必须在一致性和可用性之间做出选择。

在互联网领域的绝大多数的场景,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性,只要这个最终时间是用户可以接受的范围内即可。

BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网分布式系统实践的总结,是基于CAP定律逐步演化而来。其核心思想是即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来势系统达到最终一致性(Eventual consistency)。

  • BA :Basic Available基本可用:整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。只不过“基本可用”和“高可用”的区别是:
    • “一定时间”可以适当延长:比如当举行大促时,响应时间可以适当延长。
    • 给部分用户返回一个降级页面:给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。
  • S :Soft State:柔性状态,同一数据的不同副本的状态,可以不需要实时一致
  • E :Eventual Consistency:最终一致性,同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的

分布式事务协议

XA规范

XA就是X/Open DTP定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。XA接口函数由数据库厂商提供。

在JavaEE平台下,WebLogic、Webshare等主流商用的应用服务器提供了JTA的实现和支持。而在Tomcat下是没有实现的(现在开发部署基本使用Tomcat,特别是spring boot默认是使用Tomcat作为服务器),这就需要借助第三方的框架Jotm、Automikos等来实现,两者均支持spring事务整合。

二阶段提交协议2PC

三阶段提交协议3PC

分布式事务解决方案

  • 本地消息表(异步确保)- 最终一致性
  • TCC - 最终一致性

TCC(补偿事务)

TCC其实就是采用补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

  • Try阶段主要对业务系统做检测及资源预留
  • Confirm阶段主要是对业务系统做确认提交,try阶段执行成功并开始执行confirm阶段时,默认confirm阶段是不会出错的。即:只要try成功,confirm一定成功
  • Cancel阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

举个例子,假如A要向B转账,思路大概是:

我们有一个本地方法,里面依次调用:

  1. 首先在Try阶段,要先调用远程接口把A和B的钱给冻结起来
  2. 在Confirm阶段,执行远程调用的转账操作,转账成功进行解冻
  3. 如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法(Cancel)

优点:跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些

缺点:在2,3步中都有可能失败,TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。

TCC-Transaction

tcc-transaction不和底层使用的rpc框架耦合,也就是使用doubbo,thrift,web,service,http等都可

举例子来演示在下完订单后,使用红包账户和资金账户来付款,红包账户服务和资金账户服务在不同的系统中。演示系统中,有两个SOA提供方,一个是CapitalTradeOrderService,代表资金账户服务,另一个是RedPacketTradeService,代表红包账户服务。

具体流程:

  1. 下完订单后,订单状态为DRAFT,在TCC事务中TRY阶段,订单支付服务将订单状态变成PAYING,同时远程调用红包账户和资金账户服务,将付款方的余额减掉(预留业务资源);
  2. 如果在TRY阶段,任何一个服务失败,tcc-transaction将自动调用这些服务对应的cancel方法,订单支付服务将订单状态变成PAY_FAILD,同时远程调用红包账户和资金账户服务,将付款方余额减掉的部分增加回去;
  3. 如果TRY阶段正常完成,则进入CONFIRM阶段,在CONFIRM阶段(tcc-transaction自动调用),订单支付服务将订单状态变成CONFIRMED,同时远程调用红包账户服务和资金账户服务对应的CONFIRM方法,将收款方的余额增加。

OrderController代码:

public RedirectView placeOrder(@RequestParam String redPacketPayAmount,
@RequestParam long shopId,
@RequestParam long payerUserId,
@RequestParam String productId) {
PlaceOrderRequest request = buildRequest(redPacketPayAmount, shopId, payerUserId, productId);
//下单并支付订单
String merchantOrderNo = placeOrderService.placeOrder(request.getPayerUserId(), request.getShopId(), request.getProductQuantities(), request.getRedPacketPayAmount());
return new RedirectView("payResult/" + merchantOrderNo);
}

PlaceOrderService代码:

public String placeOrder(long payerUserId, long shopId, List<Pair<Long, Integer>> productQuantities, BigDecimal redPacketPayAmount) {
//调用 ShopRepository#findById(...)方法,查询商店
Shop shop = shopRepository.findById(shopId);
// 调用OrderService#createOrder(...)方法,创建订单状态为"DRAFT"的商城订单
Order order = orderService.createOrder(payerUserId, shop.getOwnerUserId(), productQuantities);

Boolean result = false;

try {
// 调用paymentService#makePayment(...)方法,发起支付, TCC流程
paymentService.makePayment(order, redPacketPayAmount, order.getTotalAmount.subtract(redPacketPayAmount));
} catch (ConfirmingException confirmingException) {
// exception throws with the tcc transaction status is CONFIRMING
// when tcc transaction is confirming status, the tcc transaction recovery will try to confirm the whole transaction to ensure eventually consistent
} catch (CancellingException cancellingException) {
// exception throws with the tcc transaction status is CANCELLING
// when tcc transaction is under CANCELLING status, the tcc transaction recovery will try to cancel the whole transaction to ensure eventually consistent
} catch (Throwable e) {
// other exception throws at TRYING stage,you can retry or cancel the operation
}
}

PaymentService代码:

@Compensable(confirmMethod = "confirmMakePayment", calcelMethod = "cancelMakePayment", asyncConfirm = true)
@Transactional
public void makePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
if (order.getStatus().equals("DRAFT")) {
// 更新订单状态为支付中
order.pay(redPacketPayAmount, capitalPayAmount);
try{
orderRepository.updateOrder(order);
} catch (OptimisticLockingFailureException e){

}
}
}

public void confirmMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
try{
Thread.sleep(1000L);
} catch (InterruptedException e){
throw new RuntimeException();
}
Order foundOrder = orderRepository.findByMerchantOrderNo(order.getMerchantOrderNo());
if (foundOrder != null && foundOrder.getStatus().equals("PAYING")) {
order.confirm();
orderRepository.updateOrder(order);
}
}

public void cancelMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
try{
Thread.sleep(1000L);
} catch (InterruptedException e){
throw new RuntimeException();
}
Order foundOrder = orderRepository.findByMerchantOrderNo(order.getMerchantOrderNo());
if (foundOrder != null && foundOrder.getStatus().equals("PAYING")) {
order.cancelPayment();
orderRepository.updateOrder(order);
}
}
posted @ 2019-04-01 21:16  见习小学生  阅读(180)  评论(0)    收藏  举报