分布式事务

1. 什么是分布式事务

1.1 概念

  分布式事务用于保证不同服务器节点、不同数据库的数据一致性。简单来说,就是一次大的操作由不同的小操作组成,这些小操作分布在不同服务器上、或者不同数据库上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。或者换句话说,分布式事务 = n 个本地事务。

2. 为什么会有分布式事务?

  从本地事务角度来看,可以分为两类。一个是多 service 节点,另外一个是多数据源

2.1 多 service 节点

  随着公司业务发展,单体服务越来膨胀,进而演变出微服务。举个例子,以前一个下单功能,在一个单体服务上完成,现在演变为微服务,下单功能需要几个服务器才能完成。

在这里插入图片描述

  整个下单流程分布在不同的服务器上,每个服务器上执行的都是本地事务,保证多个服务器上本地事务要么全部执行成功,要么全部执行失败就是分布式事务需要做的

2.2 多数据源

  随着公司业务发展,我们 mysql 数据库千万级、百万级就得分库分表。对于一个转账操作来说,小明转账100元给小红,小明的账户可能存在北京数据库,小红的账户深圳数据库

在这里插入图片描述

  虽然转账服务是在同一台,但是小明扣减100元、小红增加100元是在两个数据源上,是2个本地事务。

3. 分布式事务的理论基础

  解决分布式事务问题,需要一些分布式系统的基础知识作为理论指导

3.1 CAP 定理

  CAP 定理,又被叫作布鲁尔定理。对于设计分布式系统来说(不仅仅是分布式事务)的架构师来说,CAP 就是你的入门理论

  1. C (一致性):用户访问分布式系统中的任何节点,得到的数据必须一致
  2. A (可用性):用户访问集群中任意健康节点,必须得到响应,而不是超时或拒绝
  3. P (分区容错性):分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障

  网络分区,是指分布式系统中,不同的节点分布在不同的子网络(机房/异地网络)中,由于一些特殊的原因导致这些子网络之间出现网络不连通的状态,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被分为若干孤立的区域(分区)。

  • CA without P:放弃P的话,也就意味着放弃分布式系统;对于单节点,CA 必然是可以保证的。
  • CP without A:放弃A的话,在遇到网络分区情况下,受影响的服务需要等待一定的时间,再此期间无法对外提供政策的服务,即不可用
  • AP wihtout C:放弃C的话,在遇到网络分区情况下,系统无法保证数据保持实时的一致性;在数据达到最终一致性时,有个时间窗口,在时间窗口内,数据是不一致的。

CAP 权衡

  MySQL 主从异步复制是 AP wihtout C,半同步复制是 CP without A。

3.2 BASE 定理

CAP是分布式系统设计理论,BASE是CAP理论中AP方案的延伸,对于C我们采用的方式和策略就是保证最终一致性

  BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。BASE基于CAP定理演化而来,核心思想是即时无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

  1. Basically Available(基本可用):基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性,保证核心功能可用。
  2. Soft state(软状态):指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性。

软状态与硬状态相对,即是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时

  1. Eventually consistent(最终一致性):强调系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态;其本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

  BASE 解决了 CAP 中理论没有网络延迟,在 BASE 中用软状态和最终一致,保证了延迟后的一致性。

  BASE 和 ACID 是相反的,它完全不同于 ACID 的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

  具体到分布式事务的实现上,业界主要采用了 XA 协议的强一致规范以及柔性事务的最终一致规范。

4. 分布式事务解决方案

4.1 XA 方案

  XA 是 X/Open CAE Specification (Distributed Transaction Processing)模型,它定义的 TM(Transaction Manager)与 RM(Resource Manager)之间进行通信的接口。这个模型主要使用了两段提交(2PC - Two-Phase-Commit)来保证分布式事务的完整性。

  在 XA 规范中,数据库充当 RM 角色,应用需要充当 TM 的角色,即生成全局的 txId ,调用 XA Resource 接口,把多个本地事务协调为全局统一的分布式事务

4.1.1 2pc

  两阶段提交(2PC)协议是XA规范定义的 数据一致性协议。

在这里插入图片描述

  二阶段提交是 XA 的标准实现。它将分布式事务的提交拆分为 2 个阶段:prepare 和 commit/rollback

  • 第一阶段(prepare):事务管理器向所有本地资源管理器发起请求,询问是否是 ready 状态,所有参与者都将本事务能否成功的信息反馈发给协调者;
  • 第二阶段 (commit/rollback):事务管理器根据所有本地资源管理器的反馈,通知所有本地资源管理器,步调一致地在所有分支上提交或者回滚。

优点?

  • 尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于 MySQL 是从 5.5 开始支持。

缺点?

  • 同步阻塞:在准备就绪之后,本地资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
  • 数据不一致:在阶段二,如果事务管理器只发送了部分 commit 消息,此时网络发生异常,那么只有部分参与者接收到 commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
  • 单点故障:一旦事务管理器出现故障,整个系统不可用

  这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。

框架支持

  1. Sharding Sphere
  2. Spring JTA + Atomikos

4.2 TCC

  TCC 模型是把锁的粒度完全交给业务处理,它需要每个子事务业务都实现Try-Confirm-Cancel 接口。

  • Try:尝试执行业务,完成所有业务检查(一致性),冻结资源
  • Confirm:确认执行业务,真正执行业务,不作任何业务检查,只使用 Try 阶段冻结的业务资源
  • Cancel:取消执行业务,释放 Try 阶段冻结的业务资源

  这三个阶段,都会按本地事务的方式执行提交。不同于 XA 的prepare ,TCC 无需将 XA 的投票期间的所有资源挂起,因此极大的提高了吞吐量。

在这里插入图片描述

  Try-Confirm-Cancel 三个阶段都是本地事务,它采用预留冻结资源方式代替同步锁定资源,所以它下效率是非常高的。同时我们操作目标需要添加一个相关的冻结字段,然后根据业务实现 Try-Confirm-Cancel,对业务入侵比较大

上述时序图伪代码演示

库存服务

<!-- Try -->
update product_inventory set num = num - #{num}, frozen = frozen + #{num} where sku_id = #{skuId}

<!-- Confrim -->
update product_inventory set frozen = frozen - #{num} where sku_id = #{skuId}

<!-- Cancel -->
update product_inventory set num = num + #{num}, frozen = frozen - #{num} where sku_id = #{skuId}

账户服务

<!-- Try -->
update account_balance set balance = balance - #{balance}, frozen = frozen + #{balance} where user_id = #{userId}

<!-- Confrim -->
update account_balance set frozen = frozen - #{balance} where user_id = #{userId}

<!-- Cancel -->
update account_balance set balance = balance + #{balance}, frozen = frozen - #{balance} where user_id = #{userId}

框架支持

4.3 saga 方案

  Saga 是 30 年前一篇数据库伦理提到的一个概念。其核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

Saga 的组成如下

  1. 每个 Saga 由一系列 sub-transaction Ti 组成,后面简称 Ti,每个 Ti 都是一个本地事务
  2. 每个Ti 都有对应的补偿动作 Ci ,补偿动作用于撤销 Ti 造成的结果

Saga 的执行顺序

  1. 子事务序列 T1, T2, …, Tn得以完成 (最佳情况)
  2. 子事务序列 T1, T2,…,Tj,Cj,…,C2,C1,0<j<n,得以完成

Saga 定义了两种恢复策略

  • 向后恢复:补偿所有已完成的事务,如果任一子事务失败。

    向后恢复,即上面提到的第二种执行顺序,其中 j 是发生错误的 sub-transaction ,这种做法的效果是撤销掉之前所有成功的 sub-transation ,使得整个 Saga 的执行结果撤销。

  • 向前恢复:重试失败的事务,乐观假设每个子事务最终都会成功。

    显然,向前恢复没有必要提供补偿事务,如果你的业务中,子事务(最终)总会成功,或补偿事务难以定义或不可能,向前恢复更符合你的需求。理论上补偿事务永不失败,然而,在分布式世界中,服务器可能会宕机、网络可能会失败,甚至数据中心也可能会停电,这时需要提供故障恢复后回退的机制,比如人工干预。

对比
  Saga 和 TCC 相比,Saga 没有“预留 try”动作,也就不用预留冻结资源、给目标添加冻结字段,Saga 事务间不能保证隔离性,当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发。

TCC 也存在事务隔离性问题,但是它是可控的

举例

  1. T1=扣100元 T2=给用户加一瓶水 T3=减库存一瓶水、
  2. C1=加100元 C2=给用户减一瓶水 C3=给库存加一瓶水

  我们一次进行 T1,T2,T3。如果发生问题,就执行发生问题的 C 操作的反向。上面说到的隔离性的问题会出现在,如果执行到 T3 这个时候需要执行回滚,但是这个用户已经把水喝了(另外一个事务A),回滚的时候就会发现,无法给用户减一瓶水了。这就是事务之间没有隔离性的问题。(怎么理解这个隔离性?事务A 和 Saga全局事务发生并发问题,没有满足隔离性)

也就是说,给的太早,但是可以被取消!

  而 TCC 怎么做,对于 T1 (账户-100元,预留资源+100元)、T2(预留资源+1瓶水)、T3 (商品库存-1,预留资源+1)。对于 TCC 执行完 T1——>T2 还没执行 T3,另外一个事务A访问 T1 账户,这时TCC 全局事务没执行完,但是事务A访问到了全局事务未提交的数据(账户-100元),这是脏读,但这个脏读并不会引起业务故障;另外一个事务A又访问了 T2,它预留资源+1,但还没真实到手,所以针对 T2 访问没有隔离性问题。整个流程下来虽然 TCC 也存在隔离性问题,但并不会引起数据更新丢失等影响业务的问题。

saga 隔离性解决方案?

  • 在应用层面加锁:那并发性能急剧降低,不可取
  • 在应用层面预先冻结资源:那可以用 TCC 啊

框架支持

  • Apache Service Comb 的 Saga 事务引擎

  • Sharding Sphere 的 Saga 支持

    实际是基于 Apache Service Comb 的 Saga 事务引擎之上进行开发。

4.4 本地消息表、RocketMQ事务消息方案、最大努力通知方案

  严格来说这三种不算真正意义上的分布式事务解决方案,它们通过不断重试来保证数据最终一致性,并没有响应的回滚方案!

4.4.1 RocketMQ 事务消息方案

  RocketMQ 事务消息设计则主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题,严格来说不是分布式事!

  RocketMQ 的设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而 RocketMQ 本身提供的存储机制为事务消息提供了持久化能力;RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。

  在RocketMQ 4.3后实现了完整的事务消息,实际上其实是对本地消息表的一个封装,将本地消息表移动到了 MQ 内部,解决 Producer 端的消息发送与本地事务执行的原子性问题。

在这里插入图片描述

  1. Producer 发送事务消息

  Producer (MQ发送方)发送事务消息(Half)至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。

  1. MQ Server回应消息发送成功

  MQ Server 接收到 Producer 发送给的消息则回应发送成功表示 MQ 已接收到消息。

  1. Producer 执行本地事务

  Producer 端执行业务代码逻辑,通过本地数据库事务控制.

  1. 消息投递

  若 Producer 本地事务执行成功则自动向 MQServer 发送 commit 消息,MQ Server 接收到 commit 消息后将”Half消息“ 状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息。

  若 Producer 本地事务执行失败则自动向 MQServer 发送 rollback 消息,MQ Server 接收到 rollback 消息后将删除”Half消息“ 。

  MQ订阅方(积分服务)消费消息,消费成功则向 MQServer 回应 ack,否则将重复接收消息。这里 ack 默认自动回应,即程序执行正常则自动回应a ck。

没有回滚机制

  1. 事务回查

  如果执行 Producer 端本地事务过程中执行端挂掉,或者本地事务提交完成发生消息超时,MQ Server 将会不停的询问同组的其他 Producer 来获取事务执行状态,这个过程叫事务回查。MQ Server 会根据事务回查结果来决定是否投递消息。

以上主干流程已由 RocketMQ 实现,对用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。

框架支持

4.4.2 常规 MQ 解决方案

  上面说到,通过使用 RocketMQ 能实现 Producer 端的消息发送与本地事务执行的原子性问题。那我使用其他常规 MQ,比如 NSQ 呢,怎么解决这个问题?

数据库事务 + 定时补偿

在这里插入图片描述

4.5 Seata

  Seata 也是从两段提交演变而来的一种分布式事务解决方案,提供了 ATTCCSAGAXA 等事务模式,这里重点介绍 AT模式。

  既然 Seata 是两段提交,那我们看看它在每个阶段都做了点啥?下边我们还以下单扣库存、扣余额举例。

在这里插入图片描述

Seata 几种角色

  • Transaction Coordinator(TC):全局事务协调者,用来协调全局事务和各个分支事务(不同服务)的状态, 驱动全局事务和各个分支事务的回滚或提交。
  • Transaction Manager™:事务管理者,业务层中用来开启/提交/回滚一个全局事务(在调用服务的方法中用注解开启事务)。
  • Resource Manager(RM):资源管理者,一般指业务数据库代表了一个分支事务(Branch Transaction),管理分支事务且与 TC 进行协调注册分支事务并且汇报分支事务的状态,驱动分支事务的提交或回滚。

Seata 实现分布式事务,设计了一个关键角色 UNDO_LOG 表(回滚日志记录表),我们在每个应用分布式事务的业务库中创建这张表,这个表的核心作用就是,将业务数据在更新前后的数据镜像组织成回滚日志,备份在 UNDO_LOG 表中,以便业务异常能随时回滚

  1. 首先 TM 开启一个全局事务,它会生成全局事务ID(xid),会通过 RPC 调用传递给各个服务 RM

  2. 第一阶段:RM 执行本地事务,在这之间会生成镜像前后的数据,在本地事务提交前,各分支事务 RM 需向 全局事务协调者 TC 注册分支 ( Branch Id) ,接着本地事务和镜像数据+xid+branchId 插入 undolog 表一起提交

  3. 第二阶段:TC 会根据情况向所有 RM 发起全局提交 or 全局回滚请求。RM 根据请求做出响应操作。

4.5.1 第一阶段

  比如:下边我们更新 user 表的 name 字段。

update user set name = '小富最帅' where name = '程序员内点事'

  首先 Seata 的 JDBC 数据源代理通过对业务 SQL 解析,提取 SQL 的元数据,也就是得到 SQL 的类型(UPDATE),表(user),条件(where name = '程序员内点事')等相关的信息。

在这里插入图片描述

  先查询数据前镜像,根据解析得到的条件信息,生成查询语句,定位一条数据。

select name from user where name = '程序员内点事'

在这里插入图片描述

  紧接着执行业务 SQL,根据前镜像数据主键查询出后镜像数据

select name from user where id = 1

在这里插入图片描述

  把业务数据在更新前后的数据镜像组织成回滚日志,将业务数据的更新和回滚日志在同一个本地事务中提交,分别插入到业务表和 UNDO_LOG 表中。

回滚记录数据格式如下:包括 afterImage 前镜像、beforeImage 后镜像、 branchId 分支事务ID、xid 全局事务ID

{
    "branchId":641789253,
    "xid":"xid:xxx",
    "undoItems":[
        {
            "afterImage":{
                "rows":[
                    {
                        "fields":[
                            {
                                "name":"id",
                                "type":4,
                                "value":1
                            }
                        ]
                    }
                ],
                "tableName":"product"
            },
            "beforeImage":{
                "rows":[
                    {
                        "fields":[
                            {
                                "name":"id",
                                "type":4,
                                "value":1
                            }
                        ]
                    }
                ],
                "tableName":"product"
            },
            "sqlType":"UPDATE"
        }
    ]
}

这样就可以保证,任何提交的业务数据的更新一定有相应的回滚日志。

在本地事务提交前,各分支事务 RM 需向 全局事务协调者 TC 注册分支 ( Branch Id) ,为要修改的记录申请 全局锁 ,要为这条数据加锁,利用 SELECT FOR UPDATE 语句。而如果一直拿不到锁那就需要回滚本地事务。TM 开启事务后会生成全局唯一的 XID,会在各个调用的服务间进行传递。

  有了这样的机制,本地事务分支(Branch Transaction)便可以在全局事务的第一阶段提交,并马上释放本地事务锁定的资源。相比于传统的 XA 事务在第二阶段释放资源,Seata 降低了锁范围提高效率,即使第二阶段发生异常需要回滚,也可以快速 从UNDO_LOG 表中找到对应回滚数据并反解析成 SQL 来达到回滚补偿。

  最后本地事务提交,业务数据的更新和前面生成的 UNDO LOG 数据一并提交,并将本地事务提交的结果上报给全局事务协调者 TC。

4.5.2 第二阶段

  第二阶段是根据各分支的决议做提交或回滚

  如果决议是全局提交,此时各分支事务已提交并成功,这时 全局事务协调者(TC) 会向分支 RM 发送第二阶段的请求。收到 TC 的分支提交请求,该请求会被放入一个异步任务队列中,并马上返回提交成功结果给 TC。异步队列中会异步和批量地根据 Branch ID 查找并删除相应 UNDO LOG 回滚记录。

在这里插入图片描述

  如果决议是全局回滚,过程比全局提交麻烦一点,RM 服务方收到 TC 全局协调者发来的回滚请求,通过 XIDBranch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。

注意:这里删除回滚日志记录操作,一定是在本地业务事务执行之后

在这里插入图片描述
AT 模式是无侵入的分布式事务解决方案,但是它也有事务隔离性问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值