1.分布式事务简介
传统的关系型数据库只能保证单个数据库中多个数据表的事务特性。一旦多个SQL操作涉及到多个数据库,这类的事务就无法解决跨库事务问题。在传统架构下,这种问题出现的情况非常少,但是在分布式微服务架构中,分布式事务的问题变得更加突出。
举例,假设我们要涉嫌下面电商系统中的支付功能。
分析: 上图中有3个服务 支付服务、资金服务和红包服务
当用户发起支付时,就会涉及到几个事务操作
- 创建支付订单
- 从资金服务中扣除余额
- 从红包服务中扣除余额
- 更新支付结果
这是四个典型的事务操作,而且这些操作分别属于不同的数据库,最终期望 的结果是希望这三个服务所对应的数据是一致的,很显然传统的事务无法解 决这个问题!
于是,分布式事务就诞生了。说到分布式事务,我们就不得不提以下X/OpenDTP事务模型
2.X/OpenDTP事务模型
这个事务模型定义了一套分布式事务的标准,也就是定义了规范和API接口,并且这个标准提出了二阶段提交(2PC-Two-Phase-Commit)来保证分布式事务的完整性。
2.1 二阶段提交模型
那么什么是二阶段提交2PC协议呢?
如上图所示, 在分布式事务中,多个小事务的提交与回滚,只有当前进程知道,其它进程是不清楚的。而为了实现多个数据库的事务一致性,就必然需要引入第三方节点来进行事务协调,如下图所示,
通过一个全局的分布式事务协调,从而来实现多个数据库事务的提交与回滚,在这样的架构下,事务的管理方式就变成了两个步骤。
a.开启事务并向各个数据库节点写入事务日志
b.根据第一个步骤中各个节点的执行结果,来决定对事务进行提交或回滚。
这就是所谓的2PC提交协议,用图来表示为如下:
2PC的提交流程如下:
1. 表决阶段:此时 TM(协调者)向所有的参与者发送一个 事务请求,参与者在收到这请求后,如果准备好了(写事务日志)就会向 TM发送一个 执行成功 消息作为回应,告知 TM 自己已经做好了准备,否则会返回一个 失败 消息;2. 提交阶段:TM 收到所有参与者的表决信息,如果所有参与者一致认为可以提交事务,那么 TM就会发送 提交 消息,否则发送 回滚 消息;对于参与者而言,如果收到 提交 消息,就会提交本地事务,否则就会取消本地事务。
2.2 X/OpenDTP事务模型
基于对上述2PC的了解,我们再来看下X/OpenDTP事务模型,如下图所示
如上图所示,X/OpenDTP模型定义了三个角色和两个协议,其中三个角色为
AP(Application Program),表示应用程序,也可以理解成使用DTP模型的程序RM(Resource Manager),资源管理器,这个资源可以是数据库, 应 用程序通过资源管理器对资源进行控制,资源管理器必须实现XA定义的接口TM(Transaction Manager),表示事务管理器,负责协调和管理全局事务,事务管理器控制整个全局事务,管理事务的生命周期,并且协调资源。
两个协议分别是
XA协议: XA 是X/Open DTP定义的资源管理器和事务管理器之间的接口 规范,TM用它来通知和协调相关RM事务的开始、结束、提交或回滚。目前Oracle、Mysql、DB2都提供了对XA的支持; XA接口是双向的系统接口,在事务管理器(TM)以及多个资源管理器之间形成通信的桥梁(XA不 能自动提交)
XA {START|BEGIN} xid [JOIN|RESUME] --负责开启或者恢复一个事务分支,并且管理XID到调用线程
XA END xid [SUSPEND [FOR MIGRATE]] --负责取消当前线程与事务分支的关联
XA PREPARE xid --负责询问RM 是否准备好了提交事务分支
XA COMMIT xid [ONE PHASE] --知RM提交事务分支
XA ROLLBACK xid --通知RM回滚事务分支
XA RECOVER [CONVERT XID]
TX协议: 全局事务管理器与资源管理器之间通信的接口
在分布式系统中,每一个机器节点虽然都能够明确知道自己在进行事务操作过程中的结果是成功还是失败,但却无法直接获取到其他分布式节点的操作结果。因此当一个事务操作需要跨越多个分布式节点的时候,为了保持事务处理的ACID特性,就需要引入一个“协调者”(TM)来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点被称为AP。TM负责调度AP的行为,并最终决定这些AP是否要把事务真正进行提交到(RM)
2.3 基于XA协议的2PC提交流程
二阶段提交,是计算机网络尤其是在数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务处理过程中能够保持原子性和一致性而设计的一种算法。通常,二阶段提交协议也被认为是一种一致性协议,用来保证分布式系统数据的一致性。目前,绝大部分的关系型数据库都是采用二阶段提交协议来完成分布式事务处理的,利用该协议能够非常方便地完成所有分布式事务AP的协调,统一决定事务的提交或回滚,从而能够有效保证分布式数据一致性,因此2pc也被广泛运用在许多分布式系统中。
整体流程如上图所示,下面为演示Mysql使用XA实现分布式事务提交:
-- 启动一个XA事务 (xid 必须是一个唯一值; [JOIN|RESUME] 字句不被支持)
xa start 'xatest1';
insert into ums_user(username,password) values('test1','test1');
-- 结束一个XA事务 ( [SUSPEND [FOR MIGRATE]] 字句不被支持)
xa end 'xatest1';
-- 准备 此动作会把这个事务的redo日志写入innodb redo log,只要这一阶段是成功的,那么后续XACommit一定会成功
xa prepare 'xatest1';
XA COMMIT 'xatest1'; //提交事务
-- 或者回滚代码,如果xa prepare这个环节出现错误,事务协调者就把这个事务回滚。
xa rollback 'xatest'; //回滚
但是在这个过程中,数据库需要提供针对分布式事务处理的接口给到事务管理器,这样事务管理器就能够基于这些接口来协调各个资源的事务提交和回滚操作。也就是说,数据库层面必须要支持。
2.4 基于XA协议的开源框架
主流的数据库如Oracle、mysql都支持XA协议,因此都可以基于xa协议规范,通过二阶段提交来实现数据的一致性。
J2EE就遵循了这些规范,设计还并实现了Java里面的分布式事务编程接口规范-JTA,我们可以利用这些API来完成各个数据库的事务一致性处理。
但是,在XA事务中,根据前面我们的操作过程可以发现,另外,在XACOMMIT阶段,如果其中一个RM因为网络超时没有收到数据提交的指令,会导致数据不一致。所以如果我们要基于这些API来实现一个比较成熟的分布式事务解决方案, 还需要考虑到这些问题并提出解决方案。比如针对这个问题,我们可以采用 重试的机制来完成数据一致性。
因此,针对这类分布式事务解决方案的开源框架也很多,比如
Atomikos,Atomikos是为Java平台提供的开源的事务管理工具,它包含收费和开源两个版本,开源版本基本能满足我们的需求。Bitronix,是一个流行的开源JTA事务管理器实现,你可以使用spring-boot-starter-jta-bitronixstarter为项目添加合适的Birtronix依赖。Seata事务,阿里巴巴开源的事务解决方案
3.分布式事务解决方案
对于我们的分布式事务来说,存在着网络通信的不确定性,比如当分布式事务中的其中一个节点故障,导致无法提交的情况下,我们究竟是抛弃呢? 还是等待?如果是抛弃,那么我们的数据强一致性就无法保证,但是如果是等待,又容易造成大量线程阻塞,系统性能严重受损。换句话说,我们是要保证可用性呢还是一致性,二者只能选其一。
3.1 CAP理论
CAP理论说的就是分布式架构下的数据一致性和性能问题的平衡方案,
C:Consistency 一致性 同一数据的多个副本是否实时相同。A:Availability 可用性 可用性:一定时间内 & 系统返回一个明确的结果则称为该系统可用。P:Partition tolerance 分区容错性 将同一服务分布在多个系统中,从而保证某一个系统宕机,仍然有其他系统提供相同的服务。
CAP理论告诉我们,在分布式系统中,C,A,P三个条件中我们最多只能选择两个,那么问题就来了,我们究竟选择那两个条件较为合适呢?
对于一个业务系统来说,可用性和分区容错性是必须要满足的两个条件,并且这两者是相辅相成的。业务系统之所以使用分布式系统,主要原因有两个 :
提升整体性能 当业务量猛增,单个服务器已经无法满足我们的业务需求的时候,就需要使用分布式系统,使用多个节点提供相同的功能,从而整体上提升系统的性能,这就是使用分布式系统的第一个原因。实现分区容错性 单一节点 或 多个节点处于相同的网络环境下,那么会存在一定的风险,万一该机房断电、该地区发生自然灾害,那么业务系统就全面瘫痪了。为了防止这一问题,采用分布式系统,将多个子系统分布在不同的地域、不同的机房中,从而保证系统高可用性。
这说明了分区容错性是分布式系统的根本,如果分区容错性不能满足,那么使用分布式系统将失去意义。
此外,可用性对业务系统也尤为重要。在大谈用户体验的今天,如果业务系统时常出现'系统异常',响应时间过长等情况,这就使得用户对系统的好感度大打折扣。
在互联网行业竞争激烈的今天,相同领域的竞争者不甚枚举,系统的间歇性 不可用会立马导致用户流向竞争对手。因此,我们只能通过牺牲一致性来换取系统的 可用性 和 分区容错 。
因此,例如Zookeeper中就采用了基于少数服从多数的2pc落地方案
因此,也引出了另外一个理论,叫Base理论
3.2 Base理论
CAP理论告诉我们一个悲惨但不得不接受的实时--我们只能在C,A,P中选择两个条件。而对于业务系统而言,我们往往选择牺牲一致性来换取系统的可用性和分区容错性。不过这里要指出的是,所谓的“牺牲一致性”并不是完全放弃数据一致性,而是牺牲强一致性换取弱一致性。
BA:Basic Available 基本可用整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定 时间内仍然能够返回一个明确的结果。只不过“基本可用”和“高可用” 的区别是:1.“一定时间”可以适当延长 当举行大促时,响应时间可以适当延长2.给部分用户返回一个降级页面 给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。S:Soft State:柔性状态 同一数据的不同副本的状态,可以不需要实时 一致。E:Eventual Consisstency:最终一致性 同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的。
所以,对于服务来说,也有很多方案可供选择
- 提供查询服务确认数据状态
- 幂等操作对于重发保证数据的安全性
- TCC事务操作
- 补偿操作
- 定期校对
4. 基于可靠性消息的最终一致性方案
最终一致性方案,也称为弱一致性方案
它是基于BASE理论的落地。假设我们要实现一个用户服务里面去实现用户购买,积分服务里面增加用户的积分
@Transactional
public void register(){
// 用户交易 // local transaction(失败)
// 发送MQ // mq 成功了 --> 意外获得了积分
}
这样可能存在两种异常情况 用户交易成功,积分失败了 ; 用户交易失败,积分成功了
那么我们怎么要才能保证分布式事务的解决,以及数据的一致性呢。一方面我们要保证事务参与方接收消息的可靠性;另一方面还有就是消息重复消费的问题,消息重复发送怎么保证
4.1 事务参与方接收消息的可靠性
事务参与方接收消息的可靠性可以采用本地事务的方式来解决, 新增一个消息表,来记录MQ的消息发送,发送成功,那么事务同时进行,如果新增用户失败,则回滚,如果消息发送失败了,那就定时任务接着发,但同时也会记录消息表,比如采用一个状态记录成功或失败,定时任务后面可以接着发送
整体交互流程如下:
1. 用户注册 :用户服务在本地事务新增用户和增加 “积分消息日志”。(用 户表和消息表通过本地事务保证一致)下边是伪代码,这种情况下,本 地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作 与记录消息日志操作具备原子性。
begin transaction;
//1.新增用户
//2.存储积分消息日志
commit transation;
2. 定时任务扫描日志,在第一步中,我们把需要发送到消息队列的事务消 息保存到了消息日志表,为了保证消息能够百分之百的发送给消息队 列,这里可以启动一个定时任务不断扫描这个消息表中的消息发送到消 息队列。当消息队列反馈发送成功后,删除该消息日志,否则等到下一 个任务周期重试
3. 消息的可靠性消费,主流的MQ都带了消息确认机制(ack),消费者监 听MQ的消息。
1. 当消费者受到消息并处理完成后,返回一个ACK给到MQ,告诉MQ 该消息已经消费完成,MQ不需要再向消费者投递该消息。2. 否则,MQ会不断重新投递这个消息给到消费者。 当消费者收到“新增积分”消息后,根据该消息的逻辑规则完成指定用户 的积分更新,再基于MQ的ACK机制,从而可以实现可靠的消息投递功能
4.2 消息重复投递的幂等性保障
所谓幂等性,就是MQ重复调用多次产生的业务结果与调用一次产生的业务结果相同。
在分布式架构中,我们调用一个远程服务去完成一个操作,除了成功和失败 以外,还有未知状态,那么针对这个未知状态,我们会采取一些重试的行为;或者在消息中间件的使用场景中,消费者可能会重复收到消息。对于这两种情况,消费端或者服务端需要采取一定的手段,也就是考虑到重发的情况下保证数据的安全性。一般我们常用的手段:
- 消息表 用MD5+唯一约束
- redis SetNx(md5,5min)
- 状态机 数据的状态(当前状态在状态机中只会存在一次)
- 上游生成唯一id