分布式事务实战方案汇总
分布式事务:
1 CAP 定理
1.1 概念
CAP 理论在分布式系统中
- 一致性:分布式环境下多个节点的数据是否强一致
- 可用性:分布式服务能一直保证可用状态。当用户发出一个请求后,服务能在有限时间内返回结果
- 分区容忍性:特指对网络分区的容忍性
对于共享数据系统,最多只能同时拥有CAP其中的两个,没法三者兼顾。
- 任两者的组合都有其适用场景
- 真实系统应当是ACID与BASE的混合体
- 不同类型的业务可以也应当区别对待
其中,分区容忍性又是不可或缺的。
![](http://upload-images.jianshu.io/upload_images/9727275-8965837b54ff1678.png?imageMogr2/auto-orient/strip|imageView2/2/w/1114/format/webp)
结论:分布式系统中,最重要的是满足业务需求,而不是追求抽象、绝对的系统特性。
2.2 中间件实例
-
默认优先选择AP,弱化C
Cassandra、Dynamo 等 -
默认优先选择CP,弱化A
HBase、MongoDB 等
2 BASE 理论
核心思想
基本可用(BasicallyAvailable)
分布式系统在出现故障时,允许损失部分的可用性来保证核心可用。
软状态(SoftState)
允许分布式系统存在中间状态,该中间状态不会影响到系统的整体可用性。
最终一致性(EventualConsistency)
分布式系统中的所有副本数据经过一定时间后,最终能够达到一致的状态
一致性模型数据的一致性模型可以分成以下 3 类:
- 强一致性
数据更新成功后,任意时刻所有副本中的数据都是一致的,一般采用同步方式实现 - 弱一致性
数据更新成功后,系统不承诺立即可以读到最新写入的值,也不承诺具体多久后可读到 - 最终一致性:弱一致性的一种形式,数据更新成功后,系统不承诺立即可以返回最新写入的值,但是保证最终会返回上一次更新操作的值。
分布式系统数据的强一致性、弱一致性和最终一致性可以通过Quorum NRW算法分析。
只要聊到做了分布式系统,必问分布式事务,若你对分布式事务一无所知的话,确实很坑,起码得知道有哪些方案,一般怎么来做,每个方案的优缺点是什么。
现在面试,分布式系统成了标配,而分布式系统带来的分布式事务也成了标配.
你做系统肯定要用事务,那你用事务的话,分布式系统之后肯定要用分布式事务.
先不说你搞过没有,起码你得明白有哪几种方案,每种方案可能有啥坑?比如TCC方案的网络问题、XA方案的一致性问题
-
单块系统里的事务
-
分布式系统里的事务
分布式事务的几种解决方案
● 异步校对数据的方式
支付宝、微信支付主动查询支付状态、对账单的形式;
● 基于可靠消息(MQ)的解决方案
异步场景;通用性较强;拓展性较高
● TCC编程式解决方案
严选、阿里、蚂蚁金服自己封装的DTX
3 XA方案
即两阶段提交事务方案。
需要数据库厂商支持; JAVA组件有atomikos等。
3.1 程序理解
有一个事务管理器,负责协调多个数据库(资源管理器)的事务
- 事务管理器先问各个数据库预提交 ok 吗?
- 如果每个数据库都回复ok,即预提交成功,就正式提交事务,在各个数据库开始执行操作,这里失败会有失败异常重试,日志分析,人工重试。
3.3 适用场景
较适合单块应用中,跨多库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,不适合高并发场景。
如果要玩,那么基于Spring + JTA就可以搞定。
互联网公司基本都不用,因为某个系统内部如果出现跨多库的操作,是不合规的!现在的微服务,一个大的系统分成几十甚至上百个服务。一般规约每个服务只能操作自己对应的一个数据库。
如果你要操作别的服务对应的库,不允许直连别的服务的库。
要操作别人的服务的库,必须通过调用别的服务的接口
4 TCC方案
4.1 三阶段
- Try
对各个服务的资源做检测,对资源进行提前锁定或者预留 - Confirm
在各个服务中执行实际的操作 - Cancel
如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,即执行已操作成功的业务逻辑的回滚操作
4.2 跨行转账案例
涉及到两个银行的分布式事务,如果用TCC方案来实现,思路是这样的:
- Try阶段
先把两个银行账户中的资金给它冻结住,不让操作了 - Confirm阶段
执行实际的转账操作,A银行账户的资金扣减,B银行账户的资金增加 - Cancel阶段
如果任何一个银行的操作执行失败,那么就需要回滚进行补偿
比如A银行账户如果已经扣减了,但是B银行账户资金增加失败了,那么就得把A银行账户资金给加回去
该方案说实话几乎很少使用,但也有使用场景.
因为这个事务的回滚实际上严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常恶心!
比如说我们,一般来说和钱相关的支付、交易等相关的场景,我们会用TCC,严格严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性!
4.3 适用场景
除非你是真的一致性要求太高,是系统中核心之核心的场景!
常见的就是资金类的场景,那可以用TCC方案,自己编写大量的业务逻辑,自己判断一个事务中的各个环节是否ok,不ok就执行补偿/回滚代码
而且最好是你的各个业务执行的时间都比较短
但是说实话,一般尽量别这么搞,自己手写回滚逻辑,或者是补偿逻辑,实在太恶心了,业务代码也很难维护
4.4 方案示意图
![](http://upload-images.jianshu.io/upload_images/9727275-e4d9d158e62e9673.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)
5 本地消息表
ebay搞出来的这么一套思想
5.1 简介
- A系统在本地一个事务里操作的同时,插入一条数据到消息表
- 接着A系统将这个消息发送到MQ
- B系统接收到消息后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息
- B系统执行成功后,就会更新自己本地消息表的状态以及A系统消息表的状态
- 如果B系统处理失败,那么就不会更新消息表状态,那么此时A系统会定时扫描自己的消息表,如果有未处理的消息,会再次发送到MQ中去,让B再处理
5.2 优点
这个方案保证了最终一致性
哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止
5.3 缺陷
最大的问题就在于严重依赖于数据库的消息表来管理事务,这个会导致高并发场景无力,难以扩展呢,一般确实很少用
-
本地消息表方案
6 可靠消息最终一致性方案
干脆不用本地的消息表了,直接基于MQ来实现事务。比如阿里的RocketMQ就支持消息事务!
6.1 简介
- A系统先发送一个prepared消息到MQ,如果这个prepared消息发送失败,那么就直接取消操作,不执行了
- 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉MQ发送确认消息,如果失败就告诉MQ回滚消息
- 如果发送了确认消息,那么此时B系统会接收到确认消息,然后执行本地的事务
- MQ会自动定时轮询所有prepared消息回调你的接口,问你这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?
这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,别确认消息发送失败了。 - 如果系统B的事务失败了咋办?
重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如B系统本地回滚后,想办法通知系统A也回滚;或者是发送报警由人工来手工回滚和补偿
这个还是比较合适的,目前国内互联网公司大都是这么玩的,要不你举用RocketMQ支持的,要不你就自己基于类似ActiveMQ?RabbitMQ?自己封装一套类似的逻辑出来,总之思路就是这样子的
可靠消息最终一致性方案
![](http://upload-images.jianshu.io/upload_images/9727275-a65646f638c3dffd.png?imageMogr2/auto-orient/strip|imageView2/2/w/1031/format/webp)
7 最大努力通知方案
7.1 简介
-
系统A本地事务执行完后,发送一个消息到MQ
-
有一专门消费MQ的最大努力通知服务,会消费MQ,然后写入数据库中记录下来,亦可是放入内存队列,接着调用系统B的接口
-
若系统B执行成功就ok;若系统B执行失败,那么最大努力通知服务就定时尝试重新调用系统B,反复N次,最后还是不行才放弃
最大努力通知方案示意图
实战方案:
1. 最终一致性
1.1 本地事务表 + 轮询补偿
交互流程
- ① commit DB事务提交阶段 本地客户端向DB进行事务提交,此时需要将业务数据和记录消息事务状态的信息表同时实现本地事务,此时标记消息事务状态为UN_SEND未发送或未完成状态,此时MQ未发送
- ② ack DB确认阶段 返回DB事务提交成功或失败状态
- ③ commit MQ事务提交阶段 客户端发起MQ发送请求
- ④ update 本地事务表更新阶段 根据MQ发送结果进行本地消息事务表状态更新,成功则更新为SEND发送成功或发送完毕
- ⑤ MQ补偿 本地消息事务表定时轮询 对未发送成功消息事务进行补偿发送,实现分布式事务的最终一致
场景:重构业务新老系统双写库同步
项目背景
这是一个重构系统新老系统同时服役切量迁移的业务场景,老系统正在线上服役为各业务方提供接口查询功能,新系统重构完成后需要对接接入,调用流量要陆续从老系统切换到新系统,最终老系统迭代下线。
分布式事务
需要解决的分布式事务问题就是,双系统的数据是异构、分散的,线上不可停量,需要陆续切换完成,因此需要事先将老库存量数据洗入新库,此过程中增量数据写入是存在的,但是最终新老库是相对一致和统一的,该场景需要解决的是数据双库的双写问题。
设计方案
场景Q&A
- Q:如何保证双库双写? A: 同步写辛库,MQ异步写老库 本地事务 + 消息事务表
业务数据持久化开启数据库本地事务,该事务中记录业务数据和同步状态信息 确保本地事务一定成功,不保证异步MQ事务
数据库事务成功后再发送写老库的MQ,保证本地事务一定完成才会触发MQ发送,这样确保本地事务一定成功,MQ可能成功也可能失败 重试MQ事务状态,最终一致
如果MQ事务失败,通过定时任务轮询进行重发驱动,最终一致 - Q:异步MQ写,延迟问题如何解决? A: 增加冗余查询
增加冗余对另一库的冗余查询进行Double Check。由于调用方几乎不可能同时使用新老系统做业务,因此延迟时间取决于MQ异步消费写的速度,如果场景比较复杂要确保绝对一致可以增加该处理方式 - Q:双写过程中多个MQ如何保证顺序、防重等问题? A: 业务时间 + 业务ID (1)同一个业务ID代表一个同一笔业务,可以依此进行业务防重处理 消息业务ID业务时间消费时间处理逻辑IDTT执行IDTT+1防重不处理 (2)同一个业务ID的基础上增加业务时间,可以依此保证业务数据的实时刷新 业务时间、消费时间同序 消息业务ID业务时间消费时间处理逻辑IDTT执行IDT+1T+1执行(覆盖) 业务时间、消费时间乱序 消息业务ID业务时间消费时间处理逻辑IDTT+1抛弃(业务时间较早)IDT+1T执行 (3)不同业务ID的基础上增加业务时间,可以依此保证不同业务数据的按照业务时间刷新 消息业务ID业务时间消费时间处理逻辑IDT+1T+1执行(业务时间较新)ID+XTT抛弃(业务时间较早)
场景:第三方认证核验
项目背景
这是一个认证系统以来外部核验系统进行用户身份鉴权的场景,即认证系统记录认证发起记录,并请求到外部的核验系统发起一笔核验请求,用户在核验系统确认后核验结果返回到认证系统确认用户的真实数据状态。
分布式事务
该流程中认证系统是一个本地系统,存放用户发起的认证流水信息和核验状态,依赖外部核验系统返回该笔认证记录的核验状态,由于核验过程是异步的,用户可以选择任意时间完成或者永远不完成,需要保证每次认证流程只有一笔业务发起,而且需要根据业务时间进行核验流程的超时进行强制取消或者补偿查询对齐核验状态等,需要解决的分布式事务是认证流水、核验结果的一致性。
设计方案
- 正向流程
![](http://upload-images.jianshu.io/upload_images/20063139-3468b4f321129cb2.png?imageMogr2/auto-orient/strip|imageView2/2/w/1009/format/webp)
- 补偿流程
场景Q&A
- Q:本地事务认证流水成功了,外部核验系统提交失败了怎么办? A:通过定时任务补偿触发二次提交,只要外部事物提交一直处于未成功,便一直会被重试提交,直到成功
- Q:外部核验系统事务完成了,本地事务认证流水提前被作废了怎么办? A:以本地事务认证流水的结果为准。本地事务是通过定时任务进行补充提交+外部事务状态核验查询的,即时在临界点外部事务完成了,但是超过了业务处理时间已经关闭,不会再补充修改,这也是根据业务场景做的取舍,用户可以再次发起新流程进行核验
1.2 本地事务表 + 事务消息
交互流程
- ① prepare 准备阶段 本地客户端向DB、MQ发送prepare请求
- ② ack 准备确认阶段 DB、MQ作为事务参与者返回本地客户端ack确认
- ③ commit/rollback 提交/回滚阶段 根据事务参与者在准备确认阶段返回结果进行事务提交或回滚,此时一旦有事务参与者返回异常或超时未返回则进行回滚提交
- ④ callback 补偿回调阶段 当事务超时提交时,则由MQ进行回调查询数据库本地事务情况
- ⑤ commit/rollback 提交/回滚阶段 根据补偿回调阶段进行事务提交和回滚,实现分布式事务的最终一致性
场景:分库分表路由字段绑定
项目背景
该业务是在分库分表场景下,需要一个切分键字段进行数据分片路由,一般我们ToC业务的话会使用userId这样的字段进行切分。然而水平切分数据带来了数据库读写性能的同时也存在一个问题,那就是查询必须携带切分键才可以完成,因为要依赖它进行数据路由查询,比如在以userId进行数据路由切分时,如果想按照用户的身份证、姓名等进行匹配查询是无法实现的,因为我们事先是不知道userId、身份证、姓名的等值匹配关系。一般而言,我们可以通过HBase + ES的方案进行解决,即监听不同库的binlog日志,将其按照时间进行排序切分汇入HBase表中,加入要检索的索引到ES中解决分库分表下数据切片产生的聚合问题。
分布式事务
基于以上场景,除了通过HBase+ES实现之外,还可以通过切分键与非切分键进行数据绑定解决,但是由于切分键与非切分键的路由一般不一致,数据会分散到不同库,因此无法做本地事务,这是我们需要解决的分布式事务问题。
设计方案
- 交互流程
场景Q&A
- Q:异步MQ写有延迟,查不到切分键如何处理? A:事务处理开始阶段通过Redis记录事务开启的分布式锁标识,当其他存在冲突的同业务在该事务的处理过程有查询时,通过判断分布式锁标识是否存在来判断事务的开启,若存在则异步等待并发起间隔查询直到事务超时,若事务超时周期内反复查询到则返回,否则根据事务处理后结果返回
- Q:本地事务与MQ事务是如何协作的? A:相当于两个2PC事务协作。 1.一阶段DB、MQ同时prepare(并行) 2.二阶段DB先commit,成功后MQ再commit(串行) 流程阶段操作Ack反馈持久化是否结束分布式事务成功正向流程PrepareDB prepare MQ prepareYesNoNoNo正向流程CommitDB commit MQ commitYesYesYesYes------异常流程PrepareDB 或 MQ 异常NoNoYesNo异常流程CommitDB提交异常NoNoYesNo异常流程CommitMQ提交超时 回调本地事务状态,本地事务成功则提交MQ事务YesYesYesYes异常流程CommitMQ提交超时 回调本地事务状态,本地事务失败则取消MQ事务YesNoYesNo
- Q:会不会存在MQ事务成功,本地事务失败? A:不会,而且要避免这种情况发生。两个2PC事务的prepare准备阶段可以同时发起,但在commit提交阶段要先保证本地数据库事务成功之后再进行MQ事务消息的commit,也就是在commit阶段是存在依赖关系、串行化之行的
- Q:事务消息如何确保最终一致? A:prepare阶段失败、本地事务commit阶段则均不会持久化;当prepare阶段成功、本地事务commit成功,此时MQ的commit阶段异常,则依赖MQ事务消息的超时commit机制触发回调本地事务状态接口方法来决定MQ的二阶段是commit还是cancel
1.3 TCC(Try-Commit-Cancel)
交互流程
TCC事务其实主要包含两个阶段:Try阶段、Confirm/Cancel阶段。
- ① try-尝试执行业务
完成所有业务检查(一致性)预留必须业务资源(准隔离性) - ② confirm-确认执行业务
真正执行业务不作任何业务检查只使用Try阶段预留的业务资源Confirm操作必须保证幂等性 - ③ cancel-取消执行业务
释放Try阶段预留的业务资源Cancel操作必须保证幂等性
从TCC的逻辑模型上我们可以看到,TCC的核心思想是,Try阶段检查并预留资源,确保在Confirm阶段有资源可用,这样可以最大程度的确保Confirm阶段能够执行成功。这里的资源可以是DB,或者MQ,RPC,只要是分布式环境中的事务载体就可以,而且需要这些分布式事务的参与者具备正逆向条件,比如DB、MQ的事务可以支持2PC,可以根据协调者对其进行事务提交或者取消操作,RPC比如账户服务可以支持正向增加也可以支持逆向减少,除此之外,DB、MQ要自身支持事务的ACID,这是参与分布式事务的原子性保证,RPC底层也是DB,这里可以等同理解。以上参与者具备参与分布式事务的基本条件后便可以进行整合和使用,业务流程的驱动和事务的完整性由中间协调者来操作和推进。
场景:积分商品兑换
项目背景
这是一个电商系统比较经典的下单、扣款、发货流程,在下单之前会通过一系列商品在架状态、库存数量、购买限制等有效性过滤,然后在业务系统中进行一个商品兑换的场景。
分布式事务
由于是商品兑换,对于用户和系统本身来说是这个过程一个原子性的、一键完成的操作,因此下单+动账+发货是一个现实存在的分布式事务。这里假设订单数据和动账数据在一个本地数据库事务中,持久化开启数据库本地事务,该事务中记录订单生成数据和动账数据信息,以及发货状态的信息记录。这里需要解决的分布式事务是订单数据、动账数据、发货状态三种的最终一致。
设计方案
![](http://upload-images.jianshu.io/upload_images/20063139-d5b23f73a57ec670.png?imageMogr2/auto-orient/strip|imageView2/2/w/891/format/webp)
- 正向流程
- 补偿流程
![](http://upload-images.jianshu.io/upload_images/20063139-bc6a94f81698da3c.png?imageMogr2/auto-orient/strip|imageView2/2/w/1061/format/webp)
场景Q&A
- Q:动账扣款成功,但是发货失败怎么办? A: 定时任务根据发货状态进行发货流程驱动 定时任务补偿再次发货
发货成功则分布式事务最终一致,下游发货系统进行发货防重处理 发货失败进入逆向流程
定时任务驱动发货最终一致理论上可以一直进行,但是发货可能有一个流程时效和库存售罄的问题,可以根据业务场景进行配置比如2天内重复调用失败或调用返回无库存则进入逆向关单退款流程使得分布式事务恢复成最开始的一致性状态 - Q:逆向流程回滚账户扣款,怎么防重? A:账户流水表做唯一索引正逆向类型 + 业务ID,和账户额度表进行本地事务操作,确保只能成功一笔正向/逆向业务操作
场景:广告任务结算
![](http://upload-images.jianshu.io/upload_images/20063139-67ef79ffb55b6f8e.png?imageMogr2/auto-orient/strip|imageView2/2/w/1151/format/webp)
项目背景
当一个App有了足够多的用户体量,便开始有商家进行广告或商品的投放,平台通过包装运营活动、广告位等,将用户曝光与商家付费结合达到流量变现的目的。
分布式事务
当用户进行浏览、关注、商品购买、视频观看、App下载等多种任务,我们会根据商家配置等付费规则进行商家广告费用的扣减,同时还要为用户完成任务进行奖励结算,此时的分布式事务便是商家账户扣减与用户账户增加的数据一致性问题
设计方案
- 交互流程
- 补偿流程
场景Q&A
- Q:商户扣款失败或者商户扣款成功,用户结算失败怎么办? A: 定时任务根据结算状态进行结算流程驱动,会一直轮询补偿尝试结算用户,直到成功。
- Q:商户扣款成功,用户结算失败为何不进行做业务超时的逆向流程回滚商家扣款? A: 用户做任务时一定是做了前置校验进行平台任务发放的,也就是说用户任务只要完成必须要进行结算,这是一个不能逆向的流程。即使极端情况下商户余额空了暂时无法结算到用户也要一直重试,一旦商户余额充足则继续整个结算流程。
场景:运营活动抽奖
项目背景
抽奖是运营活动中比较常见的方式,对于用户来说需要做的是参加设定人物获取积分,攒够积分就可以开始抽奖,抽中后即等待奖品入账,一般券会立刻入账使用,而实物商品则需要耐心等待物流送到用户手上。
分布式事务
关于抽奖,涉及账户动账、库存参与次数扣减、抽奖等多个环节,该场景要解决的分布式事务是账户动账、活动库存变更、抽奖下单数据一致性。
设计方案
![](http://upload-images.jianshu.io/upload_images/20063139-3ef257debb55a14f.png?imageMogr2/auto-orient/strip|imageView2/2/w/1128/format/webp)
- 交互流程
- 补偿流程
场景Q&A
- Q:动账扣减失败,补偿流程为何不驱动扣减完成抽奖? A: 抽奖业务对于用户来说实时性要求很高,正向流程没有完成的话,没有通过补偿流程驱动的必要性了,用户体验不好容易产生问题,补偿流程只针对账户扣减成功扣没有顺利完成抽奖的情况进行账户补款即可。而且这部分补款是有延迟的,在C端展示可以优化或者忽略合并掉,保证的是账户额度无损。
2. 弱一致性
2.1 最大努力通知 + 消息重试控制
场景:数据变更同步下游业务方
项目背景
系统数据发生变更时,会对外部系统产生一定影响,外部系统需要知道这种数据变化,这便是数据状态同步的场景。一般来说数据交互可以有推(Push)、拉(Pull) 两种形式,这里先说推模式,即数据变更方负责将变化通知到数据关注方。
分布式事务
这里要保证的是数据变更在多个应用中的状态一致。
设计方案
- 交互流程
![](http://upload-images.jianshu.io/upload_images/20063139-f6294baf145fb8b3.png?imageMogr2/auto-orient/strip|imageView2/2/w/776/format/webp)
场景Q&A
这里是弱一致性的实现,没有做本地事务表和定时任务轮询对比各事务状态进行补偿操作。完全依赖于MQ的失败重试驱动,若RPC调用失败即数据同步业务方失败,MQ会一直进行重试操作,随着重试次数增加,重试间隔也会增加,这里也可以业务自行进行最大努力尝试次数的控制,超过多少次尝试仍失败则放弃,因此不能保证最终一致。
场景:数据变更广播下游业务方
![](http://upload-images.jianshu.io/upload_images/20063139-c3d379c0806e443a.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)
项目背景
这里是数据同步的说拉模式,即数据关注方对数据变更方进行数据状态变更的监听,这种处理方式处理的主动权在于数据关注方,数据变更方只负责和保证一定通知到数据变更情况,是否能够同步成功则由监听方处理和兼容。
分布式事务
这里要保证的是数据变更在多个应用中的状态一致。
设计方案
![](http://upload-images.jianshu.io/upload_images/20063139-6b2b2daed14fa728.png?imageMogr2/auto-orient/strip|imageView2/2/w/826/format/webp)
- 交互流程
场景Q&A
这里也是弱一致性的实现,没有做本地事务表和定时任务轮询对比各事务状态进行补偿操作。完全依赖于MQ消费方的处理,若消费方处理失败或在消息队列规定时间内没有消费完毕,则数据无法保证最终一致。
2.2 战略放弃 + 报警 + 人工修复
场景:秒杀库存回滚
项目背景
在秒杀场景中,最复杂的除了解决高并发问题外,最核心的就是活动商品的库存控制、变更问题,一般商品库存会初始化到Redis缓存中进行管理,秒杀方法会对Redis缓存库存数量进行校验、扣减操作,通过MQ异步扣减DB库存,既利用Reids原子操作进行库存数量操作,又利用缓存抗住高并发请求,起到异步削峰的作用,这是秒杀的正向流程。而逆向流程是用户秒杀到商品预占了库存,但是没有及时进行订单支付或者进行了订单取消,此时要发起对库存的恢复操作。
分布式事务
这里的分布式事务是Redis缓存库存与DB库存数量一致性问题。
设计方案
![](http://upload-images.jianshu.io/upload_images/20063139-d16b19ea0e2e6eb6.png?imageMogr2/auto-orient/strip|imageView2/2/w/572/format/webp)
- 交互流程
![](http://upload-images.jianshu.io/upload_images/20063139-07ecf7e5163514fc.png?imageMogr2/auto-orient/strip|imageView2/2/w/796/format/webp)
场景Q&A
- Q:秒杀场景会出现哪些分布式问题? A: 根据如上流程图,扣减缓存库存、创建订单、异步MQ发送是在一个同步的函数方法中的三个非原子的子步骤,而秒杀场景下流量洪峰会一瞬间打满线程,以上三个子步骤任何异步都会出现问题,因为都是先扣缓存库存数量,根据实践经验看,极端情况下会出现扣减缓存库存成功了,后面创建订单失败了或者异步MQ没有发出来无法削减DB库存数量,因此数据结果是缓存库存扣减的多,DB扣减的少,实际抢购卖出的少,换句话说就是出现了少卖的现象。
- Q:会不会出现超卖现象? A: 不会。依赖于Redis单线程命令执行的保证。这里需要注意的是读、写命令不是一致,可以结合分布式锁实现,也可以通过Lua脚本实现命令的原子性执行。
这里也是一个弱一致性的实现,业务场景我们保证不超卖即可,对于极端情况出现的库存数量无效多扣减做战略性放弃,一般情况下不会影响大多业务使用,如果非要吹毛求疵达到账户金额那种强一致性,思路也很简单,可以借助定时任务轮询对比缓存与DB库存数量进行校验,这里还要考虑到其他在行流程如超市关单库存恢复,仍然在行的秒杀活动等,保证数据处理不多加不多减。
3. 总结
3.1 分布式角色
- 参与者
可以通俗的认为是DB、RPC、MQ这些能够提供事务能力的中间件或接口服务 - 协调者
维系分布式事务各个参与者分布式状态的系统、中间件,如Zookeeper、业务系统
3.2 技术保证
- 数据库事务 数据库如MYSQL提供了2PC、XA协议,依赖于WAL + Redo Log + 刷盘策略保证
- MQ事务 提供了2PC协议,依赖于Ack机制+刷盘策略保证
3.2 强弱一致选择
- 强一致性
强一致性确保的不是事务一定成功,而是事务参与者的子事务要么全成功,要么全失败,保证子事务的最终一致。一般依赖于定时任务、补偿机制、Double Check等方式进行事务状态的校准和协调,一般设计和实现的复杂度大,参与者越多,流程越复杂,越难以维护,最终一致的延迟性和可靠性保证越难 - 弱一致性
弱一致性放弃了最终一致性的保证,通过最大努力实现而不保证最终的结果,这种场景减少和减低了开发和设计的复杂度
3.3 幂等&防重
- 业务幂等通常会定义bizId代表全局唯一的业务标识。在MQ重发、重复消费、乱序,RPC重复调用等场景进行业务防重兼容处理。
- 如账户余额的强一致防重处理,可以结合流水表唯一索引正逆向类型 + 业务ID进行拦截
- 一般大多依赖于数据库的唯一索引进行防重保证,如果担心数据库性能问题,可以前置缓存拦截处理
3.4 尽早干预&补偿一致
- 尽早干预
指的是代码逻辑上尽早对串行处理的做个子事务进行回滚或逆向操作,这样可以尽快结束分布式事务,而不需要等待相对更为延迟的定时任务或其他补偿机制来驱动,这里可以使用旁路方法或不阻塞主方法放到MQ或异步线程中进行处理,比如秒杀下单发货因为库存不足或商品下架可以立刻进行发起关单退款的逆向流程
- 补偿一致
补偿机制一般可以通过定时任务、MQ重试来进行子事务驱动整个分布式事务的完结