必修课!深度解析金融级分布式数据库一致性技术

2022-09-28 17:41:17 浏览数 (1)

作为国民经济的命脉和枢纽,金融行业对底层数据库的能力要求正在不断提高。在众多要求中,数据一致性无疑是重中之重,即数据不能出错,最好还能提高并发效率。

TDSQL采用MC(轻量级GTM) 全局MVCC的全局读一致性方案。如果只使用全局事务管理器GTM,除需维护全局序列外,还需要维护全局的事务冲突,这个过程的通信量及与GTM之间的通信频率都会成为瓶颈。TDSQL引入全局MVCC,将每个分片上MVCC版本和全局GTS做映射,通过全局GTS和全局的MVCC映射来管理每个分片上的镜像,进而实现全局的MVCC,从而极大减少和GTM 之间的通信量及避免全局的冲突事务检测。

上述方案确保了TDSQL无任何数据异常,且具备高性能的可扩展性,解决了分布式数据库在金融级场景应用的最核心技术挑战,使得国产分布式数据库实现在金融核心系统场景的可用、好用,进而推动国产基础软件产业化。基于此,TDSQL是当前国内率先进入国有大型银行核心系统正式投产的国产分布式数据库。

在WOT全球技术创新大会2022的腾讯云数据库专场中,腾讯云数据库专家架构师汪泗龙分享了金融级分布式数据库TDSQL的一致性技术及应用实践,以下为详情内容:

TDSQL产品介绍

1.1 发展历程

腾讯云数据库TDSQL诞生自腾讯内部百亿级账户规模的金融级场景。从内部自研蜕变成规模化商业产品,TDSQL的发展历程可分为四个阶段:

第一阶段是2007-2009年,当时开源的MySQL已经越来越难以应对腾讯爆发式增长的业务,研制服务于计费、定位于金融场景的分布式数据库TDSQL逐渐提上日程。

第二阶段是2009-2012年,腾讯进入开放时代,海量业务群雄并起,以开心农场等为代表的众多亿级应用比比皆是。TDSQL逐渐在性能瓶颈、数据可靠性保障、高可用等“不可能三角”的技术难题上取得突破。

第三阶段是2012-2014年,云计算兴起,数据库上云、多租户、标准化成为标配。腾讯云数据库的能力逐渐外溢,TDSQL因其优异的性能已经拥有众多外部客户。在经过公有云的海量数字化、大规模高并发业务场景打磨以及内核级的深度自研优化后,TDSQL逐渐形成标准化的国产分布式数据库产品,包括金融级分布式TDSQL、计算与存储分离的云原生数据库TDSQL-C 等产品,获得了云原生技术、多租户隔离能力。

第四阶段是2014-2020年,数字化升级成为行业趋势,TDSQL深入金融核心,走向大规模应用阶段,比如作为微众银行分布式数据库底座承担核心作用,帮助张家港农商银行上线新一代核心业务系统,助力平安银行打造信用卡“A ”新核心系统等等。

目前,TDSQL已服务金融、政务、工业制造等行业超过50万家客户,帮助20余家金融机构完成核心替换,国内TOP 10银行机构服务占比超过60%。同时TDSQL也支持了第七次全国人口普查,以及腾讯会议、健康码等关系国计民生的数字化应用,有力推进国产数据库的技术创新和发展。

1.2 核心特性

随着云计算和数字化业务的发展,针对新型企业级信息化以及实现国产化的转型升级需求,TDSQL逐渐形成6大核心特性:

  • 数据强一致性:确保多副本架构下数据强一致,避免故障后导致集群数据错乱和丢失。
  • 金融级高可用:确保99.999%以上高可用;跨区容灾;同城双活;故障自动恢复。
  • 高性能低成本:软硬结合;支持读写分离、秒杀、红包、全球同服等超高性能场景。
  • 企业级安全性:数据库防火墙;透明加密;自动脱敏;减少用户误操作/黑客入侵带来的安全风险。
  • 线性水平扩展:无论是资源还是功能,均提供良好的扩展性。
  • 便捷的运维:完善的配套设施,包括智能DBA、自助化运营管理台。

1.3 产品架构

TDSQL的架构主要分为三层。最上层是管理层,包含赤兔管理平台、MC以及Keeper。中间层是SQL引擎,可以选择使用TDSQL附带的接入层,也可使用传统意义上的F5等接入层来进行接入。最下层是存储引擎层,即TDSQL内核。

下图是实现 GTM 全局唯一序列的图签。这里是TDSQL在实现分布式事务后在全局一致性读方面的优化点,该插件实现了轻量级的GTM。

分布式事务处理

事务处理面临的挑战有两点:保障数据正确性及提高并发效率。这两点都是事务处理的关键点,如果为了提高并发效率而牺牲数据正确性,就背离了初衷,反之也是如此。为了解决这两个问题,我们需要用到很多技术,比如读写分离、物理时钟、时间戳机制、GTM等。

2.1 分布式事务模型

不管是分布式数据库还是应用,都是在分布式事务模型下,进行分布式事务的实践和开发。对数据库而言,常用的分布式事务模型是XA模型;对应用而言,主要是TCC、SAGA、AT等模型。

Oracle、MySQL、TDSQL等数据库,通常使用XA模型来进行分布式事务处理。在XA模型里,事务主要采用两阶段提交方式,先进行PREPARE,再进行COMMIT,也称之为两阶段事务。

2.2 数据异常问题

事务模型简化后划分为读写两类操作,组合下来有四种场景,即读写、写读、读读、写写。存在冲突(数据异常)的场景主要是写写、写读、读写,具体的冲突如下:

  • 写写冲突:会出现脏写、丢失回滚。
  • 写读冲突:会出现脏读、幻读、不可重复读、读偏序等。
  • 读写冲突:会出现脏写、丢失更新等。
  • 其他异常:主要是写偏序,即违背语义(约束)的异常。两笔事务同时发生,每笔事务都满足约束。但在分别进行的过程中,因为没有提前加锁,没有满足语义,导致最终事务完成后违背了语义约束。

针对上述问题,我们可以通过数据库的并发控制算法来解决:

  • 基于锁(两阶段锁)进行控制:两阶段锁除我们常用的2PL外还有S2PL、SS2PL,主要是锁定阶段的不同。在种类上主要有S锁、X锁、U锁、间隙锁。
  • 基于MVCC的方案:将读写进行分离,支持多版本和快照。
  • 基于时间戳进行控制:主要有三个时间戳,即事务启动时的时间戳、数据读的时间戳以及数据写的时间戳。
  • 基于有效性确认来进行控制

我们需要根据不同的场景来使用不同的算法。在分布式事务处理方面,TDSQL使用的是2PL加上MVCC特性。

2.3 全局读不一致问题

我们以下图为例说明全局读不一致问题。有A、B两个账户,账户余额均为100元。有两个事务,事务X和事务Y,事务X是A给B转100元,事务Y是读取A和B的余额。当事务Y发起查询时,事务X中(NA分片属于COMMITting状态,NB分片属于COMMITed状态),这时读到两人余额总和为300元,这就是分布式事务进行过程中的全局读不一致问题。

出现这种全局读不一致的情况,主要原因在于这里没有全局一致的MVCC版本,而是依赖每个分片各自的MVCC特征进行实现,我们读到的NA片还没提交,因此读到的数据是不一致的数据。针对上述异常情况,主要有以下几种解决方案:

  • 利用全局事务管理器GTM,GTM会提供一个全局序列来满足使用,还会维护全局的事务冲突列表。
  • 以封锁机制实现全局可串行化,将所有的读写都变成相关的逻辑上的DML操作,实现全局封锁。这个过程可以解决所有的读写冲突,从而实现全局读一致性。但这种模式会带来性能上的问题,导致效率降低。
  • 采用物理时钟排序,其缺点在于成本高,且效率比全局封锁的效率低。
  • 采用混合时间戳机制实现局部偏序排序,但这个方案只限于特定场景。
  • 两次读机制,第一次读发现数据不一致就会发起第二次读,直到可以给到客户一致性的版本。

TDSQL的全局读一致性方案是MC(轻量级GTM) 全局MVCC。只使用全局事务管理器GTM的方案,除需维护全局序列外,还需要维护全局的事务冲突,这个过程的通信量及与GTM之间的通信频率都会成为瓶颈。因此需要引入另一个特性——全局MVCC,这时的读写冲突可通过undo的前镜像完成一个全局的MVCC来实现,解决各副本间的读写冲突。

完全重新开发一套全局MVCC的成本较高,且和InnoDB的兼容性是个问题。因此我们采用了折中方式,我们将每个分片上MVCC版本和全局GTS做映射,通过全局GTS和全局的MVCC映射来管理每个分片上的镜像,进而实现全局的MVCC。这样就可极大减少和GTM 之间的通信量及避免全局的冲突事务检测。

TDSQL分布式事务实践

3.1 TDSQL分布式事务模型

下图为TDSQL分布式事务的实现模型,主要有三个角色:

  • TM:SQLEngine充当事务管理器角色,负责发起分布式事务。
  • RM:DB节点相当于分布式事务的资源节点(RM),作为分布式事务的重要参与者。
  • TC:XID_LOG作为分布式事务协调者的角色,确认PREPARE状态的完成。

通过Client发起一个事务BEGIN后,会往后端发起一个插入语句到Proxy,此时Proxy发起XA Start,到后端的两个SET上,两个SET返回正常后则插入成功。获取到XID后,开始正式进入COMMIT阶段。Client发起COMMIT后,Proxy往后端发时就会带着全局的XID往后提交,这时进入PREPARE和COMMIT阶段。

当所有参与者PREPARE成功后,会插入全局的XID_LOG。XID_LOG是PREPARE阶段的一个平衡点,解决悬挂等待的高成本及超时回滚等问题。一旦事务进入到这个阶段,写入全局的XID_LOG成功后,即使后面的操作失败,我们依然会认为事务是成功的。针对没有提交成功的事务,Agent参与进来,扫描XID_LOG表进行后续处理;如果执行超时,Agent会参与进来,终止掉超时的事务,之后Agent会更新XID_LOG的状态。

Proxy和Agent是协作模式,XID同一时间点只能有一种状态,我们可以通过这种状态来协调Proxy和Agent。如果Proxy Crash掉,Agent进行任务接管,根据XID_LOG的信息决定对事务进行补提交或回滚。在此过程中,Proxy本身是无状态的,这就使得TDSQL具备良好的业务体验。

如果在主备过程中,一个SET发生主备切换,另外一个SET正常,这时Agent会根据全局XID_LOG的状态进行相关补偿或者回滚。如果XID_LOG写成功,我们会进入到最终的COMMIT阶段,两个分片分别COMMIT成功后,会正常反馈给Client端。以上就是TDSQL基于XA两阶段提交的分布式事务模型和原理。

3.2 TDSQL全局一致性读实现方案

分布式数据库,不仅要解决分布式事务的问题,还要解决全局写读冲突,从而实现全局的一致性读。TDSQL具体的实现方案如图所示:

与前面提到的TDSQL分布式事务模型相比,整体架构较为相似,区别在于多了一个MC组件。

MC是全局的轻量级GTM,负责生成全局的唯一序列。以上述两个SET的插入的事务模型为例,整个过程中只是多了两次和GTM之间的获取步骤,其他流程是一致的。

当我们发起一个分布式事务,在CLIENT启动及提交时,我们会在这里再加入一个阶段,获取全局最大的GTS。GTS是标识的全局唯一序列,XID是唯一标识全局事务的标签,两者相互独立分别保证分布式场景下读取一致性和事务一致性。在COMMIT发起后进入两阶段提交,这里需要对PREPARE阶段进行判定,通过XID_LOG日志来作为全局提交成功的标识。在PREPARE成功后,PROXY和MC会进行第二次交互,重新获取COMMIT_GTS,并伴随事务的COMMIT写入到TDSQL REDO日志和缓存中。相对于XA的START和COMMIT,TDSQL为了实现全局的MVCC特性,改写了XA的语法,增加部分关键词,同时对内核做微量调整,以上就是TDSQL实现全局一致性读的方案。

在该方案中,我们和MC的通信量非常少,整个过程中基本只有2次非常轻量的通信,某些场景下当我们进行一个不涉及多分片的事务时,即如果只涉及一个分片,我们会对第二次获取COMMIT_GTS进行优化,进一步减少和MC通信。实际上,TDSQL实现分布式事务和分布式全局一致性的方案,是把InnoDB自身的MVCC和全局GTS进行全局映射,从而实现全局轻量级的GTM 全局的MVCC。

下图是InnoDB的MVCC模型,共有六个事务,当前事务ID是trx6,为活跃状态。众所周知,InnoDB会将trxid直接排序,通过全局事务链表管理维护。我们以下图为例来说明可见性算法是如何对外可见的。

我们过滤时需要过滤事务本身的可见性情况,要看哪些事务对外已经能看到属于活跃事务。我们可以过滤掉trx7和trx5,活跃事务为trx8、trx4、trx3。根据活跃的trxid,我们可以获取到两个id,一个是比较旧的快照即up_limit_id;另一个是比较新的快照地址即low_limit_id。如果当前事务的id小于up_limit_id,说明这属于比较老的快照地址,对当前查询来说事务可见;如果当前事务的id大于等于low_limit_id,说明是非常新的快照地址,事务不可见。当处于两者之间时,就需要判断是否为活跃状态。

我们以下图为例,来说明如何把全局可见性视图串起来。有四行记录,分别是ROW0到ROW3,需要查询一个比较老的快照(up_limit_id:76),我们可以看下每一条记录的查询情况。ROW0行有两个版本,trxid:100和trxid:62。ROW1行有trxid:80、trxid:75、id32三个版本。ROW2只有trxid:20一个版本。对于ROW0,满足条件的是trxid:62。对于ROW1,满足条件的是trxid:75和trxid:32,因为我们找快照时是根据列表找到满足条件的第一个快照,而非找到最老的trx:32,要找的是trx75。另外对于ROW2,当前只有一个版本,且只有20,这时已经满足条件。

InnoDB本身是使用MVCC机制来解决读写并发问题,通过Undo log来对应事务中的读写语句。Undo log记录的是每个旧的镜像版本,当事务需要读取旧版本时,可以通过链表去回溯旧的版本。当需要回滚时,也可以基于Undo来进行相关的数据回滚。

TDSQL实现全局MVCC的原理与InnoDB相似,将全局的trxid和全局事务序列对应,将trxid和全局GTS进行关联,从而实现全局的MVCC。如下图所示,全局场景下会存在全局序列GTS。对于trx6,其GTS值为150,对它可见的GTS必须小于等于150,如果比它大则不可见。因此trx7和trx8对其不可见;trx5的GTS是100,小于150,因此可见;trx4是大于100,在100-150之间因此可见;trx3的GTS值是300,也不可见。

对于trx8、trx7、trx4和trx3,我们要分别找到对应记录行的Undo历史版本。如果找到的Undo项的GTS值依然大,就继续往前找。对于trx8关联的记录行通过undo链的回溯,最终找到的记录所绑定的GTS是30,以此类推对于trx7“最终记录行”是70,trx4是40,trx3是20。这样trx6,trx8、trx7、trx5、trx4、trx3对应“可见的记录行”的GTS值分别是30、70、100、40和20。

回到前文流程,这时全局事务还在COMMTTING过程中,一个事务已经提交,另一个事务没有提交,在另外一个读事务扫描记录行的过程中,读取到前两个写事务时,我们可能都要通过undo获取他们的历史版本。TDSQL的全局可见性算法是把节点局部的trxid和全局GTS进行映射关联,实现全局MVCC,从而消除读写冲突和一致性问题。

我们以一个写事务和读事务的场景为例,开启MC后,TDSQL对PREPARE状态进行了相关处理。当写事务进行UPDATE操作后,这个事务可能为ACTIVE、PREPARE或COMMIT状态,此时需要判断这些状态是可见还是不可见。

对于处于ACTIVE状态的事务,InnoDB下对非当前事务不可见,转换为COMMIT状态后,通过比较GTS,再根据全局层面的可见性来进行判定。同样地,一个PREPARE中的事务,对于InnoDB或TDSQL都处于等待状态。如果等的时间久了,则全局的分布式事务效率会降低,需要进行优化。所以每笔请求时,SELECT都会带上GTS,当扫描到这一行时,会比较数据行的GTS和当前SELECT的GTS,如果SELECT的GTS大于数据行的GTS则可见,否则不可见。当数据行处于PREPARE状态时,我们还不知道它提交的GTS,此时可见性无法判断,需要等到进入到提交状态,才能进行可见性判断。

下图所示的场景中,存在两个UPDATE事务,当第一个节点COMMIT之后,另一个节点仍处于PREPARE状态,这时我们需要等它提交及返回结果。这个过程中它的状态不可知,因此无法比较GTS值,则需要等待,不会读取到中间状态。

3.3 TDSQL全局一致性读的设计优化

TDSQL主要从三个方面针对全局一致性读进行设计优化:

  • 在全局的trxid到GTS进行映射后,每次访问都需要获取一个MAX_GTS和COMMIT_ GTS,对事务来说需要增加两次访问。访问频次提高后,如何实现高效映射这是首先需要考虑的事情。
  • 处于PREPARE状态的记录,会触发等待机制导致吞吐量下降,需要想办法降低等待开销。
  • 对于没有走两阶段提交的非分布式事务,需要尽可能减少和MC的交互,在提交阶段可以将操作进行合并,合并后只获取一次COMMIT_GTS。

下图主要介绍TDSQL在开启MC之后的优化。首先是tlog,tlog实现了本地MVCC和全局MVCC之间的映射关系,主要存储的是GTS和trxid。tlog的整体设计非常简洁,一个tlog里包括4K的数据页,每一页包含4K个字节,一个GTS是64位的无符号整型,占8字节,整个页面是128个字节,128字节中最后的111字节是checksum值。因此每个页面能存储约496个GTS。

第二是trxid和GTS映射。要实现高效的映射,就需要有快速定位的方法。实际上,每个trxid对应到tlog时都有偏移,文件起始的trxid称为start_id,当前的trxid减去stard_id乘以8,就是偏移值。基于这个算法,我们可以快速定位到文件的地址和偏移值,实现快读定位。

第三是tlog和redo。tlog可实现了全局MVCC和GTS映射,这个过程中也需要对XA语句进行调整。我们对这块进行了微调兼容XA语法,每次XA Start和XA COMMIT时,都会携带GTS值,把GTS写到redo的同时也写到tlog中。为了避免tlog刷新带来频繁的随机读写问题,TDSQL采用WAL机制,tlog文件以内存映射方式提供读写能力。

如图所示,左侧是tlog的buffer,其中tlog buffer和tlogdatafile对应。每次读GTS时,其实是从tlog buffer页面去读。写GTS时也是写tlog buffer同时写入Redo,这样可以保证tlog的持久化。同样当实例crash后,可以在启动阶段构读取到tlog buffer,整个tlog也能构造出一致性。

第四是针对PREPARE的优化。以下图中的场景为例,在T1启动后,当前MySQL节点已经提交的最大GTS为99,所以PREPARE的GTS一定大于99,至少为100,所以此时为PREPARE的记录行绑定了100的GTS。

同时又有活跃事务T2进行了COMMIT操作,其COMMIT的GTS为150和200,所以T2提交后把SET1、SET2两个节点局部缓存的MAX_COMMIT_GTS推进到了150和200。

同时存在的活跃事务T3为一个只读事务,其GTS为250,那么它读取到T1的PREPARE的记录行时事务将会被阻塞,但是如果T3启动时绑定的GTS为99,那么它将直接跳过PREPARE记录,因为通过PREPARE绑定的100 GTS就可以确定,即使将来该记录提交也一定对T3事务不可见。

综上所述,TDSQL针对该流程进行了优化,在进行查询时,如果当前读事务绑定的GTS值比PREPARE的GTS值要小,这时我们不用等PREPARE完成,可以直接去找其undo前镜像,因为该记录“将来的”COMMIT_GTS一定比当前PREPARE的GTS要大。

第五是针对XA的优化。以下图为例,有SET1和SET2,SET1进行单机事务插入,SET2也在进行单机事务插入,即SET1和SET2插入的事务并没有任何因果关系。如果在同一SET上同时发起三笔单机事务,需要为这三次COMMIT请求三次MC获取全局GTS吗?

TDSQL采用了一个优化措施,此时没有请求三次MC获取COMMIT_GTS,而是复用当前MySQL节点上最大的“快照GTS”。如下图第三阶段所示,三个先后开启的非分布式事务,“同时”提交,此时不再请求MC而是本地获取全局最大的快照GTS 253。SET1的GTS与SET2本身没有依赖关系,对SET1而言,发生的是自身的一阶段的分布式事务,所以这里无需通过MC获取全局最新GTS只需要保证事务的局部时序性,这样可以减少和MC之间的通信的开销。

TDSQL在MC开启之后进行多种优化来提升性能。我们在内外部进行多次测试、验证,得出的结论是MC开启之后有一定损耗,但可以有效的控制在10%左右,对业务来说影响比较小。在一些INSERT和UPDATE场景下我们对相关模型也进行了优化调整,所以带来的性能提升非常高。

图中可以看到,我们在一些混合的TPMC场景测试下,在一台16C 64G的机器下测试模型,开启和关闭GTS对性能影响较小。TDSQL使用全局MVCC,再加上轻量的MC特性,可以将GTS的通信次数降低,从而带来较大的收益。

TDSQL应用实践

分布式事务或分布式一致性,不仅仅是数据库要去解决的问题,很多场景下应用下也会面临相同的挑战。对于热点账户的入账或者面对面收账等高并发的场景,数据库仍然存在自身极限,我们认为需要结合应用数据库一起做优化。

常规方案是将账户系统进行切割,包括做单元化、大小账户分离等。但是在这些场景中,一个比较极端的场景是:热点账户,只有一行记录,怎么进行拆分?这种场景下,我们就要结合应用优化配合TDSQL来实现。

以下图为例,有两个账户,账户C和账户B之间进行转账,这个过程中一个减少、一个增加。如果我们通过数据库的分布式事务来进行实现,就会有上限值。如果要谋求更低的处理时效,就需采用异步消息的方式。

比如热点商户账户收单等场景下,仅靠数据库层分布式事务已经不能满足海量的并发场景,采用异步消息优化的同时我们也会采用多级对账机制。应用层会有一个交易订单,交易订单标记着这笔分布式事务成功与否。当交易订单形成后,即使该事务在过程中出错,也可以通过应用层的补偿机制对这个事务进行处理。从而实现实时出账 异步批量入账,减少行锁竞争。

在下图的红包转账场景下,还需要分布式缓存 全局缓存来进一步深入优化。这种场景下,除了采用异步账户外,我们还需要将一个热点账户拆成多个平行子账户,使用消息队列和分布式缓存,通过多级缓存机制,分别实现多级对账、多级缓存、平行热点账户、单元化等实现实时出账、批量异步入账。这样可以将转账逻辑从频繁的网络之间的交互变成一次批量请求,也极大提高了系统的并发能力和事务处理的响应速度。

总的来说,在上述这些极限的高并发场景下,我们需要在应用侧和数据库侧做优化和结合,包括采用异步消息、多级缓存机制等。

作为领先的国产分布式数据库,TDSQL针对新型企业级信息化以及快速实现国产化的转型升级需求,具备数据强一致、金融级高可用、高性能低成本、线性水平扩展、企业级安全、便捷智能运维等核心特性。

目前,TDSQL已服务金融、政务、工业制造等多个行业领域。未来五年,TDSQL将帮助1000家金融机构实现核心系统国产化转型,持续助力国产数据库未来发展。

-- 更多精彩 --

和一群技术爱好者聊聊分布式,结果......

干货!分布式数据库在金融核心场景的落地实践

↓↓点击阅读原文,了解更多优惠

0 人点赞