事务及ACID特性

      事务是用户定义的一系列数据库操作。这些操作应该被视为一个完整的、不可分割的工作单元,要么全部执行,要么全部不执行

事务需要具备原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),即我们常说的ACID特性。

原子性

      事务的原子性是指:一个事务必须被视为一个不可分割的最小工作单元,事务中的所有操作要么全部提交成功,要么全部失败回滚,不可能只执行其中的一部分操作。

在实现事务操作时,大多数的数据库是在数据快照上进行操作的,并不会修改实际的数据,在发生错误时并不会真正提交。

一致性

      事务的一致性是指:数据库只能从一种有效状态变为另一种有效状态,写入数据库的任何数据都必须满足所有定义的规则(包括约束、级联、触发器及它们的任何组合)。
      如果数据库系统在运行过程中发生故障,有些事务尚未完成就被迫中断,这些未完成的事务对数据库所作的修改有一部分已写入物理数据库,这是数据库就处于一种不正确的状态,也就是不一致的状态

隔离性

      事务的隔离性是指:在并发环境中,并发的事务时相互隔离的,一个事务的执行不能不被其他事务干扰。不同的事务并发操作相同的数据时,每个事务都有各自完成的数据空间,即一个事务内部的操作及使用的数据对其他并发事务时隔离的,并发执行的各个事务之间不能相互干扰。
      ANSI规定的隔离级别有一下四种:

  • 读未提交(Read Uncommitted)
    在"读未提交"隔离级别中,所有事务都可以看到其他未提交事务的执行结果。该隔离级别在数据库的本地事务中很少被使用,但在分布式事务中被广泛使用。读取未提交的数据,也被称为"脏读"(Dirty Read)。
  • 读已提交(Read Committed)
    "读已提交"是大多数数据库系统的默认隔离级别(但不是 MySql 默认的)。它满足了隔离的简单定义——一个事务只能看见已经提交事务所做的改变。即一个事务从开始直到提交之前所做的任何修改对其他事务都是不可见的。

"读已提交"级别有时也被叫作"不可重复度"(Non-repeatable),即执行两次同样的查询语句,可能会得到不同的结果。该隔离级别可以解决"脏读",但是存在"不可重复读"和"幻读"。

"不可重复读"发生的一个场景:事务A需要多次读取同一个记录,当再次读取改记录时,另一个事务B已经修改了它,导致事务A读取到的该数据与上一次读到的数据不一致。

请输入图片描述

  • 可重复读(Repeatable Read)
    "可重复读"级别保证了在同一事务中多从读取同一个记录的结果是一样的。MySql默认的隔离级别就是"可重复读"。该隔离级别可以解决"脏读"和"不可重复读",但是存在"幻读"的问题。

所谓"幻读"是指在某个事务读取某个范围内的记录时,另外一个事务在该范围的记录中插入了新记录,当之前的事务再次读取同范围的记录时会产生"幻行"(Plantom Row),即多读取一些记录(另一个事务插入了新记录),或者少读取一些记录(另一个事务删除了一些记录)。

      "幻读"发生的一个场景:SELECT 语句查询某个记录,发现不存在数据,但是在执行 INSERT 语句时发现该记录已经存在数据了(另一个事务此时插入了该数据),无法再插入。
看起来"可重复读"和"不可重复读"级别都是指同一个事务前后读取的数据不一致,但是两者是不同的:

  • "可重复读"强调的是,在同一个事务的两次查询之间,第二个事务删除或者插入了一些记录,导致第一个事务看到不一样的结果(幻读)。
  • "读已提交"强调的是,在同一个事务的两次查询之间,第二个事务更新了记录,导致第一个事务看到不一样的结果(不可重复读)。

  • 可串行化(Serializable)
    "可串行化"是事务的最高隔离级别,即强制事务的串行执行。这样可以避免"幻读"的问题,但是会导致性能大大降低。

      以上四种隔离级别与"脏读"、"不可重复读"、"幻读"的关系见如下表:

隔离级别 脏        读 不可重复读 幻        读
读未提交 (Read Uncommitted)
读已提交 (Read Committed)
可重复读 (Repeatable Read)
可串行化 (Serializable)

      如下图展示了不同隔离级别下事务访问数据的差异。
请输入图片描述

持久性

      事务的"持久性"是指:一旦一个事务被提交了,则数据库中数据的改变就是永久的,即便数据库系统遇到故障也不会丢失提交事务的操作。

XA两阶段提交协议

      XA两阶段提交协议由 Tuxedo 提出给 X/Open 组织,作为资源管理器与事务协调器的接口标准。
      XA协议包括两套函数——以 xa_ 开头的函数以及以 ax_ 开头的函数。

  • xa_open()xa_close():建立/关闭与资源管理器的连接
  • xa_start()xa_end():开始/结束一个本地事务
  • xa_prepare()xa_commit()xa_rollback():预提交/提交/回滚一个本地事务
  • xa_recover():回滚一个已进行预提交的事务
  • ax_reg()ax_unreg():允许一个资源管理器在事务协调器中动态注册/撤销注册
    以"ax_"开头的函数,使得资源管理器可以在事务协调器中动态地进行注册,并可以对XID(事务ID)进行操作。

      XA两阶段提交协议为了保证事务的一致性,不管是事务协调器还是各个资源管理器的每一步操作,都会记录日志。记录日志降低了性能,但是提高了系统故障恢复能力。

两阶段提交协议的执行过程

      两阶段提交协议由两个阶段组成,如下图所示:
请输入图片描述

      在第一阶段中,应用程序向事务协调器发起提交请求,此后分为两个步骤:

  1. 事务协调器通知参与该事务的所有资源管理器开始准备事务;
  2. 资源管理器在接收到消息后开始准备(写好事务日志并执行事务,但不提交),之后将"就绪"的消息返回给事务协调器。此时已经将事务的大部分事情都做完了,第二阶段的操作耗时极短。

      在第二阶段中也分为两个步骤:

  1. 事务协调器在接收到各个资源管理器回复的消息后,基于投票结果进行决策——提交或者取消。如果有任意一个回复失败,则发送回滚命令,否则发送提交命令;
  2. 各个资源管理器在接收到二阶段提交或回滚的命令后,执行并将结果返回给事务协调器。

两阶段提交协议的缺点

      两阶段提交协议主要存在如下缺点:

  1. 同步阻塞问题。在执行过程中,所有参与节点都是事务阻塞型的——当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
  2. 单点故障。事务协调器起着关键作用,一旦事务协调器发生故障,参与者会一直阻塞下去。尤其在第二阶段,如果事务协调器发生故障,则所有参与者都还处于锁定事务资源的状态中,无法继续完成事务操作。
  3. 数据不一致。在二阶段处理中,如果在事务协调器向参与者发送 commit 请求后发生了局部网络异常,或者在发送 commit 请求过程中事务协调器发生了故障,则会导致只有一部分参与者接收到 commit 请求;这部分接收到 commit 请求的参与者会执行 commit 操作,而其他部分未接到 commit 请求的参与者则不会执行事务提交;于是整个分布式系统变出现了数据不一致的现象。
  4. 状态不确定。如果事务协调器在发出 commit 消息之后宕机了,且唯一接收到这条消息的参与者同时也宕机了,那么即使通过选举协议产生了新的事务协调器,这条事务的状态也是不确定的,因为没人知道事务是否已经被提交了。

CAP与BASE理论

CAP理论

      CAP是分布式系统的指导理论,它指出:一个分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中的两项。

  • 一致性,指"all nodes see the same data at the same time",即在更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致(强一致性)
  • 可用性,指"Reads and writes always succeed",即服务器一直可用,而且是正常响应时间
  • 分区容错性,指"the system continues to operate despite arbitrary message loss or failure of part of the system",即分布式系统在遇到某个节点或网络分区故障市,仍然能够对外提供满足一致性和可用性的服务
    请输入图片描述
放弃CAP定理 说        明
放弃 P       如果希望能够避免系统出现分区容错性问题,一种较为简单的做法是将所有的数据(或者仅仅是那些和事务相关的数据)都放在一个分布式节点上。这样的做法虽然无法100%地保证系统不会出错,但至少不会碰到由于网络分区带来的负面影响,但同时需要注意的是,放弃 P 的同时也就意味着放弃了系统的可扩展性
放弃 A       相对于放弃 P 来说,放弃 A 则正好相反,其做法是一旦系统遇到网络分区或者其他故障时,那么受到影响的服务需要等待一定的时间,因此在等待期间系统无法对外提供正常的服务,即不可用
放弃 C       这里所说的放弃一致性并不是完全不需要数据一致性,如果真是这样的话,那么系统的数据都是无意义的,整个系统也是没有价值的。
      事实上,放弃一致性指的是放弃数据的强一致性,而保留数据的最终一致性。这样的系统无法保证数据保持实时的一致性,但是能够承诺的是,数据最终会达到一个一致的状态。这就引入了一个时间窗口的概念,具体多久能够达到数据一致性取决于系统的设计,主要包括数据副本在不同节点之间的复制时间长短。

对于分布式系统来说,分区容错性是最基本的要求,因为既然是一个分布式系统,那么分布式系统中的组件必然会被部署到不同的节点,否则也就无所谓分布式系统了,因此必然会出现子网络。而对于分布式系统而言,网络又必定会出现异常情况,因此,分区容错性就成为了分布式系统必然需要面对和解决的问题。

      架构师往往需要把精力花在如何根据业务特点在 C 和 A 之间做选择:

  • CP:即实现一致性和分区容错性。此组合为数据强一致性模式,即要求在多服务之间数据一定要一致,弱化了可用性。一些对数据要求比较高的场景(比如金融业务)使用此模式。这种模式性能偏低。常用方案有XA两阶段提交、Seata AT模式的"读已提交"级别等。
  • AP:即实现可用性和分区容错性。此组合为数据最终一致性模式,即要求所有服务都可用,弱化了一致性。互联网分布式服务多数基于 AP,这种模式性能高,可以满足高并发的业务需求。常用方案有 TCC、基于消息的最终一致性、Saga等。

BASE理论

      BASE是 Basically Available(基本可用)、Soft State(软状态)和Eventually Consistency(最终一致性)这三个短语的缩写。Base理论是对 CAP 中的一致性及可用性进行权衡的结果,<font style="color:red;font-weight:600;">其核心思想是:无法做到强一致性,那么可以通过牺牲强一致性来获得可用性。</font>

  • Basically Available(基本可用)
    基本可用是对A(可用性)的一个妥协,即在分布式系统出现不可预知故障时,运行损失部分可用性。比如在秒杀场景和雪崩的业务场景下进行降级处理,使核心功能可用,而不是所有的功能可用。
  • Soft State(软状态)
    软状态是相对于原子性而言的。"硬状态"要求多个节点的数据副本是一致的。"软状态"允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本上存在数据延时。
  • Eventually Consistency(最终一致性)
    系统不可能一直是处于"软状态",必须有个时间期限。过了此时间期限后,应当保证所有副本保持数据一致性,从而达到数据的最终一致性。

TCC柔性事务

      TCC(Try-Confirm-Cancel) 的核心思想是:通过对资源的预留(如账户状态、冻结金额等),尽早释放对资源的"加锁";如果事务可以提交,则完成对预留资源的确认;如果事务要回滚,则释放预留的资源。


      TCC 方案将整个业务逻辑的每个分支显示地分成tryconfirmcancel这3个操作阶段:在try操作阶段完成业务的准备工作;在confirm操作阶段完成业务的提交;在cancel操作阶段完成事务的回滚。
      TCC基本原理如下图所示:
请输入图片描述

  1. 业务应用向事务协调器发起开始事务请求;
  2. 业务应用调用所有服务的 try 接口,完成一阶段工作;
  3. 业务应用根据调用 try 接口是否成功,决定提交或回滚事务,并发送请求到事务协调器;
  4. 事务协调器根据接收到的请求为提交还是回滚事务,决定调用 confirm 接口或 calcel 接口。如果接口调用失败,则会重试。

      TCC缺点:

  1. 对应于的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel这3个操作,应于侵入性较强,改造成本高;
  2. 实现难度较大。需要根据网络状态、系统故障等不同失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。

基于消息的最终一致性

      消息方案是将分布式事务转换为两个本地事务,然后依靠下游业务的重试机制达到最终一致性。
      在普通消息处理流程中,存在数据库数据与消息不一致的问题,进而造成消息生产者与消息消费者数据不一致。如下图展示了一个典型的消息处理流程:
请输入图片描述

  1. 消息生产者在完成本地业务操作(通常为一个数据库本地事务)后,发送消息到MQ;
  2. MQ 收到消息,将消息持久化,在存储系统中新增一条记录;
  3. MQ 返回 ACK 消息给消息生产者;
  4. MQ 推送消息给对应的消息消费者,然后等待消息消费者返回 ACK;
  5. 消息消费者在收到消息后完成本地业务操作(通常为一个数据库本地事务),返回 ACK;
  6. MQ 删除消息。

无法保证步骤一和步骤五的数据库本地事务同时成功或者同时失败。

解决方案

      基于消息的最终一致性方案如下所示:
请输入图片描述

  1. 在执行业务操作时,记录一条消息数据到数据库(状态为"待发送"),并且消息数据的记录与业务数据的记录必须在数据库的同一个本地事务内完成
  2. 在消息数据记录完成之后,通过一个定时任务轮训状态为"待发送"的消息,然后将待发送的消息投递给消息队列;
  3. 如果在这个过程中消息投递失败,则启动重试机制,直到成功收到消息队列的 ACK 确认后,再将消息状态更新为"已发送"或者删除消息;
  4. 如果下游系统消费消息失败,则不断重试,最终做到两个系统数据的最终一致性。

基于消息的最终一致性方案的主要缺点是:对应于侵入性很强,应用需要进行大量的业务改造,成本较高。

Last modification:December 5th, 2021 at 03:53 pm
如果觉得我的文章对你有用,请随意赞赏