icql

分布式基础_分布式事务

一、概述

分布式环境下,解决多资源数据一致性的方案

事务具有的ACID特性:

Atomicity(原子性):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节
Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏
Isolation(隔离性):多个并发事务不会互相影响
Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失

分布式事务讨论的前提:

1985 年 Fischer、Lynch、Paterson 证明了如果宕机最后不能恢复,那就不存在任何一种分布式协议可以正确达成一致性结果
即宕机后最终一定会恢复
原子性:严格遵循
一致性:事务完成后的一致性严格遵循;事务中的一致性可适当放宽
隔离性:并行事务间不可影响;事务中间结果可见性允许安全放宽
持久性:严格遵循


举例:A转账给B
1) 单机本地事务,A+B 在隔离级别非(读未提交)的情况下,读操作得到的数据始终一致
2) 分布式事务,A服务扣钱,B服务加钱
事务中的一致性,由于A服务和B服务的最后提交无法在同一时刻,所以绝大多情况下无法保证读操作的数据一致性

目前只有一种情况可以在事务中保持强一致性,读、写操作串行化,锁为A和B的资源
但这种情况性能极差,基本不会采用


以下的分布式事务方案(强一致性方案、最终一致性方案)是按照数据不一致的时间长短来分类的,其中强一致性方案事务中也会有短暂的数据不一致
1) 成功(业务操作结果为成功)
2) 失败(业务操作结果为失败)
3) 异常(超时等未知异常,不知道业务操作的结果)

分布式事务主要补偿的操作时 3) 中的未知异常

二、强一致性方案

舍A保C的CP模型,即通过牺牲可用性来保证很高的一致性(在db层面)。适用于对一致性要求很高的场景,比如金融交易等

1. 2PC-二阶段提交

事务管理器给每个参与者发送Prepare消息
每个参与者要么直接返回失败,要么在本地执行事务,但不提交
如果事务管理器收到了任何一个参与者的 失败消息 或 超时
直接给每个参与者发送回滚(Rollback)消息
否则,发送提交(Commit)消息

参与者根据事务管理器的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源
优点:
1) 数据强一致性

缺点:
1) 单点问题
如果事务管理器出现问题,所有事务的参与者都将收到影响从而阻塞等待

2) 同步阻塞问题
两个阶段资源都将被锁住,同步阻塞范围较大,性能不佳
2PC提交的具体实现,是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准

第一阶段:
XA START xid
执行sql
XA END xid
XA PREPARE xid

第二阶段:
XA COMMIT xid 事务提交
XA ROLLBACK xid 事务回滚

其他命令
XA RECOVER: 查看PREPARED状态的xa事务

优点:
强一致性,隔离性
缺点:
性能较差,同步阻塞

2. 3PC-三阶段提交

为了解决2PC提交的单点故障问题、同步阻塞问题提出的3PC协议,较难实现,很少使用。

事务管理器向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应
1) 第一阶段中有参与者返回No或者超时,事务管理器执行事务中断
2) 第一阶段所有参与者均返回Yes,向所有参与者发送预提交请求
1) 第二阶段中有参与者返回No或者超时,事务管理器执行事务中断
2) 第二阶段所有参与者均返回Yes,向所有参与者发送提交请求
相比2PC,引入了参与者超时机制
如果事务管理器超时无响应发生在第二阶段,参与者不再继续等待去做其他事情
如果事务管理器超时无响应发生在第三阶段,参与者不再继续等待默认执行提交

优点:
减轻了 单点问题 和 同步阻塞问题

缺点:
如果事务管理器超时无响应发生在第三阶段,可能会引起数据不一致问题
实现困难,基本不使用

三、最终一致性方案

强一致性方案性能不理想,为了提升性能和可用性,基于BASE理论使用最终一致性代替强一致性

1. 本地事务表

1) 总事务拆分成多个子事务,或者简化合并为2个子事务(本地事务+外部事务)
2) 子事务需要支持 正向执行+逆向回滚 2个操作,且均需要幂等
3) 子事务执行结果只有3种:完成-成功、完成-失败、未完成-异常,对应的状态转移事件结果也只有这3种
1) 新增一条事务记录到本地数据库(如果有本地子事务,可直接合并在这一步处理)
2) 串行执行各个子事务(同阶段的并行子事务可合并为一个子事务)
(1) 子事务执行完成-成功/完成-失败,更新记录表中的事务状态
(2) 子事务执行未完成-异常,同步返回异常,定时任务进行事务补偿
*) 定时任务补偿,定期扫描事务表中未完成的子事务,重试执行直至成功/失败完成事务,或达到最大次数人工处理
典型本地事务表结构:
1) tx_id 事务ID
2) tx_type 事务类型:业务1、业务2
3) tx_state 事务状态:100-业务1-初始、190-业务1-成功、191-业务1-失败、192-业务1-超时
4) tx_state_record 事务状态流水
5) tx_unique 事务唯一标识,用于幂等
6) retry 当前重试次数
7) retry_next_time 下次重试时间
8) retry_max 最大重试次数
9) retry_interval 重试时间间隔:单位s
10) extend 扩展数据
11) create_time 创建时间
12) update_time 更新时间

2. 可靠消息

消息发送方完成本地事务后 一定能成功发送一条消息 ,消息接收方最终一定能接收到消息进行消费(需幂等),适用于异步且不需要回滚的场景

1) 本地消息表

对于不支持事务消息的消息队列中间件:
消息发送方利用类似 本地事务表 的方案来保证(本地事务+消息发送)保证消息一定发送成功

2) 事务消息

RocketMQ支持二阶段事务消息

基本原理:
1) 本地事务开启
2) 预发送消息(半事务消息,标记为不可投递)
3) sql执行
4) 发送二次确认消息
(1)本地事务成功,发送commit消息,半事务消息标记为可投递,投递给消费者
(2)本地事务失败,发送rollback消息,取消半事务消息,不会投递给消费者
5) 异常情况,服务端未收到二次确认消息,回查任意生产者查询本地事务状态来重新执行

3. 最大努力通知

消息发送方完成本地事务后 尽最大努力成功发送一条消息 ,不保证消息一定能发送成功

1) 如果消息接收方收到消息,则进行消费(需幂等)
2) 如果消息接收方没有收到消息,过指定时间后主动回查 消息发送方提供的接口 进行结果确认

一般适用于:对最终一致性时间敏感度低的结果通知类型的业务,例如事务完成后的结果通知(交易结果通知等)

4. TCC事务

业务层面上的2阶段提交。TCC主要用于处理一致性要求较高、需要较多灵活性的短事务

尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)

准隔离性这里其实指的是共享资源的锁,预留当前操作的资源
如果所有分支的Try都成功了,则走到Confirm阶段。Confirm真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源
如果所有分支的Try有一个失败了,则走到Cancel阶段。Cancel释放 Try 阶段预留的业务资源
优点:
一致性较高,适合短事务,适合需要资源锁定(准隔离性)的场景,例如a转账b

缺点:
业务侵入性更高,所有的事务参与方都需要提供三个操作接口Try/Confirm/Cancel
不适合一些难以改动的老旧系统或外部系统

5. SAGA事务

将长事务拆分为多个短事务,由Saga事务协调器协调,如果每个短事务都成功提交完成,那么全局事务就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作

正向操作:T1, T2, T3
逆向回滚:R1, R2, R3

四、异常处理

分布式事务由于NPC问题(这里主要是NP)导致的异常需要处理,以TCC为例:

1) 空补偿/悬挂问题:

异常情况下,cancel先于try执行

空补偿:Cancel执行时,Try未执行,事务分支的Cancel操作需要判断出Try未执行,这时需要忽略Cancel中的业务数据更新,直接返回
悬挂:Try执行时,Cancel已执行完成,事务分支的Try操作需要判断出Cancel已执行,这时需要忽略Try中的业务数据更新,直接返回

2) 幂等问题:
由于任何一个请求都可能出现网络异常,出现重复请求,所有的分布式事务分支操作,都需要保证幂等性

通用解决方案(参考子事务屏障dtm):

子事务屏障技术的原理是,在本地数据库,建立分支操作状态表dtm_barrier,唯一键为全局事务id-分支id-分支操作(try|confirm|cancel)

1) 开启本地事务
2) 对于当前操作op(try|confirm|cancel),insert ignore一条数据gid-branchid-op,如果插入不成功,提交事务返回成功(常见的幂等控制方法)
3) 如果当前操作是cancel,那么在insert ignore一条数据gid-branchid-try,如果插入成功(注意是成功),则提交事务返回成功
4) 调用屏障内的业务逻辑,如果业务返回成功,则提交事务返回成功;如果业务返回失败,则回滚事务返回失败

在此机制下,解决了乱序相关的问题:
1) 空补偿控制--如果Try没有执行,直接执行了Cancel,那么3中Cancel插入gid-branchid-try会成功,不走屏障内的逻辑,保证了空补偿控制
2) 幂等控制--2中任何一个操作都无法重复插入唯一键,保证了不会重复执行
3) 防悬挂控制--Try在Cancel之后执行,那么Cancel会在3中插入gid-branchid-try,导致Try在2中不成功,就不执行屏障内的逻辑,保证了防悬挂控制


示例:

唯一键:gid, branch_id(), branch_op, branch_barrier_id
insert ingnore xxx, 返回值=0失败,返回值=1成功

drop table if exists barrier;
create table if not exists barrier(
  id bigint(22) PRIMARY KEY AUTO_INCREMENT,
  trans_type varchar(45) default '',
  gid varchar(128) default '' COMMENT '全局事务id',
  branch_id varchar(128) default '' COMMENT '子事务分支id',
  branch_op varchar(45) default '' COMMENT '子事务分支操作, try|confirm|cancel',
  branch_barrier_id varchar(45) default '' COMMENT '子事务分支屏障id, 一个子事务分支可能有多个本地事务',
  reason varchar(45) default '' comment 'the branch type who insert this record',
  create_time datetime DEFAULT now(),
  update_time datetime DEFAULT now(),
  key(create_time),
  key(update_time),
  UNIQUE key(gid, branch_id, branch_op, branch_barrier_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

五、开源框架

dtm分布式解决方案,https://www.dtm.pub

seata分布式事务,https://github.com/seata/seata