好 我们接下来开始讲分布式事务
分布式事务出现的一个场景是这样的
就是我们大家知道从巨石架构演进的一个过程中
最早其实是所有数据库
我们所有业务的表数据库基本都是在一起的
说白了就是一个单库 一个单实例
那么以前的话
我们通常解决分布式事务其实是不存在的
因为你都在一起
所以我们只要使用一个标准的IDBMS的提供了一个事务的特性
所谓的ACID
保证原子 一致性 保证持久化等等 事务隔离这些
大家学数据库基本都知道ACID
你的所有的业务体系都是在一个数据库里面的
通过一个事务
然后控制好事务的隔离级别
基本上就能做到我们业务所谓的数据一致
那么这个问题出现
我们也知道就是巨石架构它很难满足
接下来更大规模更海量的用户的这种场景
所以说我们不得不开始要做一些所谓的
scaling out也好scaling up也好
我们先看一下所谓的垂直拆分和水平拆分
对于数据库
单实例无法在去承载更多用户的数据量的时候
我们一般怎么做
最简单的方法就是分shard
就是让我们的数据库做sharding
所以我们也见过所谓的用类似像TiDB
这种新型的new SQL
然后它底层其实是分了很多shard
每一块负责一小部分数据
然后最终在TiDB
我们所谓的叫Ti server层
来去实现两个shard一个分布式事务
说白了你用起来像一个单机的数据库
但是它底层其实是分了很多个分片的
然后分片与分片之间的数据的一致性
通过TiDB自己再去实现分布式事务
来解决这个问题
所以它是通过中介的手段
让你感觉到用的时候像用单数据库一样
底层的一些复杂的分布式事务都给它解决掉了
这是一种思路
也是一种常用的思路
那么我们知道微服务它其实比较强调的是隔离
前面这种方案的确能解决不少问题
但是如果你的数据库仍然还是全部在一起的情况下
它会产生一种什么情况
是不是有可能会产生数据库与数据库业务之间的相互影响
它隔离性其实比较差
虽然问题是可以解决
但是因为隔离性不好
所以说有可能a业务意外的一些sql
可能会影响我整个实例的可用性
所以我们做垂直拆分的时候
一般是按照业务的粒度去拆成了好多个微服务
每个微服务是自己独占了一个数据库
所以说这个问题就抛出来了
因为你的业务被拆成了不同的应用服务
那么假设我有一个业务场景
需要保证几个业务的细度业务的服务
它的数据之间要保证一致性
所以说就产生了一个分布式事务的问题
也就说从我业务角度来看
这几个子系统的一个业务场景里
需要它们对应的自己的那份数据
从业务全局角度来说要保证一致
也就说这个问题就产生了
讲到分布式事务或者说讲到事务
我们就得举例
最典型的就是一个转账的问题
就是在我们金融业务里面
我举个例子
在我这边的话
支付宝的账户表和余额宝的账户表
它业务转账的一个场景里面
产生的一个事务问题
就是说我的钱从支付宝要转账到余额宝这么一个业务场景
我们先看一下在我这个地方有两张表
一张A表首先我们有一个自增的id
然后有一个use_id 就是用户id
这个表里面表示的是支付宝有多少钱 叫amount
我们有一张余额宝的账户比较叫B
同样的它有个user_id
有个自增id
有一个用户ID
然后有多少钱
我的场景也很简单
就是user_id=1的用户
从支付宝转账1万块钱到余额宝
那么其实过程它就分成两个步骤
你看我们有一个pay的支付的这么服务
因为所有的数据库都是在一起的
所以早期我们做法非常简单
就是一个本地事务
让我们的A表减去1万块钱
就是我们的支付宝这个表扣除1万
所以我们sql就会写成
UPDATE A SET amount = amount - 10000 where user_id = 1
这个就是表示支付宝要扣钱了
那么谁要加钱呢
就是余额宝要增加1万块钱
所以第二步是什么
UPDATE B SET amount = amount + 10000 where user_id = 1
那么如何去保证一致性呢
因为它是一个本地事务
所以在我MySQL里面
我天然利用IDBMS关系型数据库它的事务特性
来保证我们从数据库层面的ACID
以及最终数据的是ACID的
其实是比较简单
随着我们的业务规模变大
我们进行了微服务架构拆分
我们一般按照垂类应用拆分很明显
支付宝业务和余额宝业务可以划成两块
一个就是说自己的一个钱包的概念
余额宝可能是它偏金融的一个金融理财这种服务
我们天然就可以把它划成两个微服务去看待
甚至它是两个大的业务平台
ok 我们这个地方简化了
我们就分成了paa和balance两个服务
一个是支付宝
一个是余额宝
同样的因为每个微服务是独占了自己的一个数据库
针对user_id=1的转账动作
跨了两个服务
一个是pay服务
一个是balance服务
我们需要保证的是跨多个微服务的步骤
这种情况下它的数据要一致性
从刚刚SQL我们又演进了
第一就是说微服务pay的支付宝表要扣除1万
第二步为服务balance的余额宝表要增加1万
那么分别对应的
其实对于pay服务来说
因为它是自己独占的数据库
那么他去更新这张 a表的时候
它执行的是本地事务
注意它是本地事务
那么同样的通过被去调balanced服务加钱的时候
它也是一个本地事务
每个系统它对应的是自己独立的数据源
而且甚至有可能它们位于不同的机房
那么同时调用多个系统的服务是很难同时保证成功的
这就是跨服务的分布式事务问题
举个例子
我的pay调这一步减1万块钱成功了
但是我这个rpc请求
也就是我通过pay调用balance的rpc请求
可能会失败
因为网络原因
或者说因为对方超时了等等一系列原因
你是不知道对方的一个状态
并且你其实这种情况下
你是不知道对方加钱有没有加成功
就是余额宝它有没有加钱 加1万块钱加成功
因为你是不确定的
因为你调它超时了
你其实是不知道的
如果你是调不通可能怎么办
你怎么补偿对吧
所以说这个数据不一致
也是钱扣了但是余额宝里面钱没到账
但是我的支付宝已经扣钱了
所以说每个系统很难保证多个系统之间都成功都失败
其实很难做到
但是我们有一个是很容易保证的
那就是说我们要解决分布式事务的问题
首先我们很容易想到就是说每个系统要保证自身的ACID
比方说我的pay服务或者balanced服务
你从单个独立系统的角度来说
它们要保证自己的事务性或者保证自身的ACID
相对比较简单
因为比方说我的pay服务
我的支付宝的业务依赖本地的一个MySQL
是很容易保证事务的
因为我依赖MySQL的ACID的特性去做
这个很容易做到
那么这个时候我们就要开始想了
既然每个系统就能保证自己的事务性
似乎只需要通过一个消息系统
通过一个消息告诉它
这个消息如果能可靠地被持久化
那么另外一个我的子系统
就是我的下游的其中的一个系统
它如果能消费到这个消息
是不是大家数据一致的
基于这样的一个想法
我们就拿实际的一个业务场景
我们来看一看什么叫事务消息
我刚刚说我们要通过消息传递来解决问题
在北京有一个很有名的叫姚记炒肝的一个吃的
不知道大家吃过没有
我们一般来说 甚至是很多这种餐饮店
基本上都是类似的一个模型
就是你先付钱
付完钱以后他会给你一个收据
类似告诉你一个多少号之类的
然后拿了那张票
你就在它的出货区在排队
应该有这种经历
先给钱嘛 钱给了 你票丢了不给你另说
反正就是你先给钱
钱可以先扣
扣完之后拿着票你在那等
好 为什么
从系统设计上或者从我们现实生活中
他会把付钱和取货
两个动作分开
通常我们看什么港片枪战片
什么叫一手交钱一手交货
保证原子性
万一你钱给了你货不给我
那肯定不行
所以这种大流量的餐饮店他为什么会这样做
有一个很重要的原因是为了使他们的接待能力变强
我们系统拆分也好
或者说通过事务消息来解决分布式事务的问题也好
本质上都是因为并发量太大了
我要让我的系统吞吐更高
所以它会分成两步
因为两个系统两个步骤
它是有可能一个快一个慢
而且中间要通过网络的传输
所以整个的交付时间是不可靠的
举个例子
你要是先付钱必须先到货
那么你就要等他们的厨房做完之后给你
他才能进行下一个交易
是不是
所以我们很容易想到就是说你先付钱
然后我这边一波波的人去收钱
钱收到了反正你又不会走
然后我们在厨房排队
排队去把这些一个的订单给处理掉
那么既然你钱付掉了
那么你收到一个凭证
凭证是不是我们理解为就是一个消息
那么这个步骤地发生
扣1万块钱
假设我的支付宝账号扣除1万块钱和拿到凭证
凭证上可能写着让余额宝账号增加1万块钱
那么只要这个消息或者说凭证能够可靠地保存
最终我可以拿凭证让余额宝的账户加1万
通过利用消息的凭证完成我们数据的最终一致
这两个步骤可能是有一些时间的
可能是几百毫秒
那么我觉得在大部分长线如果是几百毫秒的延迟
其实是感觉不到的
经过了一个消息系统的扭转
但是可能几百毫秒的延迟
对我们来说感受不是那么明显
甚至说我们通过业务上的一些优化
比方说你付完钱
你要转到余额宝我可以显示说扣款成功
然后我的余额宝正在交易或者正在处理
余额宝什么时候处理完
我再提示你它交易成功
其实就像我们转账的
很早以前银行转账一样
就是你钱可以先扣掉
然后什么时候到账再说
所以分成两个动作
就像我们刚刚说的
你去交钱拿货一样的
给你的一个消息凭证拿那个票
但是必须等到厨房排队处理完
我们想想排队去处理这些订单
是不是跟利用消息系统把这些消息存起来一模一样
因为好像我们拿到凭证以后
其实在厨房那边也会生成一个订单
或者说也会生成这样一个票据
我们叫消息的消费
在厨房里面他们那边可能也会贴个订单号
比方你去一点点买奶茶
师傅可能桌上贴了好多个订单号
你一张凭证他一张凭证
我们其实不会从两份消息其实会生成一份消息
只是说厨房会去消费这份消息
就像一个消息队列在排队处理这些东西
然后处理完了之后再通知你
你来去拿货
我认为本质上一样的
所以说我们从这么一个场景
我们就很容易想到了事务消息的一个做法
那么通常解决事务消息
怎么让这个消息比较可靠的去存储
我们再看到这个图
首先发起一个转让行为还是一样的use_id=1
然后调到我们的pay服务
我先会执行一个本地事务
让我的这张表执行刚刚那个UPDATE操作
扣除1万
关键是我现在需要把我的这条消息
通过某一个数据库或者某一个消息队列之类东西存起来
之后再通过我们balance服务去消费这条消息
然后再让钱加起来
实际上分成两个动作
现在核心我们考虑就是我本地的对于pay服务务来说
对于支付宝的MySQL来说
如何保障做完这条交易成功
同时让我这条消息可靠的存储
通常有以下几种方法
我们总结下来就是说一个叫Transactional outbox
第二个就是叫Polling publisher
第三个就Transaction log tailing
第4个就是两阶段提交的msg Queue消息队列
如果能够保障
或者说我们事务消息一旦被可靠地持久化
我们整个分布式系统其实就演进为叫最终的一致性
因为我刚刚说它有一定的延迟
也就说不是一瞬间的
一手交钱一手交货
它是有一定的延迟的
只要你的消息最终能够被消费
才能保障它的最终业务数据的完整性
所以我们要什么尽全力的让这个消息消费成功
把消息送达到下游的业务消费方
努力努力去送达这个消息
让下游业务方去消费完成的这种模式
也叫Best Effort 就是尽最大努力
只有这个消息被消费完
整个交易才能理解为是结束的
并且算是一个完整完结
那么尽最大努力交付
主要用于这样的一种场景
因为我们刚刚那种模型
你毕竟还是一个系统一个体系内
余额宝和支付宝是可以理解为是一个大公司旗下的两个业务
或者说两个子公司之类的
它毕竟还是内网的通信一个交易
但我们再来看一种场景
假设你的一个分布式事务要求的是下了一个订单
然后用户去支付宝
或者是去别的一些支付平台付钱了
他是跟你完全两家公司
一家支付公司
然后你可能是做电商的
或者说你可能是做游戏的
对于这种第三方的一种调用
也存在所谓的叫分布式事务的一个问题
或者说我们假设是做游戏的
我们游戏叫in-app
就是app内的一个充值
这时候你可能会请求请求苹果的App Store的充值
所以说你的游戏和你的支付平台
或者说你的电商和支付平台
它就是两个相互独立的体系
隶属于不同的公司
对吧
怎么办
这种努力送达其实在这种业务场景里面也非常常见
可能做过支付宝交易接口的同学都知道
我们一般会在支付宝的申请他的接口
我讲假如我要去对接支付宝
那么它有个所谓的回掉的一个页面或者回调一个接口
然后会填上你的
它怎么回调告诉你支付成功
你要添个回调接口
如果在用户支付成功以后
支付宝它会定期来调你接口
它在吊你的时候 你失败了
假设你某个一些意外的原因
你的服务不可用 故障了
或者说因为网络原因它调不通
用户的是不是钱付了
买道具就不见了
用户肯定很气
如何保证两个大业务体系
完全不是一家公司的这种数据一致
其实就是靠支付宝不断地回掉
直到成功
所以我们可以想象一下
只要你的交易跳到了支付宝平台
带上了你的业务id
带上你的业务订单id
带上你的业务接入的id
告诉他支付成功了
在他那边因为你可以想象一下在支付宝那边
实际上就是它利用自己本地事务把这个东西存起来了
它的回调的流程
只要订单叫支付成功
但是没有说完成交易
也就是说它调你没成功之前
他会尽自己最大努力
不断重是你可能是每1分钟每5分钟
然后每10分钟他有一个back off
就是会退让的那种掉 你直到成功
假设你短时间内不可靠不可用 即便他调失败了
支付宝它因为会不断重
所以它最终一定会尽最大努力的让这笔交易成功
我们可以想象一下
实际上是不会出现回滚的对吧
也就说你付完钱以后钱不会退给你
然后由到去回收这种不存在的
我们这种情况下一定是最大努力去交付
让整个交易可以结束
对 当然了这里面其实有一个小细节
假设支付宝的接口回掉你 你成功了
但是你在response回去的时候
失败了 如果他下次再来调你一次
对吧
他下次再来调你一次
你的灶具有没有可能会发两份
看上去这个问题其实是一个corner case对吧
一个很边缘的case 但是实际写代码的时候
我们很多同学会不太注意我们交易的一个秘密的秘等性
之前我做 Apps应用内购买的时候
我们就遇到过向苹果支付成功了
然后他会苹果会给你一个支付成功的回
其实回执你可以拿到里面绑定账号
根据回执验证成功拿到订单号
然后去完成交易
发放我们的游戏道具
就有人利用漏洞抓包抓到了苹果支付的凭证
然后用凭证可以不断的去刷你的接口
然后就相当于有一个类似像什么pay success这种接口
不断掉你把回传进去
然后你就一遍一遍一遍的去加道具 为什么
因为你没有处理幂等
如果你处理了幂等
订单交易一旦结束只发放一次道具
那么它即便再来调你这个接口
苹果支付的receive的验证成功
但是你这个订单号其实已经完结了
那就不应该再重复发放道具
所以我这个地方要友情提醒一下
我们任何在做这种重试类的业务
甚至是这种回调的业务
如果对方至少调一次或者说至少调一次的这种场景下
意味着它可能会调多次
那么你一定要处理好同一笔交易组合
避免加多次钱或者说发放多次道具
这个其实是要业务幂等的一个问题
一定要注意
我们再来展开讲
我们刚刚说的是需要保障MySQL你的业务表A表
以及你的消息要么都成功 要么都失败
那么对于这个地方
Transactional outbox 它的做法是什么呢
其实比较简单
我们看pay服务
我们pay服务现在还是一样的
就是user_id=1我要减1万块钱
我是不是最容易想到的一个做法是什么呢
你的本地MySQL能保证
我是不是在你本地MySQL再建一张表
这张表就叫msg表
它可能会有如下的一些字段
id-自增id 用户id amount加多少钱
然后状态
那么加完这张表以后
我其实开一个本地事务
让我的A表和msg表同时都处理成功
是不是就保障了我的支付宝既扣了钱
同时我的这张msg表里面针对这笔交易的这一批流水
这个操作就通过本地事务
要么成功 要么失败就保存下来了
对应的我们的SQL就这样的一个写法
我BEGIN TRANSACTION开启一个事务
然后UPDATE A SET amount = amount - 10000 WHERE user_id = 1;
这是扣款
第二个我就INSERT INTO msg 表
然后它有user_id amount status这几个字段
那么我们就把用户id加钱的操作
然后以及状态
状态一表示的是这个消息待处理之类的
END TRANSACTION 然后再COMMIT
那么通过这样的一个Transactional outbox一个收件箱
事务性的收件箱
其实msg不就是一个事务性的收件箱的这样的表
通过这样的方式
我的msg表 我的消息凭证可以被可靠的存储
我们就要想办法将这个消息通知余额宝
那么余额宝在它处理成功以后
再发送一个消息告诉你
这个交易我钱收到了
然后我给他加了1万块钱
那么麻烦你把这条消息凭证从库里面给踢掉
我们这张msg表
怎么去通知下游的余额宝吗
我们待会也会去讲
现在我们知道有这么一种姿势
就是这张msg表是通过MySQL的一张普通的表
把这些消息给存起来
我们接下来就进行到下一个阶段
余额宝如何知道这张msg表里面新增了一条记录
也就是说你如何通知余额宝把这个事情给干完
同时你如何让余额宝给我一个ack
给我一个回馈
我可以把这条消息删掉
因为他交易完了
可能把状态更新成0
或者说我把它物理删除
delete掉都是可以的
我们就进入到第二阶段叫Polling publisher
从订阅者从发布者这边我来拖
Polling publisher的思路有点类似于
我们定时的轮巡这一张msg表
我是可以起一个后台这么一个job
可能叫pay-task这么一个job
我定时的去msg这张表
写个什么样的SQL呢
where状态等于1
然后再limit一下 limit 1000或者多少
你因为它可能太大了
我避免说全取出来
我就写个这样的SQL
然后按照id排序
保证它的顺序消费
当然有可能你的业务不需要顺消费
我把它全部定期的扫描把它捞出来
捞出来以后我这个pay-task是不是可以调balance
也就是说调余额宝的服务
告诉它给谁加钱
这个场景里面我们user_id=1
实际上就是让B表加上1万块钱
把这个事情给做完结
所以它是通过一个pay-task定时拖这张表
然后再通过努力送达的方式告诉balance服务
去把剩下的那一部分事务性的工作要做掉
所以这个地方其实也用的努力送达的一个姿势
举个例子
如果balance服务不可用
也就是说余额宝这段时间短期不可用
我是不是pay-task要做一个back off
定期去重试
直到它的交易成功
我才能把这笔订单彻底放弃
否则我会不断尝试不断重试
当时我们在做我们内部投稿系统的时候
之前大家也听到过
我们讲了很多次的一个叫archive-services
就是一个投稿服务
我们在实现CQIS模型的时候
我们的投稿流程和我们的查询服务分开服务的时候
我们最早第一个版本其实就是利用
这种Polling publisher的思路
它定时的从我们的消息表去把里面的一些数据拖出来
在发送到我们的balance
当时我们是这样一个做法
但这种做法其实它不是特别好
定时意味着延迟
那么你如果频次太密集
对我的数据库查询压力太大
第二你如果查的频次太低了
意味着我的消息延迟比较久
所以说这种Polling publisher它有些缺陷
我觉得用起来不是那么爽
所以我们很快通过这样的一个姿势
我们立马就研发了一个中间件
这个里面也叫Transaction log tailing
它是什么一个思路呢
我们上述保存消息的方式
保存的这种msg表
我的业务表和msg表
虽然通过本地事务去保存的
但是从架构上来看
因为你每次搞分布式
我还要在你的MySQL里面搞张这样的一个消息表
看起来好像不是很优雅
那么这张表如何能够有变化就能快速被订阅到
其实也好做
我们当时就是利用msg这张表
就是使用binlog的订阅
我们当时使用的是canal中间件
然后订阅这种表的任何变更
通过canal发送到我们的消息队列
最终消费的消费者
我们balance服务它可以直接去消费kafka
然后其实从某种意义上
我只需要引一个中介
然后pay和balance两个服务其实也没有直接关联
它是通过消息中间件解耦的
然后我们的canal服务只要保证我的里面的数据传输
它是只会多头投不会不会少投的话
其实我的balance服务
其实最终也是可以把里面的数据消费完
然后做一个本地事务更新
那么这样的话其实有点像事件驱动一样
就是来一条处理一条
使用的订阅也不能叫事件驱动
我觉得更偏向实时消费
或者说实时流式流式消费的一个模型
在我们的balance服务
或者说我们有一个balance-job专门负责订阅的服务
其实某种意义上也是一个努力送达的模型
就是我必须最终一定会让这笔交易成功
所以我们发现这种所有做努力送达的模型
必须事先预扣款
或者说预占资源的做法
举个例子 我要做一个交易
最终一致性的模型
或者说它是一种努力送达的模型
它其实一定是先扣钱的
你一定不会遇到先给你加钱
然后再去扣对方钱
万一你的钱包花光了
我就扣不成功了
还得处理事务回滚还回滚得了吗
因为这个时候这个人钱都到账了
他可能也花掉了
其回款不成功
所以说一定是先扣钱再加钱
一定是先支付后发道具
我们要注意
我们现在通过canal中间件流式地实时地消费数据
看上去优雅了
但是我刚刚说了
你每搞一个分布式事务
还得在你的本地MySQL撸一张msg表
实在是不太优雅
有没有更好的做法呢
有没有更好的做法
我们先把这个问题抛出来
我们也可以想一想
我们先解决一个问题
在这个场景里面我们知道kafka的消费模型
它可能是满足至少一次或者至多一次
甚至说我们前面的这种Polling publisher
也是无法保证你掉对方成功
对方的rpc回包的时候失败
你比方说你调它成功了
它加了1万块钱
但是因为rpc回调失败了
业务逻辑执行完了
但是它回包回失败了
你也不知道它成不成功
你肯定要重试
你这个时候可能会重试
有可能会加2万块钱
所以说业务幂等的问题也是会被提出来
就是说在我们的消费者很容易遇到消息重复投递
或者说消息重复消费的这么一个问题
如果同一条消息重复投递的两次
或者说被重复消费了两次
那么我的余额宝的账号将会增加2万块钱
而不是1万块钱了
那么为什么相同的消息会重复投递或者说是重复消费
因为比如说我余额宝在处理完这个消息msg之后
原则上我是不是要发一个ack消息给到支付宝
告诉你麻烦你把我msg表里面这条状态等于一的这条记录给删掉
但是如果这个时候支付宝因为一些别的原因挂掉了
意味着你之前想通过余额宝发送消息
告诉它删除这个动作失败
我肯定会重试
那么重试不知道是什么时候
但是我的支付宝一瞬间启动的时候
我一定会去扫描这张msg表
或者说通过订阅binlog的方式
我会继续把msg发送
你状态等于一
你没交易完我肯定会重复发送
那么一旦我重复发送
你肯定就会加钱加错
怎么做
所以我们在做这种流水交易型的业务
一定会生成一个交易号
或者说流水号
所以我们叫全局的唯一id
加一张去重表
那么我们看一下
在余额宝这边增加一张叫做消息应用状态表
也叫msg_apply的一张表
通俗来说它就是个账本
我们看SQL for each msg in queue
也就是从消息队列里面拿到的每一条消息
开一个事务
执行一条select * from msg_apply
就是我刚刚说的这条
消息应用状态表
那么我去找我这个消息
因为我们知道每一个消息肯定有一个消息id
我只要去把这个消息捞出来
然后看是否存在
如果存在意味着我之前肯定处理过了
那么如果没存在
也就是我的count=0
我就会执行加强这个动作
同时我要事务插入msg_apply
表示我交易已经被我处理过了
这是一种做法
还有一种做法其实是我们可能是在订单
假如他有个订单的一个组件
那么基于订单组件可以更新状态
基于这个状态判定也是可以做到的
这也是一种常见的思路
要么就是应用状态的一个信息
或者说利用我们的消息叫消息系度表
然后做一个判定
避免重复消费
幂等或者说业务避免重复消费
大家也可以网上搜一搜
有很多种思路
有基于顺序id自增
然后判定当前消费到哪来的
这种也可以
那么这样的话只需要保存一条记录
那么基于我们的消息
全局唯一id加去重表
全局自增唯一id加版本号大小于判定的
或者说别的一些姿势
本质上都是要解决重复消费的问题及状态
status基于状态判定可能也是一种
这是业务幂等的一个问题
我们在看一下分布式事务里面
可能也会还是遇到一个叫二阶段提交
二次提交你可以看一下这张图
那么可能要做二次提交
它一般会依赖一个叫TC事务协调器
叫transaction coordinator
那么第一步
我们可能会发一个事务
发一个二阶段提交的请求
那么它其实支付宝数据库也好
余额宝数据库也好
它其实会经过好几个步骤
我们看有好多次往返
这是第一次踢掉事务协调器
二阶段的事务协调器它会第一发送一个prepare锁定
我们叫消息准备阶段
那么它会发送给它
其实就是要把这些行业锁定掉
因为你这时候有很多并行操作的话
它可能会起所谓的事务冲突
所以它要先prepare先锁定
那么告诉你成功都返回
那么如果投票阶段投成功的话
我们其实就进入到提交阶段
那么它就会commit支付宝再commit余额宝
那么这个是从数据库层面看到的一个二阶段提交
其实这种不符合我们微服务
所谓的每人独占数据库这种模型
但是二阶段提交的思路
但是还是可以借鉴和应用的
那么怎么做呢
我们最后一个姿势叫二阶段提交的消息队列
我们前面的思路是通过引入一张消息的msg凭证表
那么它是在你的应用微服务里面
你们通过本地事务来保证它一定持久化成功的
你这样架构上搞得不是很爽
前面我提的这个问题在这个地方会得到解答
如果我们有一个类似的消息队列
有一点像二阶段提交的这么一个序列
比方说你可能使用的是阿里开源的RocketMQ
我们看一下它是怎么玩的
第一步发送prepare消息
是不是跟我们二阶段提交发送准备阶段有点像
发送给数据库的
我是第一步发送给RocketMQ的消息集群
也就是说我现在有一个订单要做一个什么操作
要加钱 对吧
我可能把消息先发送给它
第一步注意prepare消息虽然是发送的
但是没有提交
我的消息暂存起来了
但是没有提交
所以这是第一步
只是prepare消息叫准备阶段
我第二步会执行本地事务
也就是说让我的支付宝扣钱
如果第二步执行成功
我会进入到第三步COMMIT_MESSAGE
当然了也可能你扣钱失败
比方数据库超时或者别的一些原因
所以第三步也可能会回滚消息
那么如果是成功消息
commit了 那么最终RocketMQ认为这个消息被交付了
它就可以通知下游
或者说下游可以来消费它
可能是push的或者是poll的
它最终可以被下游消费到
也就是说只有commit来了
你才能去消费到
所以它有这么一个步骤
我们就可以分析一下
这三步任何一步失败会产生什么样的一个行为
假设第一步发送prepare消息失败
那就直接结束
直接告诉用户报错了交易失败
如果是第一步消息发送成功
第二步执行本地事务失败会发生什么
这个地方有点意思
RocketMQ在以前的版本它支持叫消息回查
叫状态回查
因为注意这个时候有一条prepare消息
在我的RocketMQ集群里面
它在我这消息里面存了
它是个带准备的一个消息
RocketMQ它发现这个消息
在准备的怎么这么久没人搭理我
所以说RocketMQ它当时支持的一个机制是什么呢
把这个消息发现一段时间没人搭理它
它就把这个消息捞出来
然后反向的询问业务
就问你这个交易到底你要搞不要搞
你到底是完没完成对吧
一问你失败的连记录都没有 我就爆掉
我就失败掉之后让它这个消息直接丢掉
如果我问你
如果我们第二步执行本地事务是成功的
但是第三步发送确认消息失败
也是这样的
因为你无论是commit还是roll back的指令
都无法提交到我RocketMQ集群里面
我来回答你的时候
你告诉我这个消息是成功
我就会自动commit
如果你告诉我失败我就abort掉
或者说我把它roll back掉
让它失败掉
所以123任何一步失败似乎都没有什么大的影响
这个就是巧妙之处
它其实利用prepare加commit两个阶段来做的
一旦本地事务成功
我们第三步也成功
其实只要第二部成功
那么无论是回查还是你主动提交
它都能保证最终这个消息可以被可靠的传递
我们才切到消费者的视角来看一看
我们第四步
第四步就是接收消息
接收消息以后我就会执行本地事务
业务的幂等性一定是你的消费方自己去搞定的
其实不是在RocketMQ来保证的
一定是你自己搞定的
如果本地事务加钱成功余额宝加钱成功
那么我的第三步本地事务执行成功后
发送确认消息
也就是说所谓的ack机制
告诉RocketMQ你ack掉了
这条消息被我正确消费了
那么麻烦你把这个消息从我RocketMQ群里面给删除掉
这样的话就保证了一个通过消息中间件来解耦
最终你可以理解为两个子系统执行本地事务
加异步的一个方式来解决的
那么本地事务间的消息传递是用这个中间件来搞定的
那么RocketMQ实际上它支持的是一个二阶段提交
同时它支持的是消息回查
我记得后面的版本是吧
下回再砍掉了
砍掉都没关系
因为你的生产方其实也可以去主动
自己回扫一遍系统有没有存在这种订单
然后再做一个补偿
就告诉RocketMQ哪些消息commit
哪些roll back
这也是一种做法
达到效果以后
其实基本上我们的就讲完了
那么对于消费者集群
你执行本地事务失败的情况
如果你一直重试都失败
那么只能去人工介入的
因为不太可能会去回滚整个流程
因为它是符合我们所谓的Best Effort
就是努力送达的一个模型
如果要做回滚的话
整个系统的复杂度也会大大提升
很容易出bug
所以我们通过二阶段的消息中间件
那么解决分布式事务的问题
但是大家请注意
我们目前所有讲的分布式事务的解决方法
其实它都是一个异步的
但如果我某一些特别变态的业务场景需要同步做
怎么办
需要同步的感知到
不是这种异步的感知
因为我不知道对方执行到什么情况
当然我们从产品设计层面一步其实也可以接受
就像我们下单支付
其实支付成功了
你的货也不会一瞬间发出来
它会经历什么仓库出仓什么乱七八糟一堆这种阶段
你在界面上刷一刷
它的状态会更新
那也是一种思路
从我们的业务流程上设计的
让你感知到他东西在变化
最终数据或会发给你这种一致性的感觉
除了利用二阶段消息来解决
我现在关注到其实阿里前段时间开源的
一个叫Seata一个事务的框架
我建议大家可以关注一下
那么Seata二阶段提交
它既支持二阶段提交
也支持TC
TC我后面会讲
那么Seata的这种二阶段提交和传统的二阶段提交
有一些差别
我觉得两个层面第一架构层面
传统的二阶段提交通常都是依赖我们数据库的这种XA的协议
XA的协议来做的
那么Seata它其实是以更符合我们现在
微服务的这种以服务的视角来做二阶段提交的
第二提交的优化
我们以前这种差异协议在数据库层面做
它要锁行的那一行它就锁定了
在commit的阶段这个行锁才会解开
那么对于二阶段提交其实是
事务性的资源锁不会保证到第二阶段
它其实是在Seata第一阶段就将本地事务提交到Seata的TC节点了
那么这样的话可以省去掉第二阶段持锁的时间
整个效率会变得更高
其实有点像努力送达
就是说你第一阶段只要成功了
那么我一定会送达到让它去成功
大家可以去研究下Seata
我理解好像说现在我不知道Seata Go版本的做的怎么样了
但是Java的版本是做的相对比较成熟的
我觉得也可以关注一下
那么Seata的TC模块实现
虽然他是自己做的
但是我理解你要自己做
在你们公司体量不是特别大的情况下
你甚至哪怕你要MySQL模仿成Seata
它也可以做到红的一个效果
因为你要MySQL
我就可以做ACID的
只不过它是个集中式的
这个可能是需要你去考虑的
最后我们再来讲一下分布式事务里面
非常典型的TCC的一个解决分布式事务的一个玩法
那么TCC它是try conform cancel这三个词语的缩写
那么TCC的要求
每一个微服务说白了要提供三个接口
第一预处理叫try
第二确认
第三叫撤销
这三个阶段
那么try这个操作我们先看这个图
我们看这有个微服务叫pay
这就是刚刚说的支付宝
我们这里balance是余额宝
同样的还是刚刚user_id转账场景
所以第一部我们叫check & try
其实就是check
我们做什么事情呢
我们现在要把这张A表小改那么一点点
我加了一个叫frozen_amount的字段
我们看名字就知道了
它是个冻结的一个金额
也就是说我现在 try & check 这个操作就是我的第一步
叫资源业务检测
或者说资源预留
注意预留
不知道大家以前去银行有没有这种预授权
然后钱要先冻结
比方我要做财产证明
证明我有30万
我可能会先做一个证明
然后他会把你这笔钱冻结了
然后你资产证明它上面有个时间戳
到时间戳结束以后
这个钱又会你还回来
会有可以不冻结就可以继续使用
为什么这样做
你不然找一个人借10万块钱
我钱够10万了
开个资产证明
完了把这个钱还给他
你怎么证明你一定有这10万块钱
所以说他自然证明就是会产生冻结的一个行为
所以这张表要加frozen_amount字段
我这个地方做try的操作其实就是第一步
钱要扣
钱也要加到哪去了呢
把你的钱冻结起来
加到我frozen_amount字段
那么cancel
跟try往往是相反的
就是回滚
回闻就是把frozen_amount的钱扣掉
让钱加回去
让你的钱就变成流动资金可以加回去
那么如果你是用Seata这个框架
它也支持TCC这种模型
那么这个模型我们可以先看一下
首先发起所有分支事务的try操作
这边是扣钱
这边其实就是加钱了
这边就是加钱
但是这个钱注意也是冻结字段
也是加到冻结字段的
就是对于余额宝来说也是冻结字段
任何一个分布式事务的操作执行失败
交给我的Seata的transaction manager
就是我的事务控制器
它会发起所有分支事务的回滚
也就是cancel操作
所以说你之前冻结的钱又把你变回来了
就这个地方你看把这个钱会滚
提供一个什么借口
把这个钱减掉
这边会滚是什么
把这个钱减掉
把这个钱加回来
如果所有的操作都成功了
那么将会发起confirm操作
confirm操作操作若执行失败
同样的它会进行重试
因为你的钱都已经冻结了
原则上来说它一定能够提交成功
所以它重试直到成功的
所以其实通过这样的一个姿势
它分成了三个阶段
然后让你的这种交易也可以变的执行下去
所以我们最终这个钱就变成
冻结的钱剪减掉这边的钱是加上去的
这边frozen是剪掉的
所以最终状态是变成下面这张表的一个那个情况
TCC幂等其实是都要注意的
我就再提交一次
幂等一定是都要考虑处理的
那么还有两个需要注意
就是空回款
如果在try失败
比方我的余额宝try失败了
当然这个失败有很多种原因
可能是网络往返的失败
那么这个时候要回滚的时候
一定要注意空回滚的问题
也有可能try里面的update操作可能都没执行
那么你在回滚的时候一定要注意要能够回款成功
这是第一个
第二个就是防悬挂
也是TCC里面提的比较多的就是说
如果因为一些原因
第一步try失败了
那么我要执行cancel
那么因为一些意外的原因
我的cancel反而先到达了
我的try因为一些网络拥堵的原因它后到达了
那么你的cancel首先执行
如果try再执行了
你不能又把它操作
因为他已经操作乱序了
那么我们其实是应该把交易彻底结束掉
那不应该让这个try继续成功
所以说叫防悬挂
大家也可以去网上
我在reference里面有更详细的一些资料
大家可以展开去看一看
就算是我们的课后的阅读
这TCC也是很常见的
但是他对我们业务来说
实现上会那么麻烦一丢丢
是有一点点麻烦的
那么最后我们刚刚抛了一个问题
我们通过消息队列也好
通过所谓的Transactional outbox
那么你是用polling来拖
polling轮询的方式也好
或者通过订阅binlog的方式也好
它都是异步的
在微服务里面其实有两种思路
一种是通过事件驱动的模型
也就说我的整个操作流程包含好几块业务子系统
那么它是通过一个时间通知下一个业务
那么这种异步的模型
有时候是不一定满足业务需求的
所以我要提出来这是第一种
那么第二种模型叫Sega
Saga这个模型也是被提到过多次的
那么Saga我在PPT里面其实没有展开讲
但是我告诉你这么一种玩法
第一种其实是通过异步事件通知
A系统通知
发卡B感兴趣订阅kafka然后去消费
那么是异步的
那么同步
有些场景我需要都操作成功
比方说我需要扣钱成功
我需要下单成功
我需要库存扣减成功
这几步可能都是必须要同步返回的
那么这种场景下
我们可以想象一下
我们是不是可以托管给一个编排服务
来编排这些流程
你可以通过一个编排服务
那么它有点像一个执行命令一样
你调一个请求到编排服务第一步会调A服务
第二步调B服务
第三步调C服务
所以这种模型叫Saga模型
它其实偏同步的一个做法
那么编排服务其实一样的
任何其中一个失败可能要考虑会滚
这个其实跟TCC某种意义上有点像
但是又不全像
但是最终你还是撸一个这样的
就是类似像一个这种多服务的编排引擎
然后要保证它们要么成功 要么失败
可能要考虑重试回滚
有可能Saga这个套路
可以用MySQL来做一个哪些服务应该被调到
那么如果都成功提交
都失败就回滚
大家可以去课后再展开来阅读一下
我今天整个分布式事务的内容就要到这里了
所以我不会把所有的case讲齐
但是我们分布式缓存和分布式事务两大块
基本上是我们业务流里面最关心的数据处理部分的一些知识了
希望能有帮助
好的
再见