分布式事务(一)

分布式事务是个复杂的问题, 针对不通的业务场景, 实现的方式也各式各样, 今天主要从下面两个方面:

  1. 为什么? 为什么需要分布式事务;
  2. 怎么做? 分布式事务如何实现;

来展开, 希望这场分享下来, 大家能对分布式事务有个基础的了解;

1. 为什么需要分布式事务?

分布式事务是伴随分布式产生的, 我们通过架构的演进来看下为什么需要分布式事务;

假设我们是一家互金公司, 刚开始的时候, 都处于一台服务器上: 有个场景是用户下单购买了基金, 这个时候需要扣账户余额, 并给用户加资产;

image.png

当所有业务都处于一个DB的Connection的时候, 可以使用单机事务来保证, 落订单成功了, 必然会给用户加上资产的;

但是随着公司的发展, 单个应用已经撑不起现有业务了, 而且随着业务的扩展, 需要进入为微服务阶段了:

image.png

这只是一个场景: 将原有的系统做了拆分, 有了三个微服务: 订单, 账户和资产, 服务间通过RPC进行信息交互; 这三个微服务共用同一个DB实例;

还是刚才用户购买基金, 但是可以看到服务拆分虽然降低了系统间的耦合, 但是也导致了原本的单机事务不可用了(这里的不可用指的是跨服务之间的写入操作, 单系统内仍然可以使用单机事务), 所以我现在其实是无法保证落订单成功必定会加资产的;

和单机事务出现的原因一样, 正因为出现了三态问题, 我们就需要引入一种机制, 来帮业务系统处理掉中间态;

2. 分布式事务如何实现

2.1 分布式常见的问题

2.1.1 部分失效问题

当一项工作需要多个节点参与时, 其中的一些节点可能会失败, 甚至你不会收到明确的结果;

2.1.2 不可靠的网络

我们的应用基本都是互联网应用, 系统间的交互都是基于网络的, 虽然平时在说网络调用时都说的是同步阻塞的, 但是我们所处的网络大部分都是异步分组网络(asynchronous packet networks); 也就是说一个节点给另一个节点发送数据时, 网络不能保证数据包什么时候到达, 甚至不能保证一定到达; 可以参考一下场景:

  1. 请求都没发出去(被拔网线);
  2. 请求阻塞了, 但是随后会发出去;
  3. 请求的远程节点可能挂了(断电或者什么原因);
  4. 远程节点收到了, 但是还没处理(比如在GC), 但是一段时间后会处理;
  5. 远程节点响应了, 但是在网络中丢了(比如交换机配置错误, 响应到另一台机器上去了);
  6. 远程节点响应了, 但是响应被阻塞了, 但是恢复后会发出;

再回过来看, 如果我们请求方没有收到响应, 是因为

  1. 请求没发出?
  2. 远程节点不可用?
  3. 远程节点挂起了?
  4. 还是远程节点响应了, 我们没收到?

其实我们是不知道远程阶段的状态的, 只是知道我们没有收到响应;

还有个词需要专门提一下, 网络分区; 简单来说就是有A, B, C三个节点, AB能正常通信, 但是AB无法与C通信, 那么就说AB与C形成了网络分区;

2.1.3 不可靠的时钟

2.1.4 拜占庭将军问题

2.2 CAP

理论计算机科学中,CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer's theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:[1][2]

  • 一致性(Consistency) (等同于所有节点访问同一份最新的数据副本)
  • 可用性Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据)
  • 分区容错性Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择[3]。)

虽然CAP看似给了三个选项, 但这里面的P其实是个必须项, 因为前面也说了, 网络是不可靠, 那么就是从C 和 A做取舍, 最终结果是CP 和 AP;

其实我觉得CAP考虑的还是比较片面的, 主要原因是P只是考虑网络异常中的网络分区, 延时节点或者死亡节点压根就没提;

2.2.1 CAP的一致性

这里指的就是线性一致性, 这个线性一致性怎么理解呢? 读写并发的时候, 只有一个读已经读取了新数据, 该读取之后的所有读取都应该读取的是新数据;

这里的线性一致性有前提 就是按时间轴, 从左至右不会不会出现回退的情况, 怎么理解:

image.png

注: 此模型不假设隔离性;

看最后一个读取 read x = 2是非法的, 因为db1已经做了CAS操作, 且最新的X值已经被db2读取到了, 那么db3就不应该还能读取到2了;

所以线性一致性通过记录所有请求和响应的时间, 再校验他们能否排列成一个时间有序的数组;

线性一致性 和 可序列化(串行化)

串行化: 指的是事务并发的时候, 一个事务必定在前一个事务完成以后才会执行; 这里事务执行的先后顺序可以和事务实际顺序不同;

线性一致性: 线性一致性保证的是读取和写入的新鲜度保证, 但是线性一致性不是事务, 所以他不会防止写入偏差;

2.3 如何实现分布式事务

2.3.1 回滚型事务: 两阶段提交(2PC)

单机事务的原子性是由数据的落盘顺序决定的: 在数据最终落盘之前, 事务都有可能被中止的;

但是分布式事务涉及到了多个节点, 那么仅向这些节点发送并独立提交事务是不可行的, 因为这样很容易违反原子性: 一些节点上提交成功了, 但是另一些节点上提交失败了; 为了解决这种情况, 必须有一个机制来协调这些节点的事务提交;

必须要注意一点是:事务的提交是不可撤销的, 原因是如果事务的提交是可撤销的, 那么该事务之后的所有写入都需要被撤销, 这个是不现实的;

接下来看下2PC是怎么保证原子提交的;

image.png

TC: 事务协调器: 主要作用就是来协调各个分支的提交与回滚的; 分布式事务的原子提交逻辑主要在TC上面;

参与者: 各个系统上的数据库;

主要流程:

  1. 当应用想要启动一个分布式事务时,它向协调者请求一个事务ID。此事务ID是全局唯一的。
  2. 应用在每个参与者上启动单节点事务,并在单节点事务上捎带上这个全局事务ID。所有的读写都是在这些单节点事务中各自完成的。如果在这个阶段出现任何问题(例如,节点崩溃或请求超时),则协调者或任何参与者都可以中止。
  3. 当应用准备提交时,协调者向所有参与者发送一个准备请求,并打上全局事务ID的标记。如果任意一个请求失败或超时,则协调者向所有参与者发送针对该事务ID的中止请求。
  4. 参与者收到准备请求时,需要确保在任意情况下都的确可以提交事务。这包括将所有事务数据写入磁盘(出现故障,电源故障,或硬盘空间不足都不能是稍后拒绝提交的理由)以及检查是否存在任何冲突或违反约束。通过向协调者回答“是”,节点承诺,只要请求,这个事务一定可以不出差错地提交。换句话说,参与者放弃了中止事务的权利,但没有实际提交。
  5. 当协调者收到所有准备请求的答复时,会就提交或中止事务作出明确的决定(只有在所有参与者投赞成票的情况下才会提交)。协调者必须把这个决定写到磁盘上的事务日志中,如果它随后就崩溃,恢复后也能知道自己所做的决定。这被称为提交点(commit point)
  6. 一旦协调者的决定落盘,提交或放弃请求会发送给所有参与者。如果这个请求失败或超时,协调者必须永远保持重试,直到成功为止。没有回头路:如果已经做出决定,不管需要多少次重试它都必须被执行。如果参与者在此期间崩溃,事务将在其恢复后提交——由于参与者投了赞成,因此恢复后它不能拒绝提交。

上面是只有TC的情况, 可以发现TC的业务其实非常重的, 那么在生产中一般会抽象出一个TM(事务管理器), TM主要是用来定义事务边界和事务编排的, 一般事务的发起者就是TM; 下面看一下具有TM的2PC是怎么做的:

image.png

2PC根据CAP来看的话, 是属于CP的, 也就是为了一致性牺牲了可用性;

2PC的问题

  1. 2PC要求所有参与者在全局事务提交之前, 各个分支事务的资源都必须锁住; 那么如果一阶段完成到全局事务提交之前, 各个参与者锁住的资源其实不可用的; 这个时候如果其中某个参与延迟特别高, 就会导致全局事务变成长事务, 其他参与者的资源长时间处于不可用的状态;
  2. 2PC的协调者如果不做高可用的话, 协调者挂了, 所有参与者都只能被动的等待协调者重启, 在这段时间内, 锁住的资源同样得不到释放;

2.3.2 补偿型事务

2.3.2.1 TCC

在回滚型事务不理想的情况下, 我们的想法是想让一阶段结束, 就让参与者释放锁; 然而因为事务是不可撤销的, 那么如果参与者出现了异常怎么处理; 我们就需要引入补偿, 二阶段的时候再起一个事务, 对一阶段的操作补偿;

补偿型事务中人气最高的是TCC, TCC是try, confirm, cancel的简写; 下面我们来看看TCC是怎么实现分布式事务的:

image.png

如果只是看图的话, 和回滚型的2PC基本没啥区别的, 但是TCC和2PC最大的区别是锁的粒度, TCC把二阶段的锁控制交给了业务层来做处理, 分布式事务随着一阶段的提交, 也就释放了锁; 因此使用TCC需要对业务做改造, 下面基于基金的购买来说明下如何做业务改造;

首先看account的改造:

假设原本的表结构是:

account_no user_id amount
账户 用户ID 可用金额

那么需要在该表中引入一个中间态的金额: 冻结金额

account_no user_id amount frozen_amount
账户 用户ID 可用金额 冻结金额

同样position中也需要引入两个中间态: 一个是购买中的金额, 另一个是赎回中的金额;

position_id user_id capital buying_capital taking_capital
资产ID 用户ID 持有中的资产 购买中的资产 赎回中的资产

表结构改造完成以后, 结合图片来看下TCC是具体怎么执行的;

a. TM会开启全局事务, 获取全局事务ID;

b. TM调用各个分支事务的分支事务的try方法:

order_try: insert order status = 0(初始化状态, 表示处理中);

account_try: update account set amount = amount -100, frozen_amount = frozen_amount + 100;

position_try: update position set buying_capital = buying_capital + 100;

c. TM 等待各个分支事务的ACK;

d. 如果各个分支事务try的ACK都是OK, 那么就需要发送commit指令给TC做全局事务的提交;

e. TC调用分支事务的confirm方法:

order_confirm: update order set status = 1(成功) where id = 1;

account_confirm: udpate account set frozen_amount = frozen_amount - 100;

position_confirm: update position set capital = capital + 100, buying_capital = buying_capital - 100;

f. 如果其中有个分支事务的ACK是失败或者超时了, 那么就需要调用cancel做补偿:

g. TC调用分支事务的cancel做补偿:

order_cancel: update order set status = -1(失败) where id = 1;

account_cancel: update account set amount = amount + 100, frozen_amount = frozen_amount + 100;

position_cancel: update position set buying_amount = buying_amount - 100;

h. 如果在二阶段(confirm | cancel)TC没有接收到分支事务二阶段的ACK, 是需要再次调用confirm | cancel方法的, 所以TCC中的confirm | cancel方法一定要是幂等的;

TCC存在的问题

虽然TCC通过将一个全局事务拆分为两个小事务, 一阶段 和 二阶段的事务, 灵活性和可用性得到了保证; 但是刚才的讲解中也可以看到, TCC对业务的侵入性很高:

  1. 需要提供三个接口, try, confirm, cancel;
  2. 需要对现有业务做改造, 需要处理一阶段造成的中间态数据;
  3. 如果是不可控的第三方业务, TCC是无法做的(不可能去推动第三方做TCC改造);

那么又没有即不需要做业务改造, 又能避免2PC的性能问题的呢? SAGA可能是一个解决方案;

2.3.2.2 SAGA

SAGA的解决方案其实和TCC挺相似的, 也是将长事务拆分; 但是SAGA的一阶段不是预留资源, 而是和2PC一样直接修改并提交; 如果在某个分支事务执行出错了, 需要按顺序回滚之前的所有分支事务, 按顺序怎么理解呢? 还是刚才的例子:

order -> account -> position; 结果在position加资产的时候出异常了, 那么SAGA会进行回滚, 顺序是:

account -> order; 也就是回滚的顺序是倒序;

虽然SAGA相对TCC省去了业务改造, 并且也省去了prepare阶段的资源占有; 但是这种直接操作的结果是对其他业务线程可见的, 就会导致充值场景下, 一阶段给用户加了资金, 但是二阶段需要回滚, 但是用户已经消费了, 导致回滚失败的情况;

2.3.3 通知型事务

通知型事务的产生是为了解决2PC的性能问题, TCC的业务侵入性, 将事务看成消息, 由消费者来保证最终一致性;

通知型事务又可以分为可靠通知和最大努力通知;

2.3.3.1 最大努力通知

最大努力通知有个前提就是服务存在上下游关系, 上游(provider)成功了, 通知的消费者必须保证消费成功, 如果消费失败会重试机制; 如果还是重试失败, 上游必须提供个接口给下游查询事务成功后数据;

这个场景比较常用的是跨系统间的信息交互, 比如支付网关和业务系统; 支付网关支付成功后会通知业务系统, 业务系统消费失败达到次数限制后会反查支付网管的数据;

2.3.3.2 可靠通知

可靠通知可以看成是支持回滚的最大努力通知, 在达到重试次数后, 允许下游执行事务回滚; 这样就没有了消费者必须成功的约束了;

我们回看分布式事务下的ACID保证,原子性(Atomicity)和持久性(Durability)与传统事务无异,但一致性(Consistency)与隔离性(Isolation)上除了2PC完全满足外, 其他的补偿型与通知型事务都有或多或少的缺失,它们都强调最终一致性,即允许在一段可接受的时间内各节点数据不一致,由于它们多半是将大事务分解成一个个本地小事务,所以在一段时间也存在隔离性问题;

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