分布式事务<转>

原文 https://zhuanlan.zhihu.com/p/79833740

只要聊到你做了分布式系统,必问分布式事务,你对分布式事务一无所知的话,确实会很坑,你起码得知道有哪些方案,一般怎么来做,每个方案的优缺点是什么。现在面试,分布式系统成了标配,而分布式系统带来的分布式事务也成了标配了。因为你做系统肯定要用事务吧,如果是分布式系统,肯定要用分布式事务吧。先不说你搞过没有,起码你得明白有哪几种方案,每种方案可能有啥坑?比如 TCC 方案的网络问题、XA 方案的一致性问题。分布式事务的实现主要有以下 5 种方案:

  • XA 方案
  • TCC 方案
  • 本地消息表
  • 可靠消息最终一致性方案
  • 最大努力通知方案

1.两阶段提交方案/XA方案

所谓的 XA 方案,即:两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。

这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。如果要玩儿,那么基于 Spring + JTA 就可以搞定,自己随便搜个 demo 看看就知道了。

这个方案,我们很少用,一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的。我可以给大家介绍一下, 现在微服务,一个大的系统分成几十个甚至几百个服务。一般来说,我们的规定和规范,是要求每个服务只能操作自己对应的一个数据库

如果你要操作别的服务对应的库,不允许直连别的服务的库,违反微服务架构的规范,你随便交叉胡乱访问,几百个服务的话,全体乱套,这样的一套服务是没法管理的,没法治理的,可能会出现数据被别人改错,自己的库被别人写挂等情况。

如果你要操作别人的服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许交叉访问别人的数据库。

image

2.TCC 方案

TCC 的全称是:Try、Confirm、Cancel。

Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留
Confirm 阶段:这个阶段说的是在各个服务中执行实际的操作
Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)
这种方案说实话几乎很少人使用,我们用的也比较少,但是也有使用的场景。因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常之恶心。

比如说我们,一般来说跟钱相关的,跟钱打交道的,支付、交易相关的场景,我们会用 TCC,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。

而且最好是你的各个业务执行的时间都比较短。

但是说实话,一般尽量别这么搞,自己手写回滚逻辑,或者是补偿逻辑,实在太恶心了,那个业务代码是很难维护的。

image

3.本地消息表

本地消息表其实是国外的 ebay 搞出来的这么一套思想。

这个大概意思是这样的:

A 系统在自己本地一个事务里操作同时,插入一条数据到消息表;
接着 A 系统将这个消息发送到 MQ 中去;
B 系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息
B 系统执行成功之后,就会更新自己本地消息表的状态以及 A 系统消息表的状态;
如果 B 系统处理失败了,那么就不会更新消息表状态,那么此时 A 系统会定时扫描自己的消息表,如果有未处理的消息,会再次发送到 MQ 中去,让 B 再次处理;
这个方案保证了最终一致性,哪怕 B 事务失败了,但是 A 会不断重发消息,直到 B 那边成功为止。
这个方案说实话最大的问题就在于严重依赖于数据库的消息表来管理事务啥的,如果是高并发场景咋办呢?咋扩展呢?所以一般确实很少用。

image

分布式事务就是在分布式的场景下,需要满足事务的需求!上篇文章我们聊过了消息中间件,那这篇文章我们要聊的是分布式事务,把两者一结合,便有了基于消息中间件的分布式事务解决方案!不管是本地事务,还是分布式事务,都是为了解决数据的一致性问题!一致性这个词咱们前面多次提及!与本地事务不同的是,分布式事务需要保证的是分布式环境下,不同数据库表中的数据的一致性问题。分布式事务的解决方案有多种,如XA协议、TCC三阶段提交、基于消息队列等等,本文只会涉及基于消息队列的解决方案!

本地事务讲到了一致性,分布式事务不可避免的面临着一致性的问题!回到最开始跨行转账的例子,如果A银行用户向B银行用户转账,正常流程应该是:

1、A银行对转出账户执行检查校验,进行金额扣减。
2、A银行同步调用B银行转账接口。
3、B银行对转入账户进行检查校验,进行金额增加。
4、B银行返回处理结果给A银行。

image

在正常情况对一致性要求不高的场景,这样的设计是可以满足需求的。但是像银行这样的系统,如果这样实现大概早就破产了吧。我们先看看这样的设计最主要的问题:

1、同步调用远程接口,如果接口比较耗时,会导致主线程阻塞时间较长。
2、流量不能很好控制,A银行系统的流量高峰可能压垮B银行系统(当然B银行肯定会有自己的限流机制)。
3、如果“第1步”刚执行完,系统由于某种原因宕机了,那会导致A银行账户扣款了,但是B银行没有收到接口的调用,这就出现了两个系统数据的不一致。
4、如果在执行“第3步”后,B银行由于某种原因宕机了而无法正确回应请求(实际上转账操作在B银行系统已经执行且入库),这时候A银行等待接口响应会异常,误以为转账失败而回滚“第1步”操作,这也会出现了两个系统数据的不一致。

对于问题的1、2都很好解决,如果对消息队列熟悉的朋友应该很快能想到可以引入消息中间件进行异步和削峰处理,于是又重新设计了一个方案,流程如下:

1、A银行对账户进行检查校验,进行金额扣减。
2、将对B银行的请求异步写入队列,主线程返回。
3、启动后台程序从队列获取待处理数据。
4、后台程序对B银行接口进行远程调用。
5、B银行对转入账户进行检查校验,进行金额增加。
6、B银行处理完成回调A银行接口通知处理结果。

image

通过上面的图我们能看到,引入消息队列后,系统的复杂性瞬间提升了,虽然弥补了我们第一种方案的几个不足点,但也带来了更多的问题,比如消息队列系统本身的可用性、消息队列的延迟等等!并且,这样的设计依然没有解决我们面临的核心问题-数据的一致性

1、如果“第1步”刚执行完,系统由于某种原因宕机了,那会导致A银行账户扣款了,但是写入消息队列失败,无法进行B银行接口调用,从而导致数据不一致。
2、如果B银行在执行“第5步”时由于校验失败而未能成功转账,在回调A银行接口通知回滚时网络异常或者宕机,会导致A银行转账无法完成回滚,从而导致数据不一致。

面对上述问题,我们不得不对系统再次进行升级改造。为了解决“A银行账户扣款了,但是写入消息队列失败”的问题,我们需要借助一个转账日志表,或者叫转账流水表,该表简单的设计如下:

image

这个流水表需要怎么用呢?我们在“第1步”进行扣款时,同时往流水表写入一条操作流水,状态为“待处理”,并且这两个操作必须是原子的,也就是说必须通过本地事务保证这两个操作要么同时成功,要么同时失败!这就保证了只要转账扣款成功,必定会记录一条状态为“待处理”的转账流水。如果在这一步失败了,那自然就是转账失败,没有后续操作了。如果这步操作后系统宕机了导致没有将消息成功写入消息队列(也就是“第2步”)也没关系,因为我们的流水数据已经持久化了!这时候我们只需要加入一个后台线程进行补偿,定期的从转账流水表中读取状态为“待处理”且最后更新的时间距当前时间大于某个阈值的数据,重新放入消息队列进行补偿。这样,就保证了消息即使丢失,也会有补偿机制!B银行在处理完转账请求后会回调A银行的接口通知转账的状态,从而更新A银行流水表中的状态字段!这样就完美解决了上一个方案中的两个不足点。系统设计图如下:

image

到目前为止,我们很好的解决了消息丢失的问题,保证了只要A银行转账操作成功,转账的请求就一定能发送到B银行!但是该方案又引入了一个问题,通过后台线程轮询将消息放入消息队列处理,同一次转账请求可能会出现多次放入消息队列而多次消费的情况,这样B银行会对同一转账多次处理导致数据出现不一致!那怎么保证B银行转账接口的幂等性呢?

同样的,我们可以在B银行系统中需要增加一个转账日志表,或者叫转账流水表,B银行每次接收到转账请求,在对账户进行操作的时候同时往转账日志表中插入一条转账日志记录,同样这两个操作也必须是原子的!在接收到转账请求后,首先根据唯一转账流水Id在日志表中查找判断该转账是否已经处理过,如果未处理过则进行处理,否则直接回调返回! 最终的架构图如下:

image

所以,我们这里最核心的就是A银行通过本地事务保证日志记录+后台线程轮询保证消息不丢失。B银行通过本地事务保证日志记录从而保证消息不重复消费!B银行在回调A银行的接口时会通知处理结果,如果转账失败,A银行会根据处理结果进行回滚。


4.可靠消息最终一致性方案

这个的意思,就是干脆不要用本地的消息表了,直接基于 MQ 来实现事务。比如阿里的 RocketMQ 就支持消息事务。

大概的意思就是:

  • A 系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就直接取消操作别执行了;
  • 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 mq 发送确认消息,如果失败就告诉 mq 回滚消息;
  • 如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务;
  • mq 会自动定时轮询所有 prepared 消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了。
  • 这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。
  • 这个还是比较合适的,目前国内互联网公司大都是这么玩儿的,要不你举用 RocketMQ 支持的,要不你就自己基于类似 ActiveMQ?RabbitMQ?自己封装一套类似的逻辑出来,总之思路就是这样子的。
image

5.最大努力通知方案

这个方案的大致意思就是:

  • 系统 A 本地事务执行完之后,发送个消息到 MQ;
  • 这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;
  • 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。

你们公司是如何处理分布式事务的?

如果你真的被问到,可以这么说,我们某某特别严格的场景,用的是 TCC 来保证强一致性;然后其他的一些场景基于阿里的 RocketMQ 来实现分布式事务。

你找一个严格资金要求绝对不能错的场景,你可以说你是用的 TCC 方案;如果是一般的分布式事务场景,订单插入之后要调用库存服务更新库存,库存数据没有资金那么的敏感,可以用可靠消息最终一致性方案。

友情提示一下,RocketMQ 3.2.6 之前的版本,是可以按照上面的思路来的,但是之后接口做了一些改变,我这里不再赘述了。

当然如果你愿意,你可以参考可靠消息最终一致性方案来自己实现一套分布式事务,比如基于 RocketMQ 来玩儿。

当然,分布式事务最好的解决方案是尽量避免出现分布式事务!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容