从一次小哥哥与小姐姐的转账开始, 浅谈分布式事务从理论到实践

2020 年 3 月 30 日

从一次小哥哥与小姐姐的转账开始, 浅谈分布式事务从理论到实践

分布式事务是个业界难题,在看分布式事务方案之前,先从单机数据库事务开始看起。


什么是事务


事务(Transaction)是数据库系统中一系列操作的一个逻辑单元,所有操作要么全部成功要么全部失败。


可以看一个经典的转账事务示例,小哥哥转账 100 元给小姐姐:


begin操作1:查询小哥哥账号余额,确保余额充足操作2:从小哥哥账号扣除100操作3:往小姐姐账号增加100commit;
复制代码


转账的一系列操作就是一个事务,事务会确保这一系列操作要么全部成功,要么全部失败。


ACID


谈起事务就不得不谈事务的四大特性 ACID


  • 原子性(Atomicity)

  • 一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。


还拿之前的转账案例来理解,原子性就是要求转账的一系列操作(操作 1、操作 2、操作 3)要么全部完成,要么全部失败。不能出现钱转了一半的情况,比如小哥哥的账号钱扣除成功了,但是小姐姐账号加钱的操作失败了,这种属于不满足原子性。


  • 一致性(Consistency)

  • 一致性这个词总是一个让人困惑话题,有时不同语境下说的都不是同一个事情。我们先看看维基百科定义:

  • Consistency ensures that a transaction can only bring the database from one valid state to another, maintaining database invariants: any data written to the database must be valid according to all defined rules, including constraints, cascades, triggers, and any combination thereof.


我们看看关键点,事务确保数据从一个 valid 状态转换到另外一个 valid 状态。什么样才算是 valid 呢,符合 all defined rules。all defined rules 包括了 constraints、 cascades、triggers 等。所以这里的一致性强调的是事务操作使得数据一直处于符合预定规则(约束、触发器等)。This prevents database corruption by an illegal transaction, but does not guarantee that a transaction is correct。但是我们通常讨论的一致性的含义往往比较广,并不局限于 ACID 的 C。


看看转账案例,怎么样才算符合一致性呢,账户余额不能为负数可以算,而两者账户余额相加=200 则属于应用语义层面的一致性,由原子性来保证。


  • 隔离性(Isolation)

  • 事务通常是并发执行的,同时对数据进行读写和修改的,隔离性强调的多个事务并发执行对数据的影响看起来跟串行一样。通常为了提高并发度,弱化了事务并发时对数据一致性的要求,允许若干种数据异常现象,从而定义了不同的事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。严格来说只有最高隔离的串行化隔离级别才是符合隔离性的。


在转账案例中,如果在转账事务执行过程中,能读取到事务中间状态,比如转了一半然后出错事务进行了回滚,读到了“转一半”的不一致的数据状态,属于脏读。为了提高并发度,在最低的读未提交隔离级别是允许这种脏读,其他几种不会出现此种脏读。


  • 持久性(Durability)

  • 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。持久性其实好理解,事务做完了要保证持久不丢失,比如转账已经转成功了,不能这笔事务丢失掉。


从某种意义来说,ACID 都是为了保障数据的一致性,不满足 ACID 则会有数据的不一致。


分布式事务


互联网时代,业务发展迅猛,数据往往超出单机数据库所能处理的极限,遇到性能的瓶颈。应用层面微服务架构越来越流行,从原来的单体应用拆分成一个个独立的微服务,当应用通过一组微服务来协助完成时,对数据的一致性就需要分布式事务来保证。


对数据库通常采用垂直拆分和水平数据分片,将数据拆分到多个不同的数据节点上。如果一个事务里的操作涉及了多个不同分片节点则产生了分布式事务。


我们来看看业界常见的几种分布式事务实现:


  • 基于XA协议的2pc

  • 两阶段提交(2pc)大概属于被提的最多的分布式事务实现方案了。XA协议是 X/Open DTP Group提出的定义的两段提交(2PC - Two-Phase-Commit)协议,主要用于分布式数据库事务管理。XA规范主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。


两阶段提交将提交过程分为两个阶段,在第一阶段,协调者询问所有的参与者是否可以提交事务(请参与者投票),所有参与者向协调者投票。在第二阶段,协调者根据所有参与者的投票结果做出是否事务可以全局提交的决定,并通知所有的参与者执行该决定。



2PC 的缺点


2PC 虽然保证了提交的原子性,但缺点也很明显,先从协议本身来看看两阶段提交的缺点:


1、同步阻塞。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有资源时,其他第三方节点访问资源不得不处于阻塞状态。


2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。


从性能来看,2PC 中协调者与每个参与者至少有 2 轮消息交互、多次写日志,过程又是同步阻塞,性能十分低下。


  • 2PC是强一致吗

  • 经常听到一些人说2PC是强一致的,真的是吗?我们先从隔离性的角度来看看,比如有两个节点a和b,一个事务已经到第二阶段准备提交,在某个时间点上,a节点已经提交,但b节点还未提交,这时另一个事务就能看到a节点提交后的值以及b节点提交前的值。


还以之前转账为例,转入分支事务已经成功,转出分支事务还未提交成功,这个时候就看到不一致,两个账号总额度是 300,属于脏读,能看到不一致还能算强一致么。


从隔离性角度来说,2PC 的分布式事务只能算最终一致,算不得强一致。一般人说的强一致只是说的原子性,事务要么全成功要么全失败。所以一致性这个词已经被玩坏了。


  • MySQL对XA支持的坑

  • 另外我们可以看看MySQL对XA的支持,在MySQL5.7之前一直有2个缺陷多年未修复。


  1. prepare未写入binlog,若主库宕机切换后则丢失prepare。

  2. 客户端退出或者服务宕机,MySQL会自动回滚。

  3. MySQL对外部xa的支持还有其他不少bug,这里不一一赘述。


  • TCC

  • 关于TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions: an Apostate’s Opinion》的论文提出。


TCC 事务机制相对于 XA 的 2PC 相比,其特征在于它不依赖资源管理器(RM)对 XA 的支持,而是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事务。


TCC 型事务(Trying/Confirming/Canceling)。


  • TRYING 阶段主要是对业务系统做检测及资源预留。

  • CONFIRMING 阶段主要是对业务系统做确认提交,TRYING阶段执行成功并开始执行CONFIRMING阶段时,默认CONFIRMING阶段是不会出错的。即:只要TRYING成功,CONFIRMING一定成功。

  • CANCELING 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

  • TCC与2PC区别

  • 当讨论2PC时,只专注于事务处理阶段,因而只讨论prepare和commit,往往忽略了业务逻辑执行阶段,或者默认为prepare包括了业务逻辑执行。可以看下MySQL的XA的一个示例就比较好理解。



2PC 的一个完整的事务生命周期是:begin -> 业务逻辑 -> prepare -> commit。再看 TCC 的一个完整的事务生命周期是:begin -> 业务逻辑(try 业务) -> commit(comfirm 业务)。


虽然 TCC 的 confirm 阶段也会包含部分业务逻辑,当然从事务执行角度可以简化来看将 commit 与 confirm 类比,所以 TCC 并不是两阶段提交。


TCC 的 Trying/Confirming/Canceling 三个接口针对每个事务都需要用户自己来实现,其实对用户不太友好,增加用户开发工作量,另外不能保证所有人实现的接口一定能符合一致性要求,如果接口实现的有漏洞很可能会造成不一致。


  • SAGA

  • Saga是由普林斯顿大学的H.Garcia-Molina等人提出。其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调。每个Saga由一系列本地分支事务组成,每个分支事务有对应一个补偿事务。如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。


SAGA 事务模型,是牺牲了一定的隔离性的,但是提高了 long-running 事务的可用性。


除了隔离性的问题,SAGA 跟 TCC 一样对于补偿的动作也是需要用户自己实现,这点其实对用户不太友好。


  • 基于消息队列

  • 这种思路最早来自于ebay,核心思想将分布式事务分成多个本地事务,这里称之为主事务与从事务。主事务本地先行提交,然后通过消息通知从事务,从事务从消息中获取信息进行本地提交。可以看出这是一种异步事务机制、只能保证最终一致性;但可用性非常高,不会因为故障而发生阻塞。



上述解决方案看似完美,实际上还没有解决分布式问题。为了使第一个事务不涉及分布式操作,消息队列必须与主事务使用同一套存储资源,但为了使第二个事务是本地的,消息队列存储又必须与第二事务的存储在一起。这两者是不可能同时满足的。本质上并没有规避分布式事务。


如果消息具有操作幂等性,也就是一个消息被应用多次与应用一次产生的效果是一样的话,上述问题是很好解决的。但实际情况下,有些消息很难具有幂等性,比如转账中的扣款操作,执行一次和执行多次的结束显然是不一样的,因此需要做很多额外处理,一般通过状态表或者事务消息来解决。


  • 最大努力提交

  • 最大努力提交(Best Efforts)的关键点在于:


  1. 与2PC相比,省去了prepare,本质上属于1PC

  2. commit推迟到最后一起执行


最大努力提交最早在 spring 中事务管理中广泛流传,感兴趣的可以参考


在分布式数据库中间件的场景也广泛应用,我们来看看 MyCAT 的事务模型,有时也被称为弱 XA。



最大努力提交优点是性能非常好且对用户透明,缺点是可能存在部分提交成功部分失败的场景(Partial commits),而对于已经 commit 成功的场景无法 rollback。但是由于将容易出错的 sql 执行阶段先执行,commit 推迟到最后一起执行,相当于可能出错的危险窗口期缩短到只有最后的 commit 阶段,实际出错概率很低。而 commit 开始之前出错时可以正常回滚,不会有不一致。


如果是在应用层采用该事务模型可以将分支事务设计成幂等性,这样在 commit 出错时可以对出错分支进行重试。在分布式数据库中间件的场景,则很难具备幂等性。


DDM 分布式事务解决方案


各种分布式事务的方案都各有优缺点,而业务场景又是复杂多样的,对一致性的要求也各不一样,很难有一种方案包打天下。所以我们 DDM 在设计分布式事务方案时,充分考虑和权衡了各种方案的优缺点,提供了四种分布式事务模型,可以由用户自由选择。TCC 等模型使用起来需要用户自己实现相应的接口,对用户非常不友好。因此 DDM 提供了全透明模型的分布式事务,使用接口与原来单机一致。


  • 单机

  • 适合业务拆分比较合理,在应用层有自己的完善的事务处理框架,到DDM的事务都是单分片事务,单分片事务由底层数据库提供强一致性的保证。单机事务模型下,如果出现跨分片的事务,会报错进行提示,避免达不到预期目的。

  • 最大努力提交

  • 该事务模型前文有描述,在此不再赘述。该模型适合绝大部分不涉及金钱往来的业务,在性能和一致性之间比较好的一个平衡。事务中commit时因为是往多个节点发送执行,有部分commit成功部分commit失败的可能性,但是可能性比较低,只有在commit的时间窗内出现异常才有可能出现此种情况。

  • 最终一致性

  • 最大努力提交模型的问题(Partial commits)本质是不满足ACID中的A原子性 ,针对该问题 做了改进,针对Partial commits中已经提交成功的分支事务进行自动的事务补偿,保证了原子性,由于从Partial commits到补偿成功会有比较短的时间窗口,该时间窗口内数据处于不一致状态但最终会达到一致状态,所以称之为最终一致性。


当出现 Partial commits 异常情况是,是允许应用支持读取,所以可能会有脏读,如果业务场景对脏读比较敏感,比如之前转账事务中的查询余额,可以通过对该 select 加 for update 或者 lock in share mode 来解决,相当于针对该语句了保证了读已提交。


从 Partial commits 到补偿成功时间窗内,业界有选择不加锁的则会出现回滚覆盖,造成数据错误回补不成功,而 DDM 采用了高效的加锁避免了该问题。


  • 强一致性

  • 强一致性模型既解决了分布式事务的原子性,又可避免脏读,确保了读取到的都是commit成功的数据,从隔离级别上来属于读已提交。适合对一致性有极端要求的场景。但是一致性级别越高,付出性能代价会越大,所以请根据业务需要选择合适的模型。


本文转载自 华为云产品与解决方案 公众号。


原文链接:https://mp.weixin.qq.com/s/pT2DvQWr40xpkgdLb4Pb7w


2020 年 3 月 30 日 10:29148

评论

发布
暂无评论
发现更多内容

《转》Spring事务传播机制

hasWhere

Electron 快速入门及最新安装教程

程序员学院

Java html 前端 Electron node,js

奈学开发者社区分享:Java - 设计模式的7个设计原则

古月木易

Java 设计模式

戴尔G系列游戏本助玩家激战英特尔大师挑战赛

intel001

PPT画成这样,述职答辩还能过吗?

小傅哥

Java 小傅哥 流程图 架构师 PPT

清华架构师整理分布式系统文档:从实现原理到系统实现,收藏吧

小Q

Java 程序员 架构 分布式 微服务

for-range造就循环永动机?快来看看go中for-range的那些事!

新世界杂货铺

golang 后端 for

关于互联网留存和收益你知道多少—带你走近用户成长体系

滴滴普惠出行

初学源码之——银行案例手写IOC和AOP

Java架构师迁哥

bug 回忆录(一)

志学Python

《转》POI的XWPFParagraph.getRuns分段问题

hasWhere

一文领略 HTTP 的前世今生

yes的练级攻略

互联网 网络 HTTP 阿帕网

区块链会替代大数据吗?

CECBC区块链专委会

区块链 大数据

Java进阶教程、大厂面试真题、项目实战,GitHub上这14个开源项目屌炸天了!

Java架构之路

Java 程序员 面试 编程语言 项目实战

揭秘App的财富密码,剖析算法工程师价值来源

峰池

人工智能 互联网 推荐算法 互联网公司

实践分享丨物联网操作系统中的任务管理

华为云开发者社区

华为 数据 物联网 进程

数字货币是大势所趋,新冠疫情后必须率先发展DCEP

CECBC区块链专委会

数字货币 银行

form表单提交get请求

hasWhere

解读华为云原生数据库设计原则,打破传统数据库上云瓶颈

华为云开发者社区

数据库 数据

公有云厂商哪家强?本月UCloud、百度云、阿里云位居三甲——2020年8月云主机性能评测排名

BonreeAPM

Binder那么弱怎么面大厂?

博文视点Broadview

Java android 通信 移动开发 Android进阶

深入理解MySQL中事务隔离级别的实现原理

X先生

MySQL 数据库 后端 事务

关于深浅拷贝

西贝

Java 前端 基础

H5选图预览到上传最佳实践

阿里云金融线TAM SRE专家服务团队

android H5

实践案例丨利用小熊派开发板获取土壤湿度传感器的ADC值

华为云开发者社区

物联网 IoT 传感

一文纵览向量检索

华为云开发者社区

数据 搜索 检索 检查

世界的下一个主宰——人工智能

CECBC区块链专委会

人工智能 智能时代

阿里P8大牛力荐Java程序员进阶必读的书籍清单(附电子版)

Java架构之路

Java 程序员 面试 编程语言 书籍推荐

写给新人算法工程师

峰池

互联网 新人 推荐算法 算法工程师

奈学开发者社区分享:Java - 设计模式的7个设计原则

奈学教育

Java 设计模式 设计原则

华为全联接2020:环信AI领跑,输出5大行业最佳实践

DT极客

从一次小哥哥与小姐姐的转账开始, 浅谈分布式事务从理论到实践-InfoQ