前言
本章主要对分布式事务进行梳理和讲解。可能在业务设计过程中,各微服务都采用了独立数据库,所以,这些微服务之间的数据共享有了更高的要求:要解决数据一致性的问题。
1. 数据一致性
数据一致性是指:数据被多次操作或者以多种方式操作时,能保持业务的不变性。数据一致性存在于很多场景中。导致数据不一致的原因也不尽相同。
-
在对数据库中的内容进行读写时,不是每个用户都能读取到正确的数据,可能在读取时数据未同步过来。
-
在分布式系统中,在Leader节点的“写”数据同步到各个Slave节点时,如果有一个节点未同步,则会造成数据不一致。
-
在多个业务方对同一个数据源获取数据时,如果业务方没有核对数据就进行相关计算,则会因为不同业务方使用了不同的处理逻辑,从而导致多个业务方最终的结果数据不一致。
-
在使用缓存的场景中,缓存更新策略的不同会造成缓存与数据库的数据不一致。
场景:
对于数据一致性问题,很典型的案例就是银行转账:在双方转账前后,双方的余额总和应该是不变的,这就是数据一致性。例如账户A向账户B转了100元:
-
如果转账成功,则账户A扣掉100元,账户B增加100元。
-
如果转账失败,则双方账户余额保持不变。
这就是数据一致性的具体表现。如果账户A的余额变少了,而账户B的余额没有增加,则数据是不一致的。
2. 数据库事务保证数据一致性
数据一致性的根本问题是如何保证操作的原子性。在关系型数据库中,可以利用其ACID(Atomic、Consistency、Isolation、Durability)特性来保证原子操作,即在同一个事务内的操作都是原子性的,所有操作要么都成功,要么都失败。只要事务被提交了,即使机器宕机了,数据都是一致的。
下面来分析一下关系型数据库的ACID特性:
-
Atomic(原子性):事务中的所有语句要么都成功,要么都失败,只要其中一条语句失败,就会回滚整个事务,不会出现“更新了其中一张表,而没有更新其他表”的情况。
-
Consistency(一致性):事务总是从一个有效状态转移到另一个有效状态,即它保证读取的数据总是一致的。
-
Isolation(隔离性):在一个事务中,所做的任何操作,在未提交前,其对其他事务都是不可见的。事务是互相隔离的,自己运行自己的,互不影响。
-
Durability(持久性):只要事务提交成功了,数据的所有修改就会持久化到磁盘中,即使宕机也不会造成数据的丢失或改变。
(在很多场景中采用数据库事务来解决数据的一致性问题。一般编程框架都支持数据库事务,例如在Spring框架中使用注解@Transactional来开启事务,其中的所有操作属于同一个事务。)
3. 分布式事务
前面介绍了通过数据库事务来保证数据的一致性,但很多公司中,都是由多个系统协作处理且涉及多个数据库的操作,用单一数据库事务很难解决数据一致性的问题。
有这样一种场景:分布式系统被分为多个系统,这些系统在不同的计算机进程中运行,且每个系统都有自己独立的数据库,一次请求操作会在这几个数据库中进行更新。这个场景比前面说的单数据库事务复杂太多。所以,接下来分析在分布式系统环境如何利用分布式事务来解决分布式系统的数据一致性问题。
(分布式事务也是事务,只不过它是在分布式系统中的事务。分布式事务由多个本地食物组合而成,其基本满足ACID特性)
随着分布式系统越来越复杂,为了满足系统的高性能及高可用,分布式系统是不完全满足ACID特性的。目前实现分布式事务的解决方案有如下几种:
-
基于XA协议的二阶段提交。
-
基于XA协议的三阶段提交。
-
TCC。
-
基于消息的最终一致性。
每一种分布式事务解决方案都有各自擅长的场景:
-
基于XA协议的二阶段和基于XA协议的三阶段提交方案,只针对强一致性,遵从ACID特性。
-
TCC方案,适合作用在服务层,不与具体的服务框架耦合。
-
基于消息的最终一致性方案是针对最终一致性的,遵从BASE理论。
在企业系统日常开发中,基于XA协议的二阶段提交和基于消息的最终一致性方案较多。下面就看下这两种分布式事务方案:
(1)基于XA协议的二阶段提交
“二阶段提交”中的“二阶段”指准备阶段和提交阶段。为了保证分布在不同节点上的事务保持一致,在“基于XA协议的二阶段提交”中引入了“事务协调者”的角色,通过它来保证各个事务被正确提交,如果提交失败则回滚所有事务。
-
在第一阶段(即准备阶段)中,“事务协调者”会向所有“事务参与者”发送“准备”指令,然后等待“事务参与者”的回复。“事务参与者”在收到“准备”指令后,就各自进行自己的业务处理,除提交事务的所有动作(包括开启本地事务和逻辑处理等)外,还要记录操作的事务日志。如果执行成功,则“事务参与者”向“事务协调者”回复“已准备完成”的消息;如果在执行过程中出现失败或者超时,则“事务参与者”向“事务协调者”发送“准备失败”的消息。
-
在第二阶段(即提交阶段)中,“事务协调者”依据“事务参与者”在第一阶段返回的消息执行相应的操作:如果都是“已准备完成”,则发送“提交指令”给“事务参与者”,“事务参与者”继续完成之前剩下的真正事务提交动作;如果有“事务参与者”返回“准备失败”的消息,则向所有“事务参与者”发送“回滚”指令,然后所有“事务参与者”回滚第一阶段中的所有操作。
(只有当“事务协调者”收到所有“事务参与者”回复的“提交成功”消息,才表示整个分布式事务结束了)
以订单系统和库存系统下单扣减库存为例,感受一下二阶段提交是如何保证分布式事务一致性的。
在第一阶段中,“事务协调者”向订单系统和库存系统发送“准备”指令。订单系统完成自己的创建订单相关动作,完成后,不提交实物,只是向“事务协调者”回复“已准备完成”的消息。库存系统完成自己的扣减库存动作,在完成后不提交事务,只是向“事务协调者”回复“已准备完成”的消息。
在第一阶段中,“事务协调者”收到了订单系统及库存系统“已准备完成”的消息。
接下来进入第二阶段,“事务协调者”向订单系统和库存系统发送“提交”指令,然后,订单系统提交创建订单的事务,库存系统提交扣减库存的事务,接着各自都向“事务协调者”发送“提交成功”的消息:
只要其中一个过程失败,则整个事务回滚,回到所有系统之前的状态。例如,在扣减库存时出现库存不足
二阶段提交算法是一种强一致性设计,适用于对数据一致性要求很高的场景,但是它也有一些缺陷,例如:
-
每个节点都是事务阻塞的,这样对于并发很多的请求就会出现吞吐率下降的情况。
-
“事务协调者”是单点的,有不可用的风险。
(2)基于消息的最终一致性。
可以利用消息中间件(如RocketMQ)来达到数据的最终一致性。
以“浏览商品并将商品加入购物车,然后进入购物车下单”的流程为例,来分析如何使用消息中间件来达到数据 的最终一致性。
场景:订单系统创建一个新的订单信息,然后购物车系统删除已存在于订单中的商品。
① 订单系统创建订单消息,并将其发送给RocketMQ,此时消息状态为“待确认”。
② 订单消息在RocketMQ中被持久化存储,存储消息状态为“待发送”。
③ 如果消息被持久化成功,则订单系统开始创建订单;如果失败,则放弃创建订单。
④ 订单系统在创建完订单后,将创建订单成功的消息发送给RocketMQ。
⑤ RocketMQ收到创建订单成功的消息,然后将消息状态设置为“可发送”,接着,RocketMQ将当前创建订单成功的消息发送给购物车系统。
⑥ 购物车系统在获取创建订单成功的消息后,删除购物车中的商品。
⑦ 购物车系统在完成自己的操作后,通知ROcketMQ将当前创建订单成功的消息改为“已完成”状态。
在分布式系统场景中,只有系统中所有节点事务都成功,整个分布式事务才算成功。目前还没有一种既简单又完美的方案来应对所有场景,需要根据实际业务去做相应的取舍。
本章主要介绍了分布式事务管理的思想和设计方案,小伙伴们需要根据自己所在公司的业务场景,制定合适的技术落地方案,确保业务是符合公司业务场景需要的。
下一章开始,会讲解生成级系统框架设计的细节,第一章会讲幂等性的设计及思路。希望有兴趣的小伙伴能够转发和分享