Java技术江湖 · 2019年11月20日

搞懂分布式技术17:浅析分布式事务

本文转载自 linkedkeeper.com

本文内容参考网络,侵删

本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看

https://github.com/h2pl/Java-...

喜欢的话麻烦点下Star哈

文章首发于我的个人博客:

www.how2playlife.com

该系列博文会告诉你什么是分布式系统,这对后端工程师来说是很重要的一门学问,我们会逐步了解常见的分布式技术、以及一些较为常见的分布式系统概念,同时也需要进一步了解zookeeper、分布式事务、分布式锁、负载均衡等技术,以便让你更完整地了解分布式技术的具体实战方法,为真正应用分布式技术做好准备。

如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。

<!-- more -->

众所周知,数据库能实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务。但现在的系统往往采用微服务架构,业务系统拥有独立的数据库,因此就出现了跨多个数据库的事务需求,这种事务即为“分布式事务”。那么在目前数据库不支持跨库事务的情况下,我们应该如何实现分布式事务呢?本文首先会为大家梳理分布式事务的基本概念和理论基础,然后介绍几种目前常用的分布式事务解决方案。废话不多说,那就开始吧~

什么是事务?
事务由一组操作构成,我们希望这组操作能够全部正确执行,如果这一组操作中的任意一个步骤发生错误,那么就需要回滚之前已经完成的操作。也就是同一个事务中的所有操作,要么全都正确执行,要么全都不要执行。

事务的四大特性 ACID
说到事务,就不得不提一下事务著名的四大特性。

原子性原子性要求,事务是一个不可分割的执行单元,事务中的所有操作要么全都执行,要么全都不执行。

一致性一致性要求,事务在开始前和结束后,数据库的完整性约束没有被破坏。

隔离性事务的执行是相互独立的,它们不会相互干扰,一个事务不会看到另一个正在运行过程中的事务的数据。

持久性持久性要求,一个事务完成之后,事务的执行结果必须是持久化保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失。

注意:事务只能保证数据库的高可靠性,即数据库本身发生问题后,事务提交后的数据仍然能恢复;而如果不是数据库本身的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于『高可用性』的范畴。因此,事务只能保证数据库的『高可靠性』,而『高可用性』需要整个系统共同配合实现。

事务的隔离级别
这里扩展一下,对事务的隔离性做一个详细的解释。

在事务的四大特性ACID中,要求的隔离性是一种严格意义上的隔离,也就是多个事务是串行执行的,彼此之间不会受到任何干扰。这确实能够完全保证数据的安全性,但在实际业务系统中,这种方式性能不高。因此,数据库定义了四种隔离级别,隔离级别和数据库的性能是呈反比的,隔离级别越低,数据库性能越高,而隔离级别越高,数据库性能越差。

事务并发执行会出现的问题
我们先来看一下在不同的隔离级别下,数据库可能会出现的问题:

更新丢失当有两个并发执行的事务,更新同一行数据,那么有可能一个事务会把另一个事务的更新覆盖掉。当数据库没有加任何锁操作的情况下会发生。

脏读一个事务读到另一个尚未提交的事务中的数据。该数据可能会被回滚从而失效。如果第一个事务拿着失效的数据去处理那就发生错误了。

不可重复读不可重复度的含义:一个事务对同一行数据读了两次,却得到了不同的结果。它具体分为如下两种情况:

虚读:在事务1两次读取同一记录的过程中,事务2对该记录进行了修改,从而事务1第二次读到了不一样的记录。
幻读:事务1在两次查询的过程中,事务2对该表进行了插入、删除操作,从而事务1第二次查询的结果发生了变化。
不可重复读 与 脏读 的区别?脏读读到的是尚未提交的数据,而不可重复读读到的是已经提交的数据,只不过在两次读的过程中数据被另一个事务改过了。

数据库的四种隔离级别
数据库一共有如下四种隔离级别:

Read uncommitted 读未提交在该级别下,一个事务对一行数据修改的过程中,不允许另一个事务对该行数据进行修改,但允许另一个事务对该行数据读。因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读。

Read committed 读提交在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此会出现不可重复读的情况。

Repeatable read 重复读在该级别下,读事务禁止写事务,但允许读事务,因此不会出现同一事务两次读到不同的数据的情况(不可重复读),且写事务禁止其他一切事务。

Serializable 序列化该级别要求所有事务都必须串行执行,因此能避免一切因并发引起的问题,但效率很低。

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

什么是分布式事务?
到此为止,所介绍的事务都是基于单数据库的本地事务,目前的数据库仅支持单库事务,并不支持跨库事务。而随着微服务架构的普及,一个大型业务系统往往由若干个子系统构成,这些子系统又拥有各自独立的数据库。往往一个业务流程需要由多个子系统共同完成,而且这些操作可能需要在一个事务中完成。在微服务系统中,这些业务场景是普遍存在的。此时,我们就需要在数据库之上通过某种手段,实现支持跨数据库的事务支持,这也就是大家常说的“分布式事务”。

这里举一个分布式事务的典型例子——用户下单过程。当我们的系统采用了微服务架构后,一个电商系统往往被拆分成如下几个子系统:商品系统、订单系统、支付系统、积分系统等。整个下单的过程如下:

用户通过商品系统浏览商品,他看中了某一项商品,便点击下单
此时订单系统会生成一条订单
订单创建成功后,支付系统提供支付功能
当支付完成后,由积分系统为该用户增加积分
上述步骤2、3、4需要在一个事务中完成。对于传统单体应用而言,实现事务非常简单,只需将这三个步骤放在一个方法A中,再用Spring的@Transactional注解标识该方法即可。Spring通过数据库的事务支持,保证这些步骤要么全都执行完成,要么全都不执行。但在这个微服务架构中,这三个步骤涉及三个系统,涉及三个数据库,此时我们必须在数据库和应用系统之间,通过某项黑科技,实现分布式事务的支持。

CAP理论
CAP理论说的是:在一个分布式系统中,最多只能满足C、A、P中的两个需求。

CAP的含义:

C:Consistency 一致性同一数据的多个副本是否实时相同。
A:Availability 可用性可用性:一定时间内 & 系统返回一个明确的结果 则称为该系统可用。
P:Partition tolerance 分区容错性将同一服务分布在多个系统中,从而保证某一个系统宕机,仍然有其他系统提供相同的服务。
CAP理论告诉我们,在分布式系统中,C、A、P三个条件中我们最多只能选择两个。那么问题来了,究竟选择哪两个条件较为合适呢?

对于一个业务系统来说,可用性和分区容错性是必须要满足的两个条件,并且这两者是相辅相成的。业务系统之所以使用分布式系统,主要原因有两个:

提升整体性能当业务量猛增,单个服务器已经无法满足我们的业务需求的时候,就需要使用分布式系统,使用多个节点提供相同的功能,从而整体上提升系统的性能,这就是使用分布式系统的第一个原因。

实现分区容错性单一节点 或 多个节点处于相同的网络环境下,那么会存在一定的风险,万一该机房断电、该地区发生自然灾害,那么业务系统就全面瘫痪了。为了防止这一问题,采用分布式系统,将多个子系统分布在不同的地域、不同的机房中,从而保证系统高可用性。

这说明分区容错性是分布式系统的根本,如果分区容错性不能满足,那使用分布式系统将失去意义。

此外,可用性对业务系统也尤为重要。在大谈用户体验的今天,如果业务系统时常出现“系统异常”、响应时间过长等情况,这使得用户对系统的好感度大打折扣,在互联网行业竞争激烈的今天,相同领域的竞争者不甚枚举,系统的间歇性不可用会立马导致用户流向竞争对手。因此,我们只能通过牺牲一致性来换取系统的可用性和分区容错性。这也就是下面要介绍的BASE理论。

BASE理论
CAP理论告诉我们一个悲惨但不得不接受的事实——我们只能在C、A、P中选择两个条件。而对于业务系统而言,我们往往选择牺牲一致性来换取系统的可用性和分区容错性。不过这里要指出的是,所谓的“牺牲一致性”并不是完全放弃数据一致性,而是牺牲强一致性换取弱一致性。下面来介绍下BASE理论。

BA:Basic Available 基本可用
整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。只不过“基本可用”和“高可用”的区别是:
“一定时间”可以适当延长当举行大促时,响应时间可以适当延长
给部分用户返回一个降级页面给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。
S:Soft State:柔性状态同一数据的不同副本的状态,可以不需要实时一致。
E:Eventual Consisstency:最终一致性同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的。
酸碱平衡
ACID能够保证事务的强一致性,即数据是实时一致的。这在本地事务中是没有问题的,在分布式事务中,强一致性会极大影响分布式系统的性能,因此分布式系统中遵循BASE理论即可。但分布式系统的不同业务场景对一致性的要求也不同。如交易场景下,就要求强一致性,此时就需要遵循ACID理论,而在注册成功后发送短信验证码等场景下,并不需要实时一致,因此遵循BASE理论即可。因此要根据具体业务场景,在ACID和BASE之间寻求平衡。

分布式事务协议
下面介绍几种实现分布式事务的协议。

理解2PC和3PC协议

为了解决分布式一致性问题,前人在性能和数据一致性的反反复复权衡过程中总结了许多典型的协议和算法。其中比较著名的有二阶提交协议(2 Phase Commitment Protocol),三阶提交协议(3 Phase Commitment Protocol)。

2PC

分布式事务最常用的解决方案就是二阶段提交。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有参与者节点的操作结果并最终指示这些节点是否要把操作结果进行真正的提交。

因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。

第一阶段:投票阶段

该阶段的主要目的在于打探数据库集群中的各个参与者是否能够正常的执行事务,具体步骤如下:

  1. 协调者向所有的参与者发送事务执行请求,并等待参与者反馈事务执行结果。
  2. 事务参与者收到请求之后,执行事务,但不提交,并记录事务日志。
  3. 参与者将自己事务执行情况反馈给协调者,同时阻塞等待协调者的后续指令。

第二阶段:事务提交阶段

在第一阶段协调者的询盘之后,各个参与者会回复自己事务的执行情况,这时候存在三种可能:

  1. 所有的参与者回复能够正常执行事务。
  2. 一个或多个参与者回复事务执行失败。
  3. 协调者等待超时。

对于第一种情况,协调者将向所有的参与者发出提交事务的通知,具体步骤如下:

  1. 协调者向各个参与者发送commit通知,请求提交事务。
  2. 参与者收到事务提交通知之后,执行commit操作,然后释放占有的资源。
  3. 参与者向协调者返回事务commit结果信息。

对于第二、三种情况,协调者均认为参与者无法正常成功执行事务,为了整个集群数据的一致性,所以要向各个参与者发送事务回滚通知,具体步骤如下:

  1. 协调者向各个参与者发送事务rollback通知,请求回滚事务。
  2. 参与者收到事务回滚通知之后,执行rollback操作,然后释放占有的资源。
  3. 参与者向协调者返回事务rollback结果信息。

两阶段提交协议解决的是分布式数据库数据强一致性问题,其原理简单,易于实现,但是缺点也是显而易见的,主要缺点如下:

单点问题:协调者在整个两阶段提交过程中扮演着举足轻重的作用,一旦协调者所在服务器宕机,那么就会影响整个数据库集群的正常运行,比如在第二阶段中,如果协调者因为故障不能正常发送事务提交或回滚通知,那么参与者们将一直处于阻塞状态,整个数据库集群将无法提供服务。

同步阻塞:两阶段提交执行过程中,所有的参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,这样效率及其低下。

数据不一致性:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

3PC

针对两阶段提交存在的问题,三阶段提交协议通过引入一个“预询盘”阶段,以及超时策略来减少整个集群的阻塞时间,提升系统性能。三阶段提交的三个阶段分别为:can_commit,pre_commit,do_commit。

第一阶段:can_commit

该阶段协调者会去询问各个参与者是否能够正常执行事务,参与者根据自身情况回复一个预估值,相对于真正的执行事务,这个过程是轻量的,具体步骤如下:

  1. 协调者向各个参与者发送事务询问通知,询问是否可以执行事务操作,并等待回复。
  2. 各个参与者依据自身状况回复一个预估值,如果预估自己能够正常执行事务就返回确定信息,并进入预备状态,否则返回否定信息。

第二阶段:pre_commit

本阶段协调者会根据第一阶段的询盘结果采取相应操作,询盘结果主要有三种:

  1. 所有的参与者都返回确定信息。
  2. 一个或多个参与者返回否定信息。
  3. 协调者等待超时。

针对第一种情况,协调者会向所有参与者发送事务执行请求,具体步骤如下:

  1. 协调者向所有的事务参与者发送事务执行通知。
  2. 参与者收到通知后,执行事务,但不提交。
  3. 参与者将事务执行情况返回给客户端。

在上面的步骤中,如果参与者等待超时,则会中断事务。 针对第二、三种情况,协调者认为事务无法正常执行,于是向各个参与者发出abort通知,请求退出预备状态,具体步骤如下:

  1. 协调者向所有事务参与者发送abort通知
  2. 参与者收到通知后,中断事务

第三阶段:do_commit

如果第二阶段事务未中断,那么本阶段协调者将会依据事务执行返回的结果来决定提交或回滚事务,分为三种情况:

  1. 所有的参与者都能正常执行事务。
  2. 一个或多个参与者执行事务失败。
  3. 协调者等待超时。

针对第一种情况,协调者向各个参与者发起事务提交请求,具体步骤如下:

  1. 协调者向所有参与者发送事务commit通知。
  2. 所有参与者在收到通知之后执行commit操作,并释放占有的资源。
  3. 参与者向协调者反馈事务提交结果。

针对第二、三种情况,协调者认为事务无法正常执行,于是向各个参与者发送事务回滚请求,具体步骤如下:

  1. 协调者向所有参与者发送事务rollback通知。
  2. 所有参与者在收到通知之后执行rollback操作,并释放占有的资源。
  3. 参与者向协调者反馈事务提交结果。

在本阶段如果因为协调者或网络问题,导致参与者迟迟不能收到来自协调者的commit或rollback请求,那么参与者将不会如两阶段提交中那样陷入阻塞,而是等待超时后继续commit。相对于两阶段提交虽然降低了同步阻塞,但仍然无法避免数据的不一致性。

Reference

https://zhuanlan.zhihu.com/p/...

http://www.infoq.com/cn/artic...

http://blog.csdn.net/jasonsun...

http://blog.csdn.net/suifeng3...

https://my.oschina.net/wangzh...

转载请并标注: “本文转载自 linkedkeeper.com ”

推荐阅读
关注数
6
文章数
185
微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站。作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息