写点什么

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

用户头像
JAVA前线
关注
发布于: 1 小时前

欢迎大家关注公众号「JAVA 前线」查看更多精彩分享,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习


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 文章总结

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


欢迎大家关注公众号「JAVA 前线」查看更多精彩分享,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习

发布于: 1 小时前阅读数: 6
用户头像

JAVA前线

关注

公众号「JAVA 前线」 2018.02.06 加入

互联网技术人员思考与分享

评论

发布
暂无评论
一种简单可落地的分布式事务实践方案,面试问起来也不慌了