黄东旭解析 TiDB 的核心优势
1727
2023-10-23
概述
分布式事务是用来解决跨数据库、跨服务更新数据一致性问题的。那么这里的一致性指的是什么,什么是强一致性,什么是弱一致性,与CAP理论中的一致性概念是一样的吗?本文将为您深入解答相关的问题。
一致性指什么
在数据库的理论中,事务具备大家都熟悉的ACID特性,分别如下:
Atomicity(原子性):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复到事务开始前的状态,就像这个事务从来没有执行过一样。
Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。完整性包括外键约束、应用定义的等约束不会被破坏。
Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
对于这里面的C(一致性),我们以一个非常具体的业务例子,来进行解释。假如我们正在处理一个转账业务,假设是A转给B 30元,在本地事务的支持下,我们的用户看到A+B的总金额,在整个转账前后,以及转账过程中,都是保持不变的。那么这个时候用户认为他看到的数据是一致的,符合业务约束的。
当我们业务变复杂,引入多个数据库和大量微服务时,上述本地事务的一致性,依旧是业务非常关心的。假如一个业务更新操作,跨库或者跨服务时,那么此时业务关心的一致性问题,就变成了分布式事务中的一致性问题。
在单机本地事务中,A+B的总金额在任何时刻去查(以常见的ReadCommitted或ReadRepeatable隔离级别),都是不变的,也就是业务约束一直都保持的这种一致性,我们称之为强一致性。
无法做到强一致
目前在跨库、跨服务的分布式实际应用中,尚未看到有强一致性的方案。
我们来看看一致性级别最高的XA事务,是否是强一致的,我们以跨行转账(在这里,我们以跨库更新AB来模拟)作为例子来说明,下面是一个XA事务的时序图:
在这个时序图中,我们在如图所示的时间点发起查询,那么我们查到的数据,将是A+B+30,不等于A+B,不符合强一致的要求。
理论上的强一致性
我们接下来思考,普通XA事务不是强一致的,但假如完全不考虑性能因素,有没有可能在理论上做到强一致:
我们先看看如果我们把XA事务涉及的数据库,隔离级别设定到Serializable,是否能到到强一致的效果呢?我们来看看前面的时序场景:
这种情况下,查到结果等于A+B,但是又有另一些场景出现了问题,如下图所示:
按照图中时序查询的结果是:A+B-30,依旧是不一致。
深入思考这个强一致的问题之后,有一种做法可以做到强一致,做法如下:
在上述策略下,我们可以看到,在时序图任何一个时间点进行查询,获得的结果都是A+B
在T0时间查询,那么修改一定发生在查询全部完成之后,所以查询得到结果A+B
在T1,T2,T3查询,那么查询结果返回一定全部发生在修改完成之后,所以查询得到结果也是A+B
很明显这种理论上的强一致,效率极低,所有有数据交集的数据库事务都是串行执行,而且还需要按照特定的顺序查询/修改数据,因此成本极高,几乎无法应用在生产中。
NewSQL的强一致性
我们讨论了跨库、跨微服务的分布式事务是无法做到强一致的,其实还有一种分布式数据内部的事务,因为事务跨节点了,也被成为分布式事务。这种分布式事务是可以做到强一致的,这种强一致是通过MVCC的技术达到的,原理和单机的数据库类似,但复杂很多。
未来有没有可能借鉴NewSQL的这种方式,来实现跨库、跨微服务这类分布式事务的强一致性?理论上是可以的。
实现跨服务但不跨库的分布式事务一致性,会相对简单一些,其中一种方式就是实现XA事务中的TMRESUME选项(因为最终只有一个xa commit,不会出现两个xa commit中间的不一致时间窗口)。
实现跨数据库的分布式事务一致性,会困难很多,因为各个数据库的内部版本机制都不一样,想要协同非常困难。
弱一致性的分类
既然现有的各种分布式事务方案都无法做到强一致,那么弱一致性之间是否有差别呢?我们进行了以下关于一致性强弱的分类:
一致性由强到弱分别是:
XA事务>消息>TCC>SAGA
这里的消息指的是本地消息表这种类型的分布式事务,关于这四种分布式事务的介绍
他们的分类为:无中间态:数据只有两个状态,事务前和事务后,没有其他第三种状态。XA、消息这两种都是这种有中间态:数据有中间态,例如TCC的Try,数据状态和事务前事务后都不一样;SAGA也有中间态,假如一个SAGA事务执行正向操作后数据为W,又回滚了,那么W也与事务前事务后的状态不同。
XA:XA虽然不是强一致,但是XA的一致性是多种分布式事务中,一致性最好的,因为他处于不一致的状态时间很短,只有一部分分支开始commit,但还没有全部commit的这个时间窗口,数据是不一致的。因为数据库的commit操作耗时,通常是10ms内,因此不一致的窗口期很短。
消息:消息型在第一个操作完成后,在所有操作完成之前,这个时间窗口是不一致的,持续时长一般比XA更久。
TCC:TCC的中间态,通常可控,可以自定义。通常情况下,这部分数据不展示给用户,因此一致性比后面的SAGA要好。
SAGA:SAGA如果发生回滚,而子事务中正向操作修改的数据会被用户看到,可能给用户带来较差的体验,因此一致性是最差的。
CAP理论中的一致性我们这里讨论的一致性是指数据库中的一致性概念,与CAP中的一致性不同。
CAP中的强一致性是指用户在分布式系统中写完之后,立刻去读,如果能够像本地读写那样,读到最新版本,那么是强一致性。
分布式事务中的强一致性,是指事务进行的过程中,用户读取的数据始终满足业务约束,目前在实际应用中的方案,都无法做到强一致。
上述两者的强一致性在具体的含义上是不同的,但从用户的视角看,也有共通性,即能否像单机系统一样,不需要关心分布式带来的新问题。读者通常会有另一个疑问,那就是分布式事务是一个分布式系统,那么在CAP中的一致性如何?
当前Paxos/Raft等分布式共识协议已经在工业领域有了成熟的实现,当遇见机器故障或网络隔离的情况时,可以做到大约几百个毫秒到几秒内选举出新的leader,从故障中恢复。也就是说CAP中,选择CP,在A上面只有大约几百个毫秒的不可用时间。因此对于NewSQL或者分布式事务这类数据敏感性应用,一般都选择CAP中的CP,而牺牲几百毫秒的A。因此在这方面,分布式事务是CAP中强一致的。例如我们的dtm分布式事务框架,将全局事务进度保存在CP的数据库中(云厂商大多提供了CP的数据库)
总结:
本文详尽的分析了分布式事务中一致性相关的问题,在确认没有强一致性方案的情况下,分析了弱一致性分类及理论上可能的强一致方案。
分布式事务数据一致性解决方案
Base理论 Basically Available 基本可用 Soft state 软状态 Eventually consistent 最终一致性 三个短语的 缩写,通过牺牲强一致性来获取最终一致性,当出现故障时允许部分不可用但要保证核心功能可用,允许数据在一段时间内不一致,但最终达到一致状态,满足BASE理论的事物,称之为柔性事务。
(1)基本可用:分布式系统出现故障,允许损失部分可用功能,但是要保证核心功能可用,例如电商系统中,交易付款出现故障,但是商品依然可以正常浏览
(2)软状态:不要求强一致性,所以BASE允许出现中间状态(也叫软状态),如支付中、数据同步中等,等到数据最终一致后再将状态改为成功状态。
(3)最终一致:经过一段时间后,所有的分布式节点数据达到一致状态,如支付中变为支付成功或支付失败,但需要一定时间的延迟
一、2PC
将事务流程分为准备Prepare phase 提交commit phase两个阶段。整个事务过程分为事务管理器和事务参与者两部分,事务管理器决定整个分布式事务的提交和回滚,事务参与者负责自己本地事务的提交和回滚。大部分关系型数据库如***、mysql均支持两阶段提交协议
(1)准备阶段:事务管理器给各个事务参与者发送prepare消息,每个参与者在本地执行事务,并写本地的Undo/Redo log,此时事务并没有提交(undo log记录修改前数据用于回滚,redo log记录修改后数据,用户提交事务时写入数据文件)
(2)提交阶段:若事务管理器收到某事务参与者执行失败的消息,直接给每个参与者发送回滚rollback 消息;否则,发送提交commit消息,参与者根据收到的事务管理器的指令执行提交或回滚操作,并释放在本地事务处理过程中占用的锁资源。
1.XA方案
如注册用户送积分
(1)应用程序AP持有用户库和积分库两个数据源
(2)应用程序AP通过TM通知用户库RM新增用户信息,同时通知积分库RM为该用户新增积分,各RM此时并未提交事务,该用户和积分资源锁定
(3)TM收到执行失败回复,则向其他RM发起通知回滚事务,回滚完毕,释放锁资源
(4)收到全部执行成功回复,向各RM发出提交通知,提交完毕,释放锁资源
AP应用程序、TM事务管理器、RM资源管理器可以理解为事务参与者,DTP模型定义了TM和RM的接口通讯规范称为XA,简单理解为数据库基于XA协议来实现2PC称为XA方案。TM向AP提供应用程序编程接口,AP通过TM来提交或回滚事务;TM中间件通过XA接口来通知RM数据库事务开始、结束、提交或回滚。
XA方案的缺点:需要数据库支持XA协议;资源锁只有等到两个阶段完成之后才会释放,系统的可用性较差。
2.Seata方案
Seata是阿里开源的一个分布式事务解决方案,它支持2PC模式(也支持TCC模式)。Seata不要求数据库支持XA协议,且不长时间持有资源锁,性能比较好,是工作在应用层(业务层)的中间件。对代码0侵入,基本上加一个注解就可以了。
Seata也包含了全局事务和分支事务两部分,且有三个组件来完成分布式事务的处理过程:
(1)Transaction Coordinator TC事务协调器,是一个独立的中间件,需要独立部署运行,它维护全局事务的运行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各分支事务的提交与回滚,会有一个单独的seata-server服务需要部署。
(2)Transaction Manager TM事务管理器,TM需要嵌入到应用程序中工作,负责开启一个全局事务,并最终向TC发起全局事务的提交或回滚
(3)Resource Manager RM资源管理器,控制分支事务,负责分支注册、状态汇报,接收TC指令用于驱动本地事务的提交或回滚
再以用户注册送积分服务解释一下Seata 2PC的执行过程:
(1)用户服务上开启TM向TC申请创建全局事务并获得XID,使用@GlobalTransactional开启一个TM,从TC获取全局事务ID XID
(2)用户服务RM向TC注册分支事务,执行用户创建逻辑,并将其纳入全局事务XID的管辖范围,会获取一个branchId 分支事务id,此时branchId和XID已经在TC绑定
(3)用户服务执行本地事务,将数据存入用户信息库中并写undo_log表,向TC上报分支事务执行结果,与XA方案不同此时用户服务本地事务已提交,持用资源锁的时间短
(4)下一步执行送积分,调用积分服务(在调用链路上会将XID传递),向TC注册分支事务,执行执行送积分逻辑,将本地事务纳入XID的管理范围
(5)送积分本地事务执行,向积分库中插入数据,记录undo_log,积分服务执行完成,上报执行结果,返回用户服务
(6)用户服务执行完成
(7)TM判断分支事务是否都执行成功,向TC发起针对XID全局提交或回滚通知
(8)TC调度XID管辖下的所有分支事务执行提交或回滚,如果需要回滚则根据undo_log回滚数据,完成后会删除undo_log记录
由于Seata在第一阶段各分支事务就会提交本地事务,所以各RM中需要有一个undo_log表,用于在TC通知RM回滚时,回滚数据。
Seata与传统XA实现2PC的区别
(1)架构层次方面,传统的RM实际作用在数据库层,通过XA协议实现,而Seata的RM是以注解的形式放在请求发起方,开启全局事务,在整个调用链路上传递XID,将所有分支事务纳入全局事务管理之中,用单独的TC服务去执行提交或回滚,不再要求数据库支持XA协议
(2)传统2PC本地事务的提交要等到第二阶段,资源锁需要持有到第二阶段结束才释放,对服务性能影响较大,Seata在第一阶段就会提交本地事务释放锁资源,减少锁的持有时间。
二、TCC
TCC是预处理Try、确认Confirm、撤销Cancel的缩写。在Try中做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作即回滚。TM首先发起所有分支事务的Try操作,当所有的分支事务都执行成功,则执行所有分支的Confirm,若有Try执行失败,TM将发起全部分支的Cancel操作,其中Confirm/Cancel执行失败时会进行重试或人工处理,默认当Try都成功了,Confirm一定会成功。TM事务管理器 创建全局事务,生成全局事务id,由于Confirm和Cancel失败需要重试,所以它要实现幂等。在每一步都要提交事务,使用的是最终一致性。
Seata方案 也支持TCC,但是需要实现try confirm cancel三个操作业务侵入性比较强。
三、可靠消息最终一致性
当事务发起方完成本地事务后发出一条消息,事务参与方接收消息消费进行自身事务的处理,此种方案只强调发送消息给事务参与方且数据保持最终一致性。可靠消息最终一致性需要解决以下几个问题:
(1)本地事务与消息发送的原子性问题,即事务发起方在本地事务执行完成后,必须保证消息能发出去,本地事务和消息发送两者要有原子性,要么都成功要么都失败。
(2)事务参与方接收消息的可靠性,事务参与方必须要能从消息队列接收到消息,接收失败可以重复接收。
(3)消息重复消费问题,事务参与方要保证处理消息处理幂等性。
1.本地消息表方案
通过本地事务保证事务发起方业务操作成功时将需要发送的消息存一条记录到消息表中,将其标为发送中,将消息通过消息中间件如kafka进行发送;
消息接收方收到消息后进行处理,要实现幂等,处理成功后反过来发送一条处理成功的消息给消息发送方,发送方收到消息后将消息表中对应记录改为发送成功;
发送方通过一个定时任务扫描本地消息表中处于发送中的消息,重复通过中间件发送消息,直到收到处理成功的消息。
利用ack机制保障消息一定发送到对应topic的partition leader所在的消息中间件broker上,由消息接收方实现消息重复消费幂等。本地业务与消息表落库在同一个本地事务中。
2.RocketMQ方案
RocketMq支持事务消息,保证本地事务和消息发送的原子性,执行流程如下所示,以注册送积分介绍下执行流程
RocketMQ提供了一个接口叫RocketMQLocalTransactionListener,其中有两个方法一个叫excuteLocalTransaction为当提交(half)prepare消息成功后该方法用于执行本地事务,重写该方法执行本地事务操作,返回值就是事务的执行结果;另一个方法是checkLocalTransaction,该方法就是用于查询本地事务的执行结果。需要定义一个listener去实现RockMQLocalTransactionListener接口,重写这两个方法,实现类上需要使用@RocketMQTransactionListener注解,在注解中标明这是哪个group的listener(使用rocketMq发送消息时需要指明group),在其中实现执行本地事务和事务执行结果查询的方法。发送消息的时候可以使用UUID等方法获取一个事务id用于记录事务的状态,便于checkLocalTransaction回查执行结果,以及在消费者端做幂等。
再以注册送积分为例介绍下执行流程:
(1)Producer发送给用户增加积分消息至MQ Server,MQ Server将此消息标记为prepared状态,此时消费者是无法消费该消息的
(2)MQ Server回执Producer消息已经接收成功
(3)Producer执行本地事务,将用户信息存入数据库中,若事务执行成功则向MQ Server发送commit消息,事务执行失败就发送rollback消息
(4)MQ Server接收到执行结果消息后根据消息类型判断是将消息标为可消费,此时消费者就可以正常消费消息,还是将该消息丢弃
(5)当MQ Server一段时间内都没有接收到事务的执行结果时,就会利用Producer提供的回查接口,定时请求查询本地事务执行结果,通过执行结果将消息标记为可消费或丢弃
利用RocketMq的消息事务机制,将生产者本地数据库事务与发送消息到broker绑定在一起,保证当生产者本地数据修改成功之后,通知与其协作的其他服务数据修改的消息一定会被写入到broker中;之后由消费者从broker中读取消息,进行处理,在customer端需要保证幂等,避免重复进行数据处理。当消息处理完毕之后,需要写入到一张记录表中,每一次消费新消息时,都要从该表中进行查询,看它之前有没有被处理过,如果已经处理过,则直接丢弃或返回与上次执行相同的结果。如果消费者端处理消息失败,阿里给我们的建议是人工进行处理,因为如果要代码处理的话,那么就需要回滚整个操作,这个开发业务量是十分巨大的,并且这种应该是小概率事件。这种方式就极大提高了系统的可用性,提供最终一致性,那么到达最终一致性的时间越短,用户体验就越良好。
四、最大努力通知
下面以工作中保司产品对接来举例:
(1)用户在保险经纪平台页面投保,如果不是经纪公司代扣而是对接方保司直接扣款,则在点击立即支付时需要调用保司支付接口,当然参数中会给出用于标示该保单的no,支付成功/失败/超时回调地址,保司支付接口会返回支付页面url
(2)当用户在保司支付页面完成相应操作后,保司会根据我们传递的回调地址去进行结果通知
(3)当收到通知后根据根据通知内容修改保单的状态
(4)当然经纪公司这边还需要有一个定时任务用于主动去查询某段时间内的保单状态,避免没有收到回调通过造成保单状态不一致或检查当前保单状态是否正确
(5)发起通知方也通过一定的机制最大努力将业务处理结果通知到接收方,请求回调接口失败时按一定的频率进行多次重试;在上面举的例子中,保司就是通知方,保险经纪平台就是接收方。
最大努力通知有以下要求:
(1)有一定的消息重复通知机制,也就是当保司请求经纪平台支付结果回调接口时如果返回了错误,则需要重复推送通知,例如失败后每隔1分钟再通知一次,超过5次后每10分钟通知一次,再超过5次后,每半个小时通知一次,最多15次;与通知方支持重复通知相对的,接收方相对的接收方也需要实现通知消息处理的幂等
(2)消息校对机制,如果尽最大努力也没有通知成功,或消息接收方有通知结果校验的机制,消息通知方需要提供一个接口用于让接收方查询操作处理的结果。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。