分布式事务一致性解决方案

1、从数据一致性谈起

一致性问题,“万恶之源”是数据冗余和分布并经过网络交互+网络异常是常态。html

一、数据一致性的情形

    • 主库、从库和缓存数据一致性,相同数据冗余,关系数据库,为保证关据库的高可用和高性能,通常会采用主从(备)架构并引入缓存。其中数据不一致性存在于数据冗余的时间窗口内。经常使用的解决方案见MySQL数据库之互联网经常使用架构方案
    • 多副本数据之间的数据一致性,相同数据副本,大数据领域,一份数据会有多个副本并存储到不一样的节点上。客户端能够访问任何一个节点进行读写操做。经常使用的解决方案是基于Paxos、ZAB、Raft、Quorum、Gossip等的开源实现。这里只是一提,暂不探讨。感兴趣能够自行谷歌或百度。
    • 分布式服务之间的数据一致性,相关数据分布,分布式服务,不一样的服务操做不一样的库(表),并且库(表)间要保持一致。经常使用的解决方案是分布式事务一致性解决方案。这也是本文要探讨的内容。

二、数据一致性的概念

    • 强一致性
    • 弱一致性
    • 最终一致性

三、数据一致性的原理

    • ACID
    • CAP
    • BASE

四、数据一致性的协议

    • 两阶段提交协议
    • 三阶段提交协议
    • TCC协议
    • Paxos协议
    • ZAB协议
    • Raft协议
    • Quorum协议
    • Gossip协议

2、分布式服务间的数据一致性

所谓分布式服务,就是把以前经过本地接口交互的模块,拆分红单独的应用独立部署,并经过RPC和MQ交互。拿电商中的订单和库存举例(新增一条订单记录,库存就要-1),集中式架构中,要想保证订单表和库存表的一致性,只要一个本地事务(ACID)就能保证二者的强一致性;而分布式架构中,订单表由订单服务操做,库存表由库存服务操做。要想保证订单表和库存表的一致性,那么就必须保证订单服务对订单表的操做和库存服务对库存表的操做同时成功。以前的一个本地事务就变成了一个分布式事务。因为服务之间经过网络交互+网络异常是常态,就会产生服务间数据不一致的状况。这就涉及一个分布式事务一致性的问题。git

3、分布式事务一致性解决方案

一、接口同步调用模式与一致性解决方案

模式分析:A服务同步调用B服务的接口并等待结果返回,后续的流程会依赖B服务的返回结果。这种交互模式下,A服务获得的结果细分有三种状况。github

  1. 请求发起阶段网络超时或异常,此时,B服务未收到请求,未做出相应的处理;
  2. 结果返回阶段网络超时或异常,此时,B服务已收到请求,并做出相应的处理;
  3. 正常结果返回(明确的成功或失败)。

业务场景:适用于大规模、高并发的短小操做且依赖返回值的场景。例如,交易服务和库存服务(卡券服务、红包服务等)的交互、用户登陆和准入服务的交互等。数据库

解决方案:方案一,服务调用方查询重试方案;方案二,TCC方案。apache

:这两种方案,保证数据一致最终仍是靠“异步”,只不过须要快速校准,准实时。缓存

  1. 服务调用方查询重试方案,适合一个从业务服务场景。
     1 下单减库存方法() {
     2     // 1.准备操做
     3     // 2.重试调用B服务
     4     result = RetryUtil {
     5         while(重试次数 < 最大重试次数) {
     6             try {
     7                 if (重试次数 != 0) {
     8                     // case1:网络超时或异常(catch分支)
     9                     // case2:查询到扣减库存操做,result=成功(return)
    10                     // case3:查不到扣减库存操做,result=失败(继续下面操做)
    11                     result = rpc.查询扣减库存是否成功();
    12                     if (result == 成功) {
    13                         return result;
    14                     }
    15                 }
    16                 // case1:网络超时或异常(catch分支)
    17                 // case2:扣减库存成功,result=成功(return)
    18                 // case3:扣减库存失败,result=失败(return)
    19                 return rpc.扣减库存();
    20             } catch (Exception e) {
    21                 if (重试次数 = 最大重试次数) {
    22                     // 报警,人工处理或者(近实时)对帐系统自动校准
    23                     // 抛出异常,中断后续流程
    24                     throw 自定义异常; //或者result封装异常
    25                 }
    26             }
    27         }
    28     };
    29     // 3.后续操做
    30 }
    View Code

    :1) 查询重试后依然失败(极少),报警,人工处理或者准实时对帐系统自动校准;服务器

      2) 重试次数不宜多,甚至只重试一次;网络

      3) B服务处理请求要作幂等。架构

  2. TCC方案,适合多个从业务服务场景。TCC是阿里在二阶段提交协议的基础上提出的一种解决分布式事务一致性的协议,原理图以下。其对应的产品是DTX(老版是DTS)。DTS中有个快速开始的例子看明白了,TCC就基本OK了。在蚂蚁金服内部被普遍地应用于交易、转帐、红包等核心资金链路,服务于亿级用户的资金操做。

   

  注:1)关于TCC,我的认为,理解原理很重要。工做中遇到吻合的场景能够根据原理自行实现,知足业务便可;并发

    2)一个开源实现:tcc-transaction

    3分布式事务一致性解决方案之TCC

二、接口异步调用模式与一致性解决方案

模式分析:A服务调用B服务,B服务先受理请求并落库,状态是待处理。B服务处理请求很耗时,或者要依赖其余的服务。B服务处理完后通知A服务或者A服务定时去查询B服务的处理结果。这种交互模式下,对于CASE-1,第1步和第2步同接口同步调用模式,第3步同消息异步处理模式;对于CASE-2,至关于两次接口同步调用模式

业务场景:适用于非核心链路上负载较高的处理环节,这个环节常常耗时较长,而且对时效性要求不高。例如,用户提现时,帐户系统和提现系统的交互(CASE-1);提现系统和三方系统(银行系统或者三方托管系统)的交互(CASE-2)。

解决方案服务被调方最大努力处理方案。因为B服务中请求有落库,因此能够用定时任务不断重试尽最大努力将请求处理出结果。处理后,将请求状态设置成对应的结果落库。而后再通知A服务或者A服务异步主动查询。

 1 受理请求方法() {
 2     // 1.请求落库,状态为待处理
 3     // 2.返回受理结果
 4     if (落库成功) {
 5         // 返回受理成功
 6     } else {
 7         // 返回受理失败
 8     }
 9 }
10 
11 定时任务处理请求方法() {
12     // 1.扫描待处理请求
13     try {
14         // 2.处理请求
15         if (处理成功) {
16             // 设置请求处理状态为处理成功
17         } eles {
18             //  设置请求处理状态为处理失败
19         }
20     } catch (Exception e) {
21         // 不作任何操做,请求状态依旧为待处理
22     }
23     // 3.消息通知A服务处理结果或者等待A服务查询处理结果
24 }
View Code

:1) B服务一般都是接受请求并持久化后才返回A服务受理成功。避免服务进程被杀掉而致使请求丢失。

  2) 不论是第(1,2)两步仍是CASE-2中的第(3,4)两步,若是查询重试失败,能够落库,用定时任务处理,知道成功。反正不像接口同步调用模式,A服务不须要实时的结果。

三、消息异步处理模式与一致性解决方案

模式分析:A服务将B服务须要的信息经过消息中间件传递给B服务,A服务无需知道B服务的处理结果。这种交互模式下,消息生产者要确保消息发送成功;消息消费者要确保消息消费成功。

业务场景:消息异步处理模式与接口异步调用模式相似,多应用于非核心链路上负载较高的处理环节中,井且服务的上游不关心下游的处理结果,下游也不须要向上游返回处理结果。例如,在电商系统中,用户下订单支付且交易成功后,发送消息给物流系统或者帐务系统进行后续的处理。

解决方案生产者最大努力通知+消费者最大努力处理方案。

  1. 非事务消息,生产者先执行本地事务并将消息落库,状态标记为待发送,而后发送消息。若是发送成功,则将消息改成发送成功。定时任务定时从数据库捞取在必定时间内待发送的消息并将消息发送。经过定时任务来保证消息的发送。为确保消息必定能消费,消费者通常采用手动ACK机制,那么消息服务器必然会重发未ACK的消息,这就要求消息消费者作好幂等。

     1 交易完成发消息方法() {
     2     // 1.设置交易状态为已完成
     3     // 2.消息落库,状态为待发送
     4     // 可异步发送,也建议异步发送
     5     try {
     6         // 3.发送消息
     7         if (发送成功) {
     8             // 设置消息状态为发送成功
     9         }
    10     } catch (Exception e) {
    11         // 不作任何操做,消息状态依旧为待发送
    12     }
    13 }
    14 
    15 定时任务发送消息方法() {
    16     // 1.扫描待发送消息
    17     try {
    18         // 2.发送消息
    19         if (发送成功) {
    20             // 设置消息状态为发送成功
    21         }
    22     } catch (Exception e) {
    23         // 不作任何操做,消息状态依旧为待发送
    24     }
    25 }
    View Code
  2. 事务消息,以RocketMQ为例,下图是RocketMQ事务消息的流程。官网有示例代码。和不支持事务的消息中间相比,只是消息发送的时候,保证了和本地事务的一致。消费者实现仍是不变。

:1) 定时任务重试发送消息和消息服务器重发未ACK的消息通常都是时间阶梯式的(2n*时间间隔);

  2) 支持事务消息中间件之RocketMQ

4、保证操做幂等性的经常使用方法

  1. 有业务状态,业务逻辑来保证幂等。好比接到支付成功的消息订单状态变成支付完成,若是当前状态是支付完成,则再收到一个支付成功或者支付成功以前状态的消息则说明消息重复了,不用再次处理。
  2. 无业务状态,业务惟一ID保证幂等。增长一个去重表(或分布式缓存)来记录有业务惟一ID的操做。好比调用充值接口,当请求过来时,会根据惟一充值ID去查充值流水表,若已经存在,则直接返回;不然继续进行充值操做。

:保证幂等性的方法不少,根据具体的业务场景,总能找到保证幂等性的方法。

5、总结

  1. 接口同步调用模式,服务调用方查询重试方案和TCC方案。
  2. 接口异步调用模式,服务被调方最大努力处理方案。
  3. 消息异步处理模式,生产者最大努力通知+消费者最大努力处理方案。
  4. 任何服务操做都须要提供一个查询接口,用来向外部输出操做执行的状态。
  5. 永远不要在本地事务中调用远程服务,在这种场景下若是远程服务出现了问题,则会拖长事务,致使应用服务器占用太多的数据库链接,让服务器负载迅速攀升,在严重状况下会压垮数据库。
  6. 最后一道防线 - 对帐系统。
  7. 同步和异步的抉择:
    • 能够异步的地方,就应该异步实现。若是业务逻辑容许,则咱们能够将一些耗时较长的、用户对响应没有特别要求的操做异步化,以此来减小核心链路的层级,释放系统的压力。
    • 能用同步解决的问题,不要引入异步。若是性能不是问题,或者所处理的操做是短小的轻量级处理逻辑,那么同步调用方式是最理想不过的,由于这样不须要引入异步化的复杂处理流程。

注:若是,以上场景和解决方案,没能包含您工做中遇到的场景,欢迎交流,并共同讨论解决方案。

相关文章
相关标签/搜索