数据库事务的基本特性:
- 原子性(Atomicity )、
- 一致性( Consistency )、
- 隔离性或独立性( Isolation)、
- 持久性(Durabilily),
简称就是ACID
在一个分布式微服务的系统之上,如果一个业务操作,涉及到了多个服务一起完成,有可能涉及到多个数据库的数据变更,这种情况下,需要分布式事务的解决方案。
分布式理论
集群环境下,再想保证集群的ACID几乎是很难达到,这时我们就需要引入一个新的理论原则来适应这种集群的情况,就是 CAP 原则或者叫CAP定理。
CAP理论
- 一致性(Consistency) : 客户端知道一系列的操作都会同时发生(生效)
- 可用性(Availability) : 每个操作都必须以可预期的响应结束
- 分区容错性(Partition tolerance) : 即使出现单个组件无法可用,操作依然可以完成
BASE理论
- Basically Available(基本可用)
- Soft state(软状态)
- Eventually consistent(最终一致性)
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)
分布式事务解决方案
列出以下几个常用的解决方案,每个方案适用的场景不同。
两阶段提交2PC[分布式一致性理论]
系统一般包含两类机器(或节点):一类为协调者(coordinator),通常一个系统中只有一个;另一类为事务参与者(participants,cohorts或workers),一般包含多个。
两阶段:
- 请求阶段(commit-request phase,或称表决阶段,voting phase) :在请求阶段,协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)
- 提交阶段(commit phase) :在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或取消。 当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务。参与者在接收到协调者发来的消息后将执行响应的操作
XA协议
XA协议是 2PC在数据库分布式事务方面( 理解成分布式一致性问题2PC方案的一种实际应用,分布式事务其实也是一致性问题 ) 的定义,又叫做XA Transactions,XA规范主要定义了(全局)事务管理器(Transaction Manager,TM)和(局部)资源管理器(Resource Manager,RM)之间的接口,接口是双向互通的,可以彼此完成数据交换。
JTA
Java Transaction API,定义了Java平台的XA协议的规范和接口,JTA 中,事务管理器抽象为javax.transaction.TransactionManager接口,并通过底层事务服务(即Java Transaction Service)实现。像很多其他的Java规范一样,JTA仅仅定义了接口,具体的实现则是由供应商(如J2EE厂商)负责提供。
JTA定义了接口,需要不同的J2EE厂商和数据库厂商共同完成,即,需要支持事务管理器以及事务的提交和撤回。
目前,框架Atomikos可以实现JTA,通过和Spring等配合,实现JTA功能。
2PC的优缺点
- 同步阻塞: 当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态
- 单点故障:一旦协调者发生故障。参与者会一直阻塞下去。第二阶段,若协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作
- 数据不一致:提交的阶段中,当协调者向参与者发送commit请求之后,若在发送commit请求过程中协调者发生了故障,导致只有部分参与者接受到了commit请求。而这部分参会执行commit操作。但是其未接到commit请求的协调者则无法执行事务提交。于是出现了数据不一致性的现象
- 耗费资源(JTA,Atomikos)
三阶段提交3PC[分布式一致性理论]
在协调者和参与者引入超时机制,将2PC的第一个阶段拆分,询问和锁资源,最后是真正提交。如下图,
执行流程解释,
- can-commit阶段:协调者向参与者发送commit请求,参与者如果可以提交,就返回yes,反之,no,之后,协调者计时等待(等待协调者返回yes还是no,没有及时返回,就是no)。
- pre-commit阶段:协调者根据参与者返回来判断是否执行pre-commit阶段
- 参与者都返回yes
(1) 协调者发送预提交请求后,进入到prepare阶段
(2) 参与者接受到请求后,执行事务预提交,记录undo和redo信息(MVCC)
(3) 参与者执行完后,返回ACK,同时,开始计时等待最终指令。 - 任何一个返回no 或者 协调者在获取所有参与者返回之前等待超时。
(1)协调者向参与者发送abort中断指令
(2)参与者执行中断任务,收到中断指令,或者等待超时(等待协调者发送提交指令还是abort指令,没有及时返回,就是默认提交指令)。
3.do-commit阶段: 根据第二阶段的结果,分成两种情况 - 执行提交
(1)协调者发送ack指令
(2)参与者执行事务提交,并发送ack相应
(3)参与者发送ack响应 - 中断事务
(1)协调者发送abort指令
(2)参与者回滚事务
三阶段3PC协议缺点
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit,因此pre-Commit之后,如果协调者发送abort请求,但是有部分协调者未收到abort请求,那么在他们等待超时后,会继续执行commit操作,导致数据不一致。
补偿事务TCC
全称 try-confirm-cannel,则是将业务逻辑分成try、confirm/cancel两个阶段执行。原理性质的,可以参考中华石杉的笔记,https://mp.weixin.qq.com/s?__biz=MzU0OTk3ODQ3Ng==&mid=2247483862&idx=1&sn=f94857a050ae0e98521a70f331fe5420&chksm=fba6e9d5ccd160c3c39b2a474f2e0a636465a79446d71822148e0129164cb91dfb11e61e7555&mpshare=1&scene=1&srcid=03044xUUHt8UlgTcU1ZscXeH#rd。
这里说下和2PC的区别和关系,
- 2PC机制的业务阶段 等价于 TCC机制的try业务阶段
- 2PC机制的提交阶段(prepare & commit) 等价于 TCC机制的提交阶段(confirm)
- 2PC机制的回滚阶段(rollback) 等价于 TCC机制的回滚阶段(cancel)
- 2PC的案JTA是需要底层数据库支持,而TCC则不要求,甚至后面都不是数据库
2PC是begin -> 业务逻辑 -> prepare -> commit, 而TCC是begin -> 业务逻辑(try业务) -> commit(comfirm业务)。这里TCC的try业务在中华石杉笔记中得到体现。
本地消息列表
该方法是业界使用最多的,其实在我们日常的开发中,使用过类似的方式,只是我们不知道这种方式叫做本地消息列表而已,核心思想,是将分布式的业务,拆分成本地的事务处理,看如下图,
含义是,将后续跨库的操作,以消息的形式,落在本地统一库中,这样就可以控制本地事务。而落地的消息会通过kafka等队列,在其他库中消费者会获取到,进行更改相应的数据。
思路如下,
- 消息生产方,建立额外消息表,记录消息的发送状态(消息是对后续业务数据的更改),后续消息操作表和当前消息发送方在一个数据库,如果消息生成成功,表明消息已经记录,且数据已经发送到mq中。
- 消费方,监听队列,提取消息,根据消息指定的操作完成业务,后,更改消息生产方中的消息表中对应的记录,如果最终事务执行成功,则完成。如果事务出现异常,则捕获异常,通过mq发送业务补偿的消息给生产方。
- 生产方和消费方,都需要定期扫描本地的消息表,将未成功的记录,根据异常类型,判断是否再次发送。
- 发送方还是继续扫描本地的消息表
- 消费方如果执行成功,但是,毕竟要垮库去修改生产方的数据,也会出现事务问题,因此,消费者也可以本地设置一个信息表,自己定期轮训这个信息表,直接跨库操作,不需要在通过kafka传递,毕竟通过kafka的话,生产方还要定义消费者,影响业务逻辑。
优缺点
- 这种方案遵循BASE理论,采用的是最终一致性
- 不会出现像2PC那样复杂的实现,多子系统,要每个都反复调用,影响性能
- 不会像TCC那样可能出现确认或者回滚不了的情况
- 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理
事务消息队列
本地消息队列的严重限制是消息表和业务表耦合度过大,而且当业务库出现问题,导致消息表也失去功能,因此,将消息表独立出来做好,这就诞生了MQ事务消息队列的应用。
这里我们使用RocketMQ为例子,
- 第一阶段Prepared消息,会拿到消息的地址。
- 第二阶段执行本地事务,
- 第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
业务需要向消息队列中发送两次请求,一次消息发送和一次确认消息,过程如下,
- 发送Prepared消息
- update DB
- 根据update DB结果成功或失败,Confirm或者取消Prepared消息。
最后1步失败了怎么办?这里就涉及到了RocketMQ的关键点:RocketMQ会定期(默认是1分钟)扫描所有的Prepared消息,询问发送方,到底是要确认这条消息发出去?还是取消此条消息?
通过RocketMQ的checkListener来实现,RocketMQ会回调此Listener,从而实现上面所说的方案。
RocketMQ最大的改变,其实就是把“扫描消息表”这个事情,不让业务方做,而是消息中间件帮着做了。
至于消息表,其实还是没有省掉。因为消息中间件要询问发送方,事物是否执行成功,还是需要一个“变相的本地消息表”,记录事物执行状态。
如果消费者失败了如何?虽然如图上所示,会一直循环,但是终有到头的时候,此时,需要人工介入。从工程实践角度讲,这种整个流程自动回滚的代价是非常巨大的,不但实现复杂,还会引入新的问题。比如自动回滚失败,又怎么处理?对应这种极低概率的case,采取人工处理,会比实现一个高复杂的自动化回滚系统,更加可靠,也更加简单