vlambda博客
学习文章列表

一种简单可落地的分布式事务实践方案,面试官再问起来也不慌了


JAVA前线 


互联网技术人思考与分享,欢迎长按关注


1 案例背景

用户在电商网站购买了一件衣服,在支付成功后,支付系统需要将订单状态改为支付完成。需要注意支付结果和订单结果需要保持一致,不能出现支付成功后,订单状态却修改失败类似情况,否则用户就会很疑惑:明明支付成功了订单却是待支付状态。

对于这种要么同时成功,要么同时失败的场景,最容易想到的是使用事务。我们假 设支付表和订单表属于同一个数据库。

一种简单可落地的分布式事务实践方案,面试官再问起来也不慌了

这种场景代码实现并不难,我们只要利用数据库事务特性,就可以保证两张数据表要么同时成功,要么同时失败:

public class PayServiceImpl implements PayService {
  @Transactional
  public void pay() {
    updatePayInfo();
    updateOrderInfo();
  }
}

但是在分布式场景中可没有这么简单。在分布式场景中订单是由订单团队维护,支付是由支付团队维护,所以订单系统和支付系统根本是两个系统,分别部署在不同服务器,分别提供服务,更不可能使用一个数据库:

一种简单可落地的分布式事务实践方案,面试官再问起来也不慌了

所以在分布式场景中上述代码已经不适用了,我们需要新方案。在谈具体方案之前我们需要讲解一些理论知识。


2 理论知识

2.1 ACID

传统数据事务具有四大特性:原子性,一致性,隔离性,持久性,这些特性首字母组合在一起简称ACID特性。

原子性(Atomicity)
一致性(Consistency)
隔离性(Isolation)
持久性(Durability)

(1) 原子性

一个事务要么全部提交成功,要么全部失败回滚,不能只执行其中一部分操作

(2) 一致性

数据库系统在运行过程中发生故障,有些事务尚未完成就被迫中断,如果这些事务一部分修改已经写入数据库,那么数据库就处在不一致状态,这种情况不能被允许

(3) 隔离性

事务之间相互隔离,一个事务执行不能被其它事务干扰

(4) 持久性

当事务执行成功后,对数据库的修改将会永远保留在数据库。即使数据库出现故障,只要数据库能够重新启动,一定可以恢复到事务成功结束状态 在上述电商系统同一个数据库场景,正是使用了ACID这些特性才能达到我们预期业务要求。但是在分布式场景中就不再适用了,而且在分布式场景中解决分布式事务问题并不容易,这就引出了下一个概念:CAP理论。

2.2 CAP

1998年加州大学计算机科学家Eric Brewer提出分布式系统有三个指标,这三个指标首字母组合在一起称为CAP理论。

一致性(Consistency)
可用性(Availability)
分区容错性(Partition tolerance)

(1) 分区容错性

在分布式系统中不同节点(服务器)一般部署在不同子网络中,每一个子网络被称为一个区,不同子网络在网络通信时可能会失败,我们需要接受这种情况

(2) 可用性

只要收到用户请求,服务器就必须给出响应,用户在访问数据时必须得到及时响应

(3) 一致性

在分布式系统同一个数据可能在不同节点保存多份,当更新操作成功并返回客户端完成后,所有节点(服务器)同一时间数据必须完全一致

一种简单可落地的分布式事务实践方案,面试官再问起来也不慌了

但是CAP理论三个指标无法同时满足,我们需要证明这个命题。我分析了很多证明文章,认为反证法这种方法比较清晰。下面我使用反证法证明这个命题:

假设CAP都满足则一定满足C
假设CAP都满足则一定满足P
因为满足C则节点间数据传递不能有网络故障
第三点与第二点矛盾,证明完毕

在分布式实践中CAP理论有点不够用了,这里我们就要引出下一个概念:BASE理论,这是众多分布式实践的指导理论。

2.3 BASE

BASE理论扩展自CAP理论,核心思想是既然无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当方式来使系统达到最终一致性,这个理论也是我们分布式事务方案的理论基础。BASE由三个短语组成:

基本可用(Basically Available)
柔性状态(Soft State)
最终一致(Eventually Consistency)

(1) 基本可用

当分布式系统发生故障时,允许损失部分系统可用性。例如电商系统在大促无法承受流量洪峰时,可能会将用户引导至降级页面,或者触发服务熔断,保证大部分用户可用

(2) 柔性状态

CAP理论一致性这个指标不允许出现中间状态,所有状态必须保证强一致性。但是柔性状态可以允许短时间节点间不一致状态出现

(3) 最终一致

既然节点间存在短时间不一致状态,那么在一段时间后通过业务手段,节点间状态需要最终达成一致。分布式实践中不追求强一致性,而追求最终一致性


3 事务性消息实践

分布式事务解决方案有很多,例如两阶段提交,TCC,事务性消息。我在分布式事务实践中最常使用事务性消息。首先我们用一个生活实例来描述什么是事务性消息。

小明去面馆吃面条,有很多人在排队。小明付过钱之后就找空位坐了下来,因为空位是随机分布的,那么服务员怎么保证可以把面条准确送到小明的桌上?答案是凭借小明手上的取餐小票。取餐小票是小明购物凭证和消费依据,有了小票就不用担心吃不到面条。取餐小票就是事务性消息。

本章节就把事务性消息这个方案讲透,还是使用订单系统和支付系统这个案例。

一种简单可落地的分布式事务实践方案,面试官再问起来也不慌了

3.1 场景一

假设你是支付团队成员,订单系统由订单团队维护。订单团队愿意配合你们做系统改造,那么可以使用如下事务性消息架构图:

一种简单可落地的分布式事务实践方案,面试官再问起来也不慌了

根据这张图分析事务性消息如何工作:当用户支付成功后,支付系统将支付完成数据保存在数据库,并且在同一个事务中新增一条消息,状态是「待处理」。注意这个消息与支付数据保存具有强一致性,同时成功或者同时失败,我们称这种消息为事务性消息。

当保存成功事务性消息后,发送事务性消息进入消息队列。订单系统通过消息队列订阅到这个消息后,把对应订单状态设置为已支付,同时调用支付系统接口将这条消息设置为「已处理」,整个正向流程就结束了。

但是订单系统调用修改消息接口有可能失败,也就是虽然业务处理成功了,事务性消息状态依然是「待处理」,这时就需要定时补偿器发挥作用了。定时补偿器定时将之前一段时间,状态为「待处理」的消息再次发送至消息队列,由订单系统再次订阅处理,这需要订单系统保证幂等性。

分析到这里事务性消息工作原理已经讲清楚了,但是不能就此止步。我们还要继续分析一个问题:假设订单系统很长时间一直处理不成功,导致消息一直「待处理」怎么办?

出现这种原因可能是订单系统宕机了,那么补偿器一直频繁重试是没有结果的,所以我们要给消息重试一个阶梯时间:第一次不成功过5分钟重试,第二次不成功过15分钟重试,第三次不成功30分钟后重试,这样可以给订单系统恢复时间。

我们还要设置一个最大重试次数,假设重试十次后仍然不成功,那么系统要将消息设置为「已过期」,同时发送告警并进行人工干预。事务性消息场景一般不会像传统事务方式出现异常时发生回滚,而是通过重试继续进行链路,或者进行人工干预。

3.2 场景二

假设你是支付团队成员,订单系统由订单团队维护。订单系统只是通过暴露接口的方式对外进行交互,没有时间配合改造系统为监听消息模式。面对这种场景我们就需要对场景一架构图进行改造,但是核心思想不变。

一种简单可落地的分布式事务实践方案,面试官再问起来也不慌了

我们根据这张图分析事务性消息如何工作:当用户支付成功后,保存支付业务数据和新增消息原理和场景一相同。当保存成功事务性消息后,直接调用处理订单业务接口,调用成功后将这条消息设置为「已处理」,整个正向流程就结束了。

当调用处理订单业务接口失败或者无响应,消息状态仍然为「待处理」。定时补偿器定时将之前一段时间,状态为「待处理」的消息再次调用处理订单业务接口,这需要订单系统保证幂等性。

如果订单系统没有保证幂等性,在再次调用处理订单业务接口时,需要先查询订单接口是否已经处理过,明确返回未处理时才进行调用,否则放弃本次补偿调用,等待再次重试,补偿重试策略与场景一相同。


4 RocketMQ

如果使用RocketMQ消息中间件,可以直接使用RocketMQ事务消息机制,通过发送half消息、提交或回滚half消息、定时业务回查也可以实现最终一致性。



5 文章总结

本文从一个简单场景开始,分析了本地事务和分布式事务的不同,随后我们介绍了基础理论知识ACID、CAP、BASE,为后续分布式事务方案打基础。在分布式事务方案章节讲解了事务性消息方案,事务性消息作为全局事务日志,使得系统拥有全局调度的依据和能力。



JAVA前线 


互联网技术人思考与分享,欢迎长按关注