“分布式事务”,此次完全懂了!

在分布式、微服务大行其道的今天,相信你们对这些名词都不会陌生。而说到使用分布式,或者拆分微服务的好处,你确定能想到一大堆。数据库


image.png

图片来自包图网安全


好比每一个人只须要维护本身单独的服务,没有了之前的各类代码冲突。本身想测试、想发布、想升级,只须要 Care 本身写的代码就 OK 了,很方便很贴心!网络


然而事物都有两面性,它同时也会带来一些问题,今天的文章谈的就是分布式系统架构带来的其中一个棘手的问题:分布式事务!架构


什么是事务?分布式


首先抛出来一个问题:什么是事务?有人会说事务就是一系列操做,要么同时成功,要么同时失败;而后会从事务的 ACID 特性(原子性、一致性、隔离性、持久性)展开叙述。ide


确实如此,事务就是为了保证一系列操做能够正常执行,它必须同时知足 ACID 特性。微服务


可是今天咱们换个角度思考下,咱们不只要知道 What(好比什么是事务),更要知道事务的 Why(好比为何会有事务这个概念?事务是为了解决什么问题)。测试


有时候,换个角度说不定有不同的收获。spa


换个角度看事务代理


就像经典的文学做品均来自于生活,却又高于生活,事务的概念一样来自于生活,引入“事务”确定是为了解决某种问题,否则,谁又愿意干这么无聊的事情呢?


最简单最经典的例子:银行转帐,咱们要从 A 帐户转 1000 块到 B 帐户。


正常状况下若是从 A 转出 1000 到 B 帐户以后,A 帐户余额减 1000(这个操做咱们用 Action1 表明),B 帐户余额加 1000(这个操做咱们用 Action2 表明)


首先咱们要明确一点,Action1 和 Action2 是两个操做。既然是两个操做那么就必定会存在执行的前后顺序。


那么就可能会出现 Action1 执行完刚准备去执行 Action2 的时候出问题了(好比数据库负载过大暂时拒绝访问)。


类比到咱们生活中,那就是我给朋友转了 1000 块钱,而后我卡里的余额少了 1000,可是我朋友却没有收到钱。


为解决这种“Money 去哪儿了”的问题,引入了“事务”的概念。也就是说,既然我转帐的时候你保证不了 100% 能成功,好比银行系统只能保证 99.99% 的高可用,那么在那 0.01% 的时间里若是出现了上述问题,银行系统直接回滚 Action1 操做?(即把 1000 块钱再加回余额中去)


对于银行系统来讲,可能在 0.01% 的时间里我保证不了 Action1 和 Action2 同时成功,那么在出问题的时候,我保证它俩同时失败。(事务的原子性)


经过这个例子,就已经回答了刚开始提出的 2 个问题(为何会有事务?事务是为了解决什么问题?)


总结一下:事务就是经过它的 ACID 特性,保证一系列的操做在任何状况下均可以安全正确的执行。


Java 中的事务


搞清楚了事务以后,咱们来看点眼熟的,Java 中的事务是怎么玩的?


Java 中咱们平时用的最多的就是在 Service 层的增删改方法上添加 @Transactional 注解,让 Spring 去帮咱们管理事务。


它底层会给咱们的 Service 组件生成一个对应的 Proxy 动态代理,这样全部对 Service 组件的方法都由它对应的 Proxy 来接管。


当 Proxy 在调用对应业务方法好比 add() 时,Proxy 就会基于 AOP 的思想在调用真正的业务方法前执行 setAutoCommit(false)打开事务。


而后在业务方法执行完后执行 Commit 提交事务,当在执行业务方法的过程当中发生异常时就会执行 Rollback 来回滚事务。


固然 @Transactional 注解具体的实现细节这里再也不展开,这个不是本篇文章的重点,本文的 Topic 是“分布式事务”,关于 @Transactional 注解你们有兴趣的话,能够本身打断点 Debug 源码研究下,源码出真知。


啥又是分布式事务?


铺垫了辣么久,终于到了本篇的第一个重点!首先你们想过没:既然有了事务,而且使用 Spring 的 @Transactional 注解来控制事务是如此的方便,那为啥还要搞一个分布式事务的概念出来啊?


更进一步,分布式事务和普通事务究竟是啥关系?有什么区别?分布式事务又是为了解决什么问题出现的?


各类疑问接踵而至,别着急,带着这些思考,我们接下来就详细聊聊分布式事务。


既然叫分布式事务,那么必然和分布式有点关系啦!简单来讲,分布式事务指的就是分布式系统中的事务。


好,那我们继续,首先来看看下面的图:

image.png

如上图所示,一个单块系统有 3 个模块:员工模块、财务模块和请假模块。咱们如今有一个操做须要按顺序去调用完成这 3 个模块中的接口。


这个操做是一个总体,包含在一个事务中,要么同时成功要么同时失败回滚。不成功便成仁,这个都没有问题。


可是当咱们把单块系统拆分红分布式系统或者微服务架构,事务就不是上面那么玩儿了。


首先咱们来看看拆分红分布式系统以后的架构图,以下所示:

image.png

上图是同一个操做在分布式系统中的执行状况。员工模块、财务模块和请假模块分别给拆分红员工系统、财务系统和请假系统。


好比一个用户进行一个操做,这个操做须要先调用员工系统预先处理一下,而后经过 HTTP 或者 RPC 的方式分别调用财务系统和请假系统的接口作进一步的处理,它们的操做都须要分别落地到数据库中。


这 3 个系统的一系列操做实际上是须要所有被包裹在同一个分布式事务中的,此时这 3 个系统的操做,要么同时成功要么同时失败。


分布式系统中完成一个操做一般须要多个系统间协同调用和通讯,好比上面的例子。


三个子系统:员工系统、财务系统、请假系统之间就经过 HTTP 或者 RPC 进行通讯,而再也不是一个单块系统中不一样模块之间的调用,这就是分布式系统和单块系统最大的区别。


一些平时不太关注分布式架构的同窗,看到这里可能会说:我直接用 Spring 的 @Transactional 注解就 OK 了啊,管那么多干吗!


可是这里极其重要的一点:单块系统是运行在同一个 JVM 进程中的,可是分布式系统中的各个系统运行在各自的 JVM 进程中。


所以你直接加 @Transactional 注解是不行的,由于它只能控制同一个 JVM 进程中的事务,可是对于这种跨多个 JVM 进程的事务无能无力。


分布式事务的几种实现思路


搞清楚了啥是分布式事务,那么分布式事务究竟是怎么玩儿的呢?下边就来给你们介绍几种分布式事务的实现方案。


可靠消息最终一致性方案


整个流程图以下所示:

image.png

咱们来解释一下这个方案的大概流程:

  • A 系统先发送一个 Prepared 消息到 MQ,若是这个 Prepared 消息发送失败那么就直接取消操做别执行了,后续操做都再也不执行。

  • 若是这个消息发送成功了,那么接着执行 A 系统的本地事务,若是执行失败就告诉 MQ 回滚消息,后续操做都再也不执行。

  • 若是 A 系统本地事务执行成功,就告诉 MQ 发送确认消息。

  • 那若是 A 系统迟迟不发送确认消息呢?此时 MQ 会自动定时轮询全部 Prepared 消息,而后调用 A 系统事先提供的接口,经过这个接口反查 A 系统的上次本地事务是否执行成功。

    若是成功,就发送确认消息给 MQ;失败则告诉 MQ 回滚消息。(后续操做都再也不执行)

  • 此时 B 系统会接收到确认消息,而后执行本地的事务,若是本地事务执行成功则事务正常完成。

  • 若是系统 B 的本地事务执行失败了咋办?基于 MQ 重试咯,MQ 会自动不断重试直到成功,若是实在是不行,能够发送报警由人工来手工回滚和补偿。


这种方案的要点就是能够基于 MQ 来进行不断重试,最终必定会执行成功的。


由于通常执行失败的缘由是网络抖动或者数据库瞬间负载过高,都是暂时性问题。


经过这种方案,99.9% 的状况都是能够保证数据最终一致性的,剩下的 0.1% 出问题的时候,就人工修复数据呗。


适用场景:这个方案的使用仍是比较广,目前国内互联网公司大都是基于这种思路玩儿的。


最大努力通知方案


整个流程图以下所示:

image.png

这个方案的大体流程:

  • 系统 A 本地事务执行完以后,发送个消息到 MQ。

  • 这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ,而后写入数据库中记录下来,或者是放入个内存队列。接着调用系统 B 的接口。

  • 假如系统 B 执行成功就万事 OK 了,可是若是系统 B 执行失败了呢?

  • 那么此时最大努力通知服务就定时尝试从新调用系统 B,反复 N 次,最后仍是不行就放弃。


这套方案和上面的可靠消息最终一致性方案的区别:可靠消息最终一致性方案能够保证的是只要系统 A 的事务完成,经过不停(无限次)重试来保证系统 B 的事务总会完成。


可是最大努力方案就不一样,若是系统 B 本地事务执行失败了,那么它会重试 N 次后就再也不重试,系统 B 的本地事务可能就不会完成了。至于你想控制它究竟有“多努力”,这个须要结合本身的业务来配置。


好比对于电商系统,在下完订单后发短信通知用户下单成功的业务场景中,下单正常完成,可是到了发短信的这个环节因为短信服务暂时有点问题,致使重试了 3 次仍是失败。


那么此时就再也不尝试发送短信,由于在这个场景中咱们认为 3 次就已经算是尽了“最大努力”了。


简单总结:就是在指定的重试次数内,若是能执行成功那么皆大欢喜,若是超过了最大重试次数就放弃,再也不进行重试。


适用场景:通常用在不过重要的业务操做中,就是那种完成的话是锦上添花,但失败的话对我也没有什么坏影响的场景。


好比上边提到的电商中的部分通知短信,就比较适合使用这种最大努力通知方案来作分布式事务的保证。


TCC 强一致性方案


TCC的 全称是

  • Try(尝试)

  • Confirm(确认/提交)

  • Cancel(回滚)


这个实际上是用到了补偿的概念,分为了三个阶段:

  • Try 阶段:这个阶段说的是对各个服务的资源作检测以及对资源进行锁定或者预留。

  • Confirm 阶段:这个阶段说的是在各个服务中执行实际的操做。

  • Cancel 阶段:若是任何一个服务的业务方法执行出错,那么这里就须要进行补偿,就是执行已经执行成功的业务逻辑的回滚操做。


仍是给你们举个例子:

image.png

好比跨银行转帐的时候,要涉及到两个银行的分布式事务,若是用 TCC 方案来实现,思路是这样的:

  • Try 阶段:先把两个银行帐户中的资金给它冻结住就不让操做了。

  • Confirm 阶段:执行实际的转帐操做,A 银行帐户的资金扣减,B 银行帐户的资金增长。

  • Cancel 阶段:若是任何一个银行的操做执行失败,那么就须要回滚进行补偿,就是好比 A 银行帐户若是已经扣减了,可是 B 银行帐户资金增长失败了,那么就得把 A 银行帐户资金给加回去。


适用场景:这种方案说实话几乎不多有人使用,咱们用的也比较少,可是也有使用的场景。


由于这个事务回滚其实是严重依赖于你本身写代码来回滚和补偿了,会形成补偿代码巨大,很是之恶心。


好比说咱们,通常来讲跟钱相关的,跟钱打交道的,支付、交易相关的场景,咱们会用 TCC,严格保证分布式事务要么所有成功,要么所有自动回滚,严格保证资金的正确性,在资金上不容许出现问题。


比较适合的场景:除非你是真的一致性要求过高,是你系统中核心之核心的场景,好比常见的就是资金类的场景,那你能够用 TCC 方案了。


你须要本身编写大量的业务逻辑,本身判断一个事务中的各个环节是否 OK,不 OK 就执行补偿/回滚代码。


并且最好是你的各个业务执行的时间都比较短。可是说实话,通常尽可能别这么搞,本身手写回滚逻辑,或者是补偿逻辑,实在太恶心了,那个业务代码很难维护。


总结


本篇介绍了什么是分布式事务,而后还介绍了最经常使用的 3 种分布式事务方案

但除了上边的方案外,其实还有两阶段提交方案(XA 方案)和本地消息表等方案。


可是说实话极少有公司使用这些方案,鉴于篇幅所限,不作介绍。后续若是有机会再出篇文章,详细聊聊这两种方案的思路。