一般的事务处理,分为一下几种:
- 本地事务
- 共享事务
- 全局事务
- 分布式事务
本地事务
常见的本地事务,有数据库的事务, redis事务。即是一种单服务,单数据源的提交形式
共享事务
这是一个伪需求,实际上多服务,单数据源的场景很少见
全局事务
这是单服务, 多数据源的场景,一种强一致性的事务解决方案
有以下实现:
2PC: 两阶段提交
提交过程: 准备、提交
缺点:单点问题, 性能问题(同步阻塞), 一致性风险
3PC: 三阶段提交
提交过程: CanCommit, PreCommit, DoCommit
缺点: 单点问题和回滚性能有所改善, 一致性问题依旧存在
分布式事务
谈起分布式,不得不说CAP理论
CAP理论
- C: 一致性
- P:分区容忍性
- A: 可用性
三者最多只能满足其中的两项,这是一种多服务,多数据源的场景
同时,根据CAP理论引申出了BASE理论
- BA: 基本可用(Basically Available)
- Soft State: 软状态, 状态可以有一段时间不同步
- Eventually Consistent:最终一致性, 最终数据是一致的就可以,不是时刻都保持一致
从而,基于不同的一致性需求产生了不同的分布式事务解决方案,追求强一致的两阶段提交、追求最终一致性的柔性事务和事务消息
最终一致性(base理论): AP without C
需要考虑的因素:
- 锁定资源时长(吞吐量,阻塞状态);
- 协调者单点故障;
- 脑裂(参与者接受指令不一致);
实现方式
可靠消息队列
可靠消息的最终一致性,最大努力通知,支持的mq有阿里的rocket mq
要解决问题:消息丢失和重复消费问题
消息丢失场景
-
MQ 自动应答机制导致的消息丢失; 订阅消息事件的服务在接收服务投递的消息后,消息中间件(如 RabbitMQ)默认是开启消息自动应答机制,当系统消费了消息,消息中间件就会删除这个持久化的消息,因此你要采取编程的方式手动发送应答。
-
高并发场景下的消息积压导致消息丢失
队列双向确认
消息重复消费场景
消息日志表:两个字段,消息 ID 和消息执行状态 (幂等性保证的前提)
想要解决“消息丢失”和“消息重复消费”的问题,有一个前提条件就是要实现一个全局唯一 ID 生成的技术方案。
在分布式系统中,全局唯一 ID 生成的实现方法有:
- 数据库自增主键
- UUID
- Redis
- Twitter-Snowflake 算法
XA事务
业务无侵入, 原理类似2PC
AT事务
业务无侵入, 基于SAGA数据补偿来代替回滚的思路
在业务数据提交时,自动拦截所有SQL,分别保存SQL对数据修改前后结果的快照,生成行锁,通过本地事务一起提交到操作的数据源中,这就相当于自动记录了重做和回滚日志。
AT 事务是参照了 XA 两段提交协议来实现的,但针对 XA 2PC 的缺陷,即在准备阶段,必须等待所有数据源都返回成功后,协调者才能统一发出 Commit 命令而导致的木桶效应(所有涉及到的锁和资源,都需要等到最慢的事务完成后才能统一释放),AT 事务也设计了针对性的解决方案,即是:自动记录了重做和回滚日志,同时基于 支持本地 ACID 事务 的 关系型数据库:
-
一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
-
二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
-
二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
不过需要注意读隔离和写隔离的问题,以确保数据补偿能成功
写隔离
-
一阶段本地事务提交前,需要确保先拿到 全局锁 。
-
拿不到 全局锁 ,不能提交本地事务。
-
拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理
AT缺点:
1. 脏读: 默认隔离级别是读未提交
2. 脏写:当在本地事务提交之后、分布式事务完成之前,该数据被补偿之前又被其他操作修改过,即出现了脏写(DirtyWirte)
优化:
使用全局锁来实现读写隔离机制,来避免脏读和脏写的发生,所以,基于这种补偿方式,分布式事务中所涉及的每一个数据源都可以单独提交,然后立刻释放锁和资源。
AT事务这种异步提交的模式,相比2PC极大地提升了系统的吞吐量水平。而使用的代价就是大幅度地牺牲了隔离性,甚至直接影响到了原子性。因为在缺乏隔离性的前提下,以补偿代替回滚不一定总能成功(脏写)
TCC
Try、Confirm、 Cancel, 原理类似3PC
业务侵入, Try:尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好事务需要用到的所有业务资源(冻结金额,库存等,保障隔离性)
Confirm:确认执行阶段,不进行任何业务检查,直接使用Try阶段锁定的资源来完成业务处理。注意,Confirm阶段可能会重复执行,因此需要满足幂等性。
Cancel:取消执行阶段,释放Try阶段锁定的业务资源。注意,Cancel阶段也可能会重复执行,因此也需要满足幂等性
适用场景
适合用于需要强隔离性的分布式事务中,基于TCC实现分布式事务,会将原来只需要一个接口就可以实现的逻辑拆分为Try 、Confirm 、 Cancel三个接口,所以代码实现复杂度相对较高, 是一种业务侵入性较强的事务方案,要求业务处理过程必须拆分为“预留业务资源”和“确认 / 释放消费资源”两个子过程, 才能够有效避免“超售”的问题。TCC 最主要的限制在Try阶段需要锁定资源,如果依赖外部系统的资源,这个锁定资源的操作就显得不可控了。
SAGA
业务无侵入,一种全新的思路: 基于数据补偿代替回滚, 能提高长时间事务效率,避免大事务长时间锁定数据库的资源。
事务基于数据补偿代替回滚的解决思路,与 TCC 相比,SAGA 不需要为资源设计冻结状态和撤销冻结的操作,补偿操作往往要比冻结操作容易实现得多。
但由于 Saga 事务不保证隔离性, 在极端情况下可能由于脏写无法完成回滚操作
比如举一个极端的例子, 分布式事务内先给用户A充值, 然后给用户B扣减余额, 如果在给A用户充值成功, 在事务提交以前, A用户把余额消费掉了, 如果事务发生回滚, 这时则没有办法进行补偿了。
实践中一般的应对方法是:
-
业务流程设计时遵循“宁可长款, 不可短款”的原则, 长款意思是客户少了钱机构多了钱, 以机构信誉可以给客户退款, 反之则是短款, 少的钱可能追不回来了。所以在业务流程设计上一定是先扣款。
-
有些业务场景可以允许让业务最终成功, 在回滚不了的情况下可以继续重试完成后面的流程, 所以状态机引擎除了提供“回滚”能力还需要提供“向前”恢复上下文继续执行的能力, 让业务最终执行成功, 达到最终一致性的目的。
过程:
1. 定义一系列子事务:Ti
2. 定义一系列子事务的补偿动作:Ci
数据恢复策略
正向恢复
正向恢复的执行模式为:T1,T2,…,Ti(失败),Ti(重试)…,Ti+1,…,Tn。
反向恢复
反向恢复的执行模式为:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1。
流程示意图:
实践: 基于某些分布式事务中间件实现(Seata)
综合对比下几种分布式事务解决方案
- 一致性保证:XA > TCC = SAGA > 事务消息
- 业务友好性:XA > 事务消息 > SAGA > TCC
- 性 能 损 耗:XA > TCC > SAGA = 事务消息
END