• 微服务数据库分库设计解决方案(跨库关联查询、分布式事务处理)



    前言

    现在互联网应用已经普及,数据量不断增大。对BAT等互联网业务来说,传统单实例数据库很难支撑其性能和存储的要求,所以数据库拆分势在必行。
    同时数据库分库后,又面临跨库关联查询、分布式事务等问题,如何解决?


    一、微服务数据库分库设计

    1.烟囱式的微服务数据库设计

    示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。
    避免使用烟囱式的微服务数据库设计,当商品表变更时,需要更改多个微服务,维护成本高。

    2.小而专的微服务数据库设计

    在这里插入图片描述
    应采用小而专的微服务数据库设计,各个微服务管理各自的数据表,其他微服务需要读取时,应访问对应的微服务接口。

    3.按照业务纵向拆分

    在这里插入图片描述
    按照业务纵向拆分数据库后,将面临跨库表关联查询问题和分布式事务问题,如何解决呢?


    二、跨库关联查询解决方案

    1.普通查询-数据补填

    在这里插入图片描述

    如上图,查询订单表时,需要返回客户信息,如何处理呢?

    • 分页查询:采用分页方式一次返回有限小的数据,然后对关联的数据进行补填,完善相关信息后,返回数据给前端。
    • 数据补填:数据需要返回关联信息时,可通过调用关联数据的微服务接口进行数据补填(待关联数据量要小)。
    • 借助缓存:在调用关联数据的微服务接口时,在微服务中增加缓存的设计,补填的时候先查询缓存数据,再读数据库。
    • 一次性调用:在补填数据时,如果逐条数据的补填,会产生多次微服务接口调用;可设计将多条记录的关联ID进行一次性远程调用,一次性查询,一次性补填。

    2.个别字段条件查询

    查询订单时,需要通过客户姓名进行条件查询,如何处理呢?

    • 如果数据库采用3NF的设计思路,在进行数据查询时,会涉及到数据关系查询的问题;这时,在设计时,可适当的包含部分冗余数据(空间换时间),将客户表中部分关联查询内容包含到待查询表中(如姓名),通过数据冗余设计,解决数据跨库查询的问题。

    3.全字段条件查询

    查询订单时,需要通过客户表中全部字段进行条件查询,如何处理呢?

    • 如果查询的过滤条件很多,每次用到的查询条件都不同,应采用读写分离,设计查询库解决。
    • 为了提高查询库的查询性能,需要在同步到查询库时提前完成join操作,之后将数据直接制作成单表进行分布式存储,这种设计叫“宽表”。在这样的宽表中进行单表查询,可以实现海量数据的秒级查询。

    三、分布式事务解决方案

    1. 2PC(两阶段提交):强一致性

    方案简介

    二阶段提交协议(Two-phase Commit,即 2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段。事务的发起者称协调者,事务的执行者称参与者。

    在分布式系统里,每个节点都可以知晓自己操作的成功或者失败,却无法知道其他节点操作的成功或失败。

    当一个事务跨多个节点时,为了保持事务的原子性与一致性,而引入一个协调者来统一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚(rollback)。

    二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

    核心思想就是对每一个事务都采用先尝试后提交的处理方式,处理后所有的读操作都要能获得最新的数据,因此也可以将二阶段提交看作是一个强一致性算法。

    处理流程

    简单一点理解,可以把协调者节点比喻为带头大哥,参与者理解比喻为跟班小弟,带头大哥统一协调跟班小弟的任务执行。

    阶段 1:准备阶段

    准备阶段有如下三个步骤:

    • 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待所有参与者答复。
    • 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
    • 如参与者执行成功,给协调者反馈 yes,即可以提交;如执行失败,给协调者反馈 no,即不可提交。

    阶段 2:提交阶段

    如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息。

    参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源) 接下来分两种情况分别讨论提交阶段的过程。

    image

    情况 1,当所有参与者均反馈 yes,提交事务,如上图:

    • 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
    • 参与者执行 commit 请求,并释放整个事务期间占用的资源。
    • 各参与者向协调者反馈 ack(应答)完成的消息。
    • 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
      在这里插入图片描述

    情况 2,当任何阶段 1 一个参与者反馈 no,中断事务,如上图:

    • 协调者向所有参与者发出回滚请求(即 rollback 请求)。
    • 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
    • 各参与者向协调者反馈 ack 完成的消息。
    • 协调者收到所有参与者反馈的 ack 消息后,即完成事务中断。

    方案总结

    2PC 方案实现起来简单,实际项目中使用比较少,主要因为以下问题:

    • 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
    • 可靠性问题:如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态。
    • 数据一致性问题:在阶段 2 中,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。

    2. 3PC(三阶段提交)方案

    方案简介

    三阶段提交协议,是二阶段提交协议的改进版本,与二阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制。

    三阶段提交将二阶段的准备阶段拆分为 2 个阶段,插入了一个 preCommit 阶段,使得原先在二阶段提交中,参与者在准备之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。

    处理流程

    阶段 1:canCommit

    协调者向参与者发送 commit 请求,参与者如果可以提交就返回 yes 响应(参与者不执行事务操作),否则返回 no 响应:

    • 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
    • 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。

    阶段 2:preCommit

    协调者根据阶段 1 canCommit 参与者的反应情况来决定是否可以进行基于事务的 preCommit 操作。根据响应情况,有以下两种可能。

    image

    情况 1:阶段 1 所有参与者均反馈 yes,参与者预执行事务,如上图:

    • 协调者向所有参与者发出 preCommit 请求,进入准备阶段。
    • 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
    • 各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。
      image

    情况 2:阶段 1 任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务,如上图:

    • 协调者向所有参与者发出 abort 请求。
    • 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。

    阶段 3:do Commit

    该阶段进行真正的事务提交,也可以分为以下两种情况。

    image

    情况 1:阶段 2 所有参与者均反馈 ack 响应,执行真正的事务提交,如上图:

    • 如果协调者处于工作状态,则向所有参与者发出 do Commit 请求。
    • 参与者收到 do Commit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
    • 各参与者向协调者反馈 ack 完成的消息。
    • 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
      image

    情况 2:阶段 2 任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务,如上图:

    • 如果协调者处于工作状态,向所有参与者发出 abort 请求。
    • 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
    • 各参与者向协调者反馈 ack 完成的消息。
    • 协调者收到所有参与者反馈的 ack 消息后,即完成事务中断。

    注意:进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 do Commit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。

    方案总结

    优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。

    缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

    3. TCC 事务:最终一致性

    方案简介

    TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。

    TCC 是服务化的二阶段编程模型,其 Try、Confirm、Cancel 3 个方法均由业务编码实现:

    • Try 操作作为一阶段,负责资源的检查和预留。
    • Confirm 操作作为二阶段提交操作,执行真正的业务。
    • Cancel 是预留资源的取消。
      TCC 事务的 Try、Confirm、Cancel 可以理解为 SQL 事务中的 Lock、Commit、Rollback。

    处理流程

    为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上。

    ①Try 阶段

    从执行阶段来看,与传统事务机制中业务逻辑相同。但从业务角度来看,却不一样。

    TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:

    • 完成所有业务检查( 一致性 ) 。
    • 预留必须业务资源( 准隔离性 ) 。
    • Try 尝试执行业务。
      TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。

    因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。

    image

    假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。

    ②Confirm / Cancel 阶段

    根据 Try 阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。

    Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。

    Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作

    image

    这里使用的资源一定是 Try 阶段预留的业务资源。在 TCC 事务机制中认为,如果在 Try 阶段能正常的预留资源,那 Confirm 一定能完整正确的提交。

    Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。

    Cancel:当 Try 阶段存在服务执行失败, 进入 Cancel 阶段

    image

    Cancel 取消执行,释放 Try 阶段预留的业务资源,上面的例子中,Cancel 操作会把冻结的库存释放,并更新订单状态为取消。

    方案总结

    TCC 事务机制相对于传统事务机制(X/Open XA),TCC 事务机制相比于上面介绍的 XA 事务机制,有以下优点:

    • 性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
    • 数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
    • 可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。

    缺点:
    TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。

    4. 本地消息表:最终一致性

    方案简介

    本地消息表的方案最初是由 eBay 提出,核心思路是将分布式事务拆分成本地事务进行处理。

    方案通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。

    这样设计可以避免”业务处理成功 + 事务消息发送失败",或"业务处理失败 + 事务消息发送成功"的棘手情况出现,保证 2 个系统事务的数据一致性。

    处理流程

    下面把分布式事务最先开始处理的事务方称为事务主动方,在事务主动方之后处理的业务内的其他事务称为事务被动方。

    为了方便理解,下面继续以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤。

    库存服务和订单服务分别在不同的服务器节点上,其中库存服务是事务主动方,订单服务是事务被动方。

    事务的主动方需要额外新建事务消息表,用于记录分布式事务的消息的发生、处理状态。

    整个业务处理流程如下:

    image

    步骤1:事务主动方处理本地事务。

    事务主动方在本地事务中处理业务更新操作和写消息表操作。上面例子中库存服务阶段在本地事务中完成扣减库存和写消息表(图中 1、2)。

    步骤 2:事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。

    消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。

    上面例子中,库存服务把事务待处理消息写到消息中间件,订单服务消费消息中间件的消息,完成新增订单(图中 3 - 5)。

    步骤 3:事务被动方通过消息中间件,通知事务主动方事务已处理的消息。

    上面例子中,订单服务把事务已处理消息写到消息中间件,库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中 6 - 8)。

    为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等。

    具体保存一致性的容错处理如下:

    • 当步骤 1 处理出错,事务回滚,相当于什么都没发生。
    • 当步骤 2、步骤 3 处理出错,由于未处理的事务消息还是保存在事务发送方,事务发送方可以定时轮- 询为超时消息数据,再次发送到消息中间件进行处理。事务被动方消费事务消息重试处理。
    • 如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。
    • 如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。

    方案总结

    方案的优点如下:

    • 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
    • 方案轻量,容易实现。

    缺点如下:

    • 与具体的业务场景绑定,耦合性强,不可公用。
    • 消息数据与业务数据同库,占用业务系统资源。
    • 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。

    5.MQ 事务:最终一致性

    方案简介

    基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。

    处理流程

    下面主要基于 RocketMQ 4.3 之后的版本介绍 MQ 的分布式事务方案。

    在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ,相对于提供了 2PC 的提交接口,方案如下:

    正常情况:事务主动方发消息

    image

    这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:

    • 图中 1:发送方向 MQ 服务端(MQ Server)发送 half 消息。
    • 图中 2:MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功。
    • 图中 3:发送方开始执行本地事务逻辑。
    • 图中 4:发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
    • 图中 5:MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。
      异常情况:事务主动方消息恢复

    image

    在断网或者应用重启等异常情况下,图中 4 提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:

    • 图中 5:MQ Server 对该消息发起消息回查。
    • 图中 6:发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
    • 图中 7:发送方根据检查得到的本地事务的最终状态再次提交二次确认。
    • 图中 8:MQ Server基于 commit/rollback 对消息进行投递或者删除。
      介绍完 RocketMQ 的事务消息方案后,由于前面已经介绍过本地消息表方案,这里就简单介绍 RocketMQ 分布式事务:

    image

    事务主动方基于 MQ 通信通知事务被动方处理事务,事务被动方基于 MQ 返回处理结果。

    如果事务被动方消费消息异常,需要不断重试,业务处理逻辑需要保证幂等。

    如果是事务被动方业务上的处理失败,可以通过 MQ 通知事务主动方进行补偿或者事务回滚。

    方案总结

    相比本地消息表方案,MQ 事务方案优点是:

    • 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
    • 吞吐量由于使用本地消息表方案。

    缺点:

    • 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
    • 业务处理服务需要实现消息状态回查接口。

    6.方案比较

    在这里插入图片描述


    结尾

    • 感谢大家的耐心阅读,如有建议请私信或评论留言。
    • 如有收获,劳烦支持,关注、点赞、评论、收藏均可,博主会经常更新,与大家共同进步
  • 相关阅读:
    工具大全使用
    SQL必需掌握的100个重要知识点:汇总数据
    Redis集群
    HTML学生个人网站作业设计成品 HTML+CSS肖战明星人物介绍网页 web结课作业的源码
    unigui手机端如何防止左右滑动退出页面
    「SpringCloud Alibaba」Nacos服务注册和中心配置
    matlab高斯消元法求逆
    鸿枫网盘,文件夹面包屑跳转实现功能
    React+Electron快速创建并打包成桌面应用
    猿创征文|2022年快过完了,是时候总结一下那些优秀的 React 组件库
  • 原文地址:https://blog.csdn.net/qq359605040/article/details/126162302