seata-分布式事务与seata

分布式事务与 Seata

分布式事务

分布式事务是个现实中很常见的现象,日常的跨行转账就是一个很典型的分布式事务。

现实中,每个银行各自管理各自的账户,在执行跨行转账时,需要确保转出账户扣费正确,转入账户增加正确的金额。在电子渠道上操作看着很简单,其后台需要执行分布式事务的处理流程有很多步骤,如果账户不平,还需要进行人工对账,中间涉及到一系列的制度和机制。这里暂且不涉及电子交易的安全可信的问题,只讨论分布式事务。

转账的数字形式上无非是一系列的电子信号,但是它是用户的财富的事实代表,其转移有法律上的意义,银行不能随意改动(理论上),任何的修改都是需要有事实依据的,是得到用户授权的。正如经典数据库的说法,银行的数据库存储的是一系列事实。

在早期,没有银行间共同信任的中间机构时,银行要与其它银行转账时,必须要在对方开设一个银行专属的账户,转账时变成了从这个专属账户交易到普通账户,然后银行间再定时进行结算。这种方式会导致 n 个银行之间相互转账时,需要 n^2 个专属账户,试想 1 万间银行的情况,所以基本无法在大量银行间进行维护。

然后银行间共同组织起共同信任的中间机构,如中国银联和 SWIFT ,大家都通过中间机构进行转账(还是会有需要银行之间直接对接的情况的)。在转账时,可能会出现各种各样的情况:

1. 扣款失败
2. 扣款成功,通知失败
3. 扣款成功,通知成功,入账失败
4. 扣款成功,通知成功,用户要求撤回
5. ...

这个场景下,涉及到多方参与者,为保证整个交易事实的正确一致,必须满足多方都成功或撤销,就是一个分布式的事务。

为实现分布事务的问题,有一些协议:两阶段提交、三阶段提交、事务补偿、saga 等等。

两阶段提交(2pc)

2pc 认为,既然各方参与者都无法确定对方的状态,那么引入一个事务协调者来统一安排参与者的操作吧。

1. 协调者首先向所有参与者发一个准备消息 prepare ,参与者在本地进行预处理事务,并报告能否执行
2.1 协调者收到所有参与者的确认消息,那么就发一个 global_commit 消息,所有参与者进行提交
2.2 协调者收到有任意的参与者预处理失败,或者参与者超时,就发一个 global_rollback 消息,所有参与者取消

但 2pc 没有想到的是,这个世界充满了不确定性,任何的节点都可能会出现问题:

  1. 同步阻塞,所有参与者都是事务阻塞型,公共资源会处于阻塞状态
  2. 单点故障,协调者发出 prepare 后异常,那么所有参与者都在处理本地事务挂起状态
  3. 不一致,部分参与者节点在 global_commit 前异常,这部分事务没有提交,导致局部数据不一致
  4. 全局事务状态无法确定,一旦协调者异常,即使出现新的协调者,也是无法确定之前的事务状态

于是,在 2pc 的基础上,又推出了三阶段提交(3pc)

三阶段提交(3pc)

2pc 无法确定协调者状态,在 3pc 时为协议者也引入超时机制;针对同步阻塞和不一致问题,增加一个准备阶段。整个过程为成 can_commit、pre_commit、do_commit(abort) 3个阶段。

1. 协调者发 can_commit,参与者检查,类型于 prepare
2. 协调者发 pre_commit,参与者开始本地事务,写 undo 日志
3.1 所有参与者成功 pre_commit,协调者发 do_commit,参与者提交,清理 undo
3.2 任意参与者失败或超时,协调者发 abort,参与者回滚并释放资源
3.3 协调者超时,参与者默认 commit

如果出现同步阻塞,那么会在 2 出现失败,全局回滚。参与者默认 commit ,使得只要 pre_commit 阶段成功,那么分布式事务就能默认提交。

但是这里还存在数据不一致问题,如果 pre_commite 阶段,部分参与者成功,部分失败,而成功参与者没有收到后续的 abort ,会默认提交,造成不一致。

分布式一致性算法,目前好像基本上都是 Paxos 的变种,它实现的也不是全部都一致,是基于少数服从多数的原则,在使用场景上有所区别,多见于存储系统中。zookeeper 就是基于 paxos 的实现。

CAP 原理

CAP 原理描述的在分布式中,3种属性:一致性 consistency, 可用性 avaliablity, 分区容错 partition tolerance,无法 3个同时满足。

以转账为例,A 系统扣款,通知 B 系统进行入账,有可能出现网络中断导致通知失败的情况,A 可以采取:

1. 放弃可用性,禁止对这个账户进行的任何操作,直到得到 B 确认
2. 放弃强一致性,后台任务继续通知 B 系统,同时允许对账户的继续操作

但是无法放弃分区容错的,在分布式系统中,必然是有多个节点的,也就是 P 是必然,因此一个分布式系统要么以 CP 、要么以 AP 为设计目标。在不同的应用中,会采用不同的策略,像 zookeeper 用的 cp ,而 eureka 以 ap 为目标。

一般而言,实现分布式强一致的代价会比较高,并会导致整个服务暂时不可用。试想下,一笔转账还没有得到确认前,所有的其它的动账操作都不可执行,怎么向脾气火爆的客户解析系统不支持继续操作,只因为对方系统刚好在发出了交易消息后挂了!当然,也不能让转账的事实丢失,扣款后对方系统没有入账,要做些补偿操作,实现最终一致性。

所谓的最终一致性,即系统是某个时间段存在不一致性,但是过了此段时间后会是一致的(通过一些补偿操作)。最终一致性只在分布式下有意义,其在本地也是强一致性的,否则无法做补偿。为此,又提出了 BASE 模型

BASE 模型

BASE 模型从另外的角度来看待分布式系统,提出 3 方面的想法:

1. 基本可用 Basically avaliable
2. 软状态(柔性事务) Soft-state
3. 最终一致 Eventually consitence

BASE 模型不追求经典数据库系统的 ACID ,而是和分布式系统的特点结合起来,在可用性、一致性上争取平衡。

  1. 基本可用

    允许分布式系统的若干节点出现不可预测的问题,在设计时应该在部分节点出现问题时,系统可以以低一点的效率运行,系统其它部分还能提供基本的服务。

    在这样的设计约束下,基本上无法追求强一致性的目标,否则就会把所有的服务锁死。

  2. 软状态(柔性事务)

    允许数据存在中间状态,在中间状态下暂时不一致。中间状态为最终一致服务,可提交或撤销。

  3. 最终一致

    系统必须能在某个时间内实现最终一致性。对于同一个逻辑全局事务,各分支事务必须都提交或都撤销。一般分成几种:

     1. 因果一致性
         A 节点操作后导致 B 节点操作,那么 B 操作时获得的状态是 A 的结果状态,和 A 无关的操作则无此要求
     2. 读自己已写
         同一个节点总是能读到自己已处理后的结果状态,属于特殊的因果一致性,也可理解为单节点自己的本地一致性
     3. 会话一致性
         同一个会话中,客户端总是能读到自己更新的最新状态,这是要求比较高的因果一致性
     4. 单调读一致性
         在获得某个读取状态之后,所有的读取的状态都不能取得比此状态更旧的状态,也就是读取的结果只能越来越新
     5. 单调写一致性
         系统保证来自同一节点的写操作总是按顺序执行
    

要留意到一点,并不能保证所有节点的写一致性。时间的确定在分布式系统中是比较复杂的问题,在单点系统中,系统只有一个时间源,操作的先后顺序是比较确定的,即使是多线程的操作中,时间相差一般不会超过 100 毫秒级(从取当前时间值到用此时间进行写操作);但分布系统时间中,各个节点都要依赖于自己的时间源,即使使用 ntp 进行定时校对,也会比单节点系统的时间差异大,网络的延迟更加放大了时间的差异,一般在收到交易的时候要分别记上客户端时间和服务器时间,并抛弃时间差异过大的交易。

好了,有了这些基础可以去看看 seata 框架了。

seata 框架

seata 是 alibaba 开源的一套分布式事务方案,同时支持 TCC/AT/SAGA/XA 等多种分布式事务模型。它由 alibaba 的 fescar 升级而来,并合并了一些其它的技术。

seata 适合于微服务架构,其云版本 GTS 是 aliyun 企业分布式应用服务 EDAS 的一个组件。

基本概念

TC transaction coordinator 事务协调器

维护全局事务的运行状态、协调和驱动全局事务的提交或回滚

TM transaction manager 事务管理器

控制全局事务的边界,负责开启全局事务,并最终发起全局提交或回滚动

RM resource manager 资源管理器

控制子事务,负责子事务开启和状态管理,接收 TC 的指令,驱动子事务的提交和回滚动。

GT global transaction 全局事务

在全局范围内执行的事务

BT branch transaction 分支事务(本地事务)

在本地范围内执行的事务

seata 分成两个部分,seata server 和 seata client。

seata server 是一个独立的可运行组件,担当 TC 的角色,它可以注册到微服务的注册中心,被 client 发现和调用。

AT 模式

AT automatic transaction 自动事务模式,AT 模式基于 XA 演化,是 2PC 的改进,需要本地数据库支持的 ACID 。

需要分支事务支持 ACID,每个分支事务独立地完成工作,基本流程:

1. prepare 准备
2. 提交或回滚

基本条件:1) 本地数据库支持 ACID ;2) java 应用使用 jdbc 驱动访问数据库

seata 基于 jdbc 分析 sql 条件,从而确定 sql 的执行范围,在执行前、执行后分别获得数据的镜像数据,并写入到 undo_log 中,从而在失败时进行回滚。

机制

阶段1:提交业务数据 + 创建 undo 日志,然后释放锁和连接资源;

阶段2:
结果为 commit ,那么异步提交各个主事务;
结果为 rollback ,进行补偿,使用阶段1的回滚日志进行回滚;

写隔离

在阶段1提交前,先要获得一个全局锁,如果阶段1获取全局锁失败,则不能提交,可以进行多次尝试,超时之后取消本地事务。

两个写事务时,单个流程如下:

tx1: 
启动本地事务,本地锁
update m=m-100 where id=1
获取全局锁
本地事务提交
释放本地锁
全局提交
释放全局锁 

没有脏写问题

读隔离

本地数据库读已提交,无锁情况下,全局读未提交(如果改为读已提交,需要使用 select for update,相当于启动一次写事务)

select for update 获得一个全局锁,从而实现读已提交

执行过程

对 product 表执行 update product set name = 'GTS' where name = 'TXC'

过程如下:

阶段1

  1. 预备 sql,通过 sql 获得类型为 update,表名 product,通过 where 子句查出数据行,并取出数据

    进行数据定位

     select id, name, sice from productt where name = 'TXC'
    

    通过表结构,可得知 id 为键,并得到执行前数据镜像

     id  |  name | since
     1   |  TXC  | 2014
    
  2. 执行 update 语句,然后再次查询得到更新后的数据镜像

    查询语句

     select id, name, since from product where id = 1;
    

    结果

     id  | name  | since
     1   | GTS   | 2014
    
  3. 创建一个 undo_log (回滚日志),标记事务 id ,执行前、执行后结果

    日志格式

     {
         "branchId": 641789253,
         "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"
         }],
         "xid": "xid:xxx"
     }
    
  4. 提交前,通过 TC 获得 product 表全局锁,主键为 1

  5. 提交本地事务,更新 product 表和 undo_log 表,向 TC 汇报执行结果

阶段2

如果节点收到 TC 的 rollback 通知,那么通过 xid 和 子事务 id 找到 undo_log 日志,比较当前状态和 afterImage 的数据,如果一致则进行回滚

如果不一致,就要通知 TC

收到 commit 通知,那么把请求直接放到一个队列,马上返回,再异步在本地逐个删除队列的 undo_log 日志。

undo_log 格式

与数据库类型相关,不同数据库有点不一样,但基本构成都是 branch_id ,xid ,beforeImage/afterImage (rollback_info),时间 等。

TCC 模式

AT 借助了数据库的 ACID 特性,但分布式事务中,部分步骤不具备 ACID 能力,如长时间的人工操作、库存清点等,TCC (try-comfirm-cancel) 模式比较
适合这种场景。

TCC 需要一个微服务提供 3 种操作:

1. try 资源预留,扣减资源并创建 undo_log
2. commit 成功提交,删除 undo_log
3. cancel 取消,根据 undo_log 进行回滚

流程

TM -> TC : 启动 GT,得到 xid
服务 -> RM : prepare
RM -> TC : 注册 branch
RM -> TC : branch 状态报告
TC -> RM : commit / rollback

SAGA 模式

SAGA 是对长耗时事务的解决方案,比如一些交易步骤需要到人工参与的审核,或需要调用外部服务时,就是 saga 的合适场景。它与 TCC 不同,没有 prepare 动作。 见 https://www.jianshu.com/p/e4b662407c66?from=timeline&isappinstalled=0

每个 SAGA 由一系列的子事务组成,为 T1,T2..Tn ,每个 Ti 对应一个补偿动作 Ci,用于撤销 Ti 造成的结果。

执行时,执行 T1,T2..Tn,如果 Ti 出错,那么就要进行倒过来执行补偿动作,以撤销前 i-1 个结果(栈方式)。

每个本地事务都是原子的,但是全局不能实现读写隔离。

用于与外部系统集成、或其它组件不支持 TCC 的情况。

posted @ 2019-12-20 11:06  drop *  阅读(1621)  评论(0编辑  收藏  举报