分布式事务及常见解决方案总结

一,数据库事务

1.1 概念

一个数据库事务通常包含了一个序列的对数据库的读/写操作。它的存在包含有以下两个目的:

  • 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
  • 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。

1.2 特性

并非任意的对数据库的操作序列都是数据库事务。事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。

原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。 持久性(Durability):一个事务一旦提交,他对数据库的修改应该永久保存在数据库中。

举例:

1
2
3
4
5
6
1、从A账号中把余额读出来(500)
2、对A账号做减法操作(500-100)
3、把结果写回A账号中(400)
4、从B账号中把余额读出来(500)
5、对B账号做加法操作(500+100)
6、把结果写回B账号中(600)
  • 原子性:整个过程是一个原子操作要么全部成功要不全部失败,比如5操作失败,那么之前的所有操作都应该回滚到执行事务之前的状态
  • 一致性:在转账之前,A和B的账户中共有500+500=1000元钱。在转账之后,A和B的账户中共有400+600=1000元。也就是说,数据的状态在执行该事务操作之后从一个状态改变到了另外一个状态。同时一致性还能保证账户余额不会变成负数等
  • 隔离性:在A向B转账的整个过程中,只要事务还没有提交(commit),查询A账户和B账户的时候,两个账户里面的钱的数量都不会有变化。 如果在A给B转账的同时,有另外一个事务执行了C给B转账的操作,那么当两个事务都结束的时候,B账户里面的钱应该是A转给B的钱加上C转给B的钱再加上自己原有的钱。
  • 持久性:一旦转账成功(事务提交),两个账户的里面的钱就会真的发生变化(会把数据写入数据库做持久化保存)

1.3 事务处理过程简单介绍(MySQL)

  • 回滚日志(Undo Log)

1541668143212

1
2
3
4
想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚,而在MySQL中,恢复机制是通过回滚日志
(undo log)实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后在对数据库中的对应行进行写入。回滚
日志可以简单的理解为当每执行一次Insert操作都会记录一条对应的Delete日志,每执行一次Update都会记录相反的
Update日志
  • 重做日志(Redo Log)

1541580117320

1
2
3
4
5
6
7
事务的持久化是通过日志来实现的,MySQL 使用重做日志(redo log)实现事务的持久性,重做日志由两部分组成,
一是内存中的重做日志缓冲区,因为重做日志缓冲区在内存中,所以它是易失的,另一个就是在磁盘上的重做日志文
件,它是持久的。当我们在一个事务中尝试对数据进行修改时,它会先将数据从磁盘读入内存,并更新内存中缓存的数
据,然后生成一条重做日志并写入重做日志缓存,当事务真正提交时,MySQL 会将重做日志缓存中的内容刷新到重做日
志文件,再将内存中的数据更新到磁盘上,图中的第 4、5 步就是在事务提交时执行的。除了所有对数据库的修改会产
生重做日志,因为回滚日志也是需要持久存储的,它们也会创建对应的重做日志,在发生错误后,数据库重启时会从重
做日志中找出未被更新到数据库磁盘中的日志重新执行以满足事务的持久性。

二, 分布式事务介绍

分布式事务是指会涉及到操作多个数据库的事务。其实就是将对同一库事务的概念扩大到了对多个库的事务。目的是为了保证分布式系统中的数据一致性。分布式事务处理的关键是必须有一种方法可以知道事务在任何地方所做的所有动作,提交或回滚事务的决定必须产生统一的结果(全部提交或全部回滚)

1
场景一:单应用多数据库或单应用单数据库分库分表

1541660356194

1
2
3
场景二:基于SOA或微服务多个应用多个数据库
说明:App前台用户使用积分兑换优惠券,需要调用优惠券服务(coupon-server)给用户发放优惠券同时调用
user-server服务扣减用户积分

1541660383531

三,XA协议

XA协议由Tuxedo首先提出的,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准。目前,Oracle、Informix、DB2和Sybase等各大数据库厂家都提供对XA的支持。XA协议采用两阶段提交方式来管理分布式事务。XA接口提供资源管理器与事务管理器之间进行通信的标准接口

1
2
3
4
X/OpenDTP角色
AP application:客户端
RM resouces manager:资源管理器(数据库)
TM transaction manager:事务管理器,事务协调者

四,分布式事务解决方案

4.1 两阶段提交(2PC)

1
2
两阶段提交(2PC)就是使用XA协议的原理,所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:
提交阶段(执行阶段)。

1541645786548

牧师(Transaction Manager):”你愿意娶这个女人吗?爱她、忠诚于她,无论她贫困、患病或者残疾,直至死亡。Doyou(你愿意吗)?”

新郎(参与者-1):”Ido(我愿意)!”

牧师:”你愿意嫁给这个男人吗?爱他、忠诚于他,无论他贫困、患病或者残疾,直至死亡。Doyou(你愿意吗)?”

新娘(参与者-2):”Ido(我愿意)!”

优点:

​ 1,尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)

​ 2,原理简单、实现方便

缺点:

​ 1, 同步阻塞

​ 2, 单点问题

​ 3, 数据不一致

4.2 三阶段提交(3PC)

1
2
3
4
3PC最关键要解决的就是协调者和参与者同时挂掉的问题,所以3PC把2PC的准备阶段再次一分为二,这样三阶段提交就
有CanCommit、PreCommit、DoCommit三个阶段。在第一阶段,只是询问所有参与者是否可可以执行事务操作,并不在
本阶段执行事务操作。当协调者收到所有的参与者都返回YES时,在第二阶段才执行事务操作,然后在第三阶段在执行
commit或者rollback。

img

Participant如果在不同阶段宕机,我们来看看3PC如何应对:

  • 阶段1: coordinator或watchdog未收到宕机participant的vote,直接中止事务;宕机的participant恢复后,读取logging发现未发出赞成vote,自行中止该次事务
  • 阶段2: coordinator未收到宕机participant的precommit ACK,但因为之前已经收到了宕机participant的赞成反馈(不然也不会进入到阶段2),coordinator进行commit;watchdog可以通过问询其他participant获得这些信息,过程同理;宕机的participant恢复后发现收到precommit或已经发出赞成vote,则自行commit该次事务
  • 阶段3: 即便coordinator或watchdog未收到宕机participant的commit ACK,也结束该次事务;宕机的participant恢复后发现收到commit或者precommit,也将自行commit该次事务
1
2
3
4
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,
他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,
由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。
这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
1
2
3
3PC的实现框架:
- 1.JOTM(java open transaction manager)
- 2.Atomikos

优点:降低参与者阻塞范围,并能够在出现单点故障后继续达成一致

缺点:引入preCommit阶段,在这个阶段如果出现网络分区,协调者无法与参与者正常通信,参与者依然会进行事务提交,造成数据不一致

4.3 补偿事务(TCC)

TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

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

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

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

    1541659939230

1
2
3
4
5
举个例子,假入 Bob 要向 Smith 转账,思路大概是:
我们有一个本地方法,里面依次调用
1、首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。
2、在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
3、如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。

优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些

缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以在实现的时候多写很多补偿的代码。

应用:要求实时性比较高的业务

1
2
3
TCC实现框架:
- tcc-transaction
- bytetcc

4.4 本地消息表(异步确保)

本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。我们可以从下面的流程图中看出其中的一些细节:

img

基本思路:

1
2
3
4
5
6
7
8
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要
在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处
理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操
作。生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账
补账逻辑,这种方案还是非常实用的。这种方案遵循BASE理论,采用的是最终一致性,是这几种方案里面比较适合实际
业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可
能出现确认或者回滚不了的情况。

优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

4.5 MQ 事务消息

有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。

以阿里的 RocketMQ 中间件为例,其思路大致为:

  • 第一阶段Prepared消息,会拿到消息的地址。

  • 第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。

1
核心思想:大事务 = 小事务 + 异步

1541663382323

1
2
3
第一阶段发送Prepared消息时,会拿到消息的地址
第二阶段执行本地事物
第三阶段通过第一阶段拿到的地址去访问消息,并修改消息的状态。

1. 先发消息还是先执行扣款?

1
RocketMQ支持事务消息 ,可以实现事务回滚

2. 如果确认消息发送失败了怎么办?

1
2
3
如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向
消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是
继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。

3.如果Bob的账户的余额已经减少,且消息已经发送成功,会出现消费失败和消费超时两个问题 ?

1
2
3
4
5
6
超时问题:解决超时问题的思路就是一直重试,直到消费端消费消息成功
消费失败:阿里提供给我们的解决方法是:人工解决
#按照事务的流程,因为某种原因Smith加款失败,那么需要回滚整个流程。如果消息系统要实现这个回滚流程的话,
#系统复杂度将大大提升,且很容易出现Bug,估计出现Bug的概率会比消费失败的概率大很多。这也是RocketMQ目前
#暂时没有解决这个问题的原因,在设计实现消息系统时,我们需要衡量是否值得花这么大的代价来解决这样一个出现
#概率非常小的问题

优点: 实现了最终一致性,不需要依赖本地数据库事务。

缺点: 实现难度大,主流MQ不支持

总结

还是那句话,能不用分布式事务就不用,如果非得使用的话,结合自己的业务分析,看看自己的业务比较适合哪一种,是在乎强一致,还是最终一致即可。上面对解决方案只是一些简单介绍,如果真正的想要落地,其实每种方案需要思考的地方都非常多,复杂度都比较大 。

参考

  1. 2PC到3PC到Paxos到Raft到ISR
  2. 事务原理
  3. Spring Cloud分布式事务终极解决方案探讨
  4. 微服务架构的分布式事务解决方案
  5. 分布式开放消息系统(RocketMQ)的原理与实践
  6. 分布式事务的实现原理
  7. 分布式系统理论基础 - 一致性、2PC和3PC
  8. 深入理解分布式系统的2PC和3PC