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

一、从数据一致性谈起

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

1、数据一致性的情形

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

2、数据一致性的概念

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

3、数据一致性的原理

    • ACID
    • CAP
    • BASE

4、数据一致性的协议

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

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

  

  所谓分布式服务,就是把之前通过本地接口交互的模块,拆分成单独的应用独立部署,并通过远程接口和网络消息交互。且不管这样说严不严密,正不正确,理解就好。本文的重点也不是这个话题。简单画一张图辅助理解,如图。集中式架构,要想保证订单表和库存表的一致性,只要一个本地事务(ACID)就能保证两者的强一致性。分布式架构,订单表由订单服务操作,库存表由库存服务操作。要想保证订单表和库存表的一致性,那么就必须保证订单服务对订单表的操作和库存服务对库存表的操作同事成功。之前的一个本地事务就变成了一个分布式事务。由于服务之间通过网络交互+网络异常是常态,就会产生服务间数据不一致的情况。这就涉及一个分布式事务一致性的问题。

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

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

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

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

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

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

  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了。在蚂蚁金服内部被广泛地应用于交易、转账、红包等核心资金链路,服务于亿级用户的资金操作。

    关于TCC,个人认为,理解原理很重要。工作中遇到吻合的场景可以根据原理自行实现,满足业务即可。

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

模式分析: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服务不需要实时的结果。

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

模式分析: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:https://rocketmq.apache.org/docs/quick-start

四、保证操作幂等性的常用方法

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

:保证幂等性的方法很多,根据具体的业务场景,总能很容易找到保证幂等性的方法。

五、总结

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

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

作者: 尜尜人物
使命:为中华软件之崛起而编程
愿景:愿程序员皆因喜欢而编程
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第1章 课程介绍 介绍该课程的内容、学习成果、实例,还有学习所需的前提知识。 1-1 导学-分布式事务实践 第2章 事务原则与实现 介绍了事务的四大原则,并通过实例介绍数据库实现事务的方法,以及使用JDBC实现事务的方法。 2-1 事务原则与实现:事务 2-2 事务原则与实现:SQL事务 2-3 事务原则与实现:JDBC事务(上) 2-4 事务原则与实现:JDBC事务(下) 第3章 使用Docker搭建环境 介绍了Docker的使用,通过Docker将课程环境搭建起来,方便那些不了解这些技术的同学之后的学习。 3-1 docker简介与mysql安装-1 3-2 docker简介与mysql安装-2 3-3 SpringBoot基础 第4章 Spring事务机制 介绍了Spring的事务机制、事物抽象、内部事务和外部事物,以及常用的几种事务管理的实现,包括DataSource、JPA、JMS、JTA都通过实例进行说明。还有XA以及两阶段提交,并通过实例演示了使用JTA,通过两阶段提交,实现多数据源的事务实现。... 4-1 Spring事务机制_基本接口 4-2 Spring事务机制_实现 4-3 Jpa事务实例 4-4 Jms事务原理 4-5 Jms-session事务实例 4-6 Jms-spring事务实例 4-7 外部事务与JTA 4-8 JTA单数据源事务实例 4-9 JTA多数据源事务实例 第5章 分布式系统 介绍了分布式系统的定义、实现原则和几种形式,详细介绍了微服务架构的分布式系统,并使用Spring Cloud框架演示了一个完整的微服务系统的实现过程。 5-1 CAP原则和BASE理论简介 5-2 分布式系统综述 5-3 SpringCloud微服务架构 5-4 实现registry 5-5 实现proxy 5-6 user服务 5-7 order服务 5-8 添加hystrix 5-9 使用feign 5-10 优化服务间调用 第6章 分布式事务实现,模式和技术 介绍分布式事务的定义、原则和实现原则,介绍使用Spring框架实现分布式事务的几种方式,包括使用JTA、Spring事务同步、链式事务等,并通过实战介绍其实现。除此以外还介绍了一些分布式事务相关的技术,如幂等性、全局一致性ID、分布式对象等。... 6-1 分布式事务介绍 6-2 spring分布式事务实现_使用JTA 6-3 spring分布式事务实现_不使用JTA 6-4 实例1-DB-DB 6-5 实例1-DB-DB.链式事务管理器 6-6 实例2-JPA-DB.链式事务管理器 6-7 实例3-JMS-DB.最大努力一次提交 6-8 分布式事务实现模式与技术 6-9 全局一致性ID和分布式对象_ 第7章 分布式事务实现:消息驱动模式 详细介绍3种分布式事务实现的模式中的消息驱动模式并通过完整实例演示了消息驱动模式下,实现微服务系统的分布式事务的完整过程。 7-1 分布式事务实现:消息驱动模式 7-2 消息驱动模式实例:设计 7-3 消息驱动模式实例:创建ticket服务 7-4 消息驱动模式实例:实现基本ticket功能 7-5 消息驱动模式实例:锁票1 7-6 消息驱动模式实例:锁票2 7-7 按消息流程实现业务 7-8 支付过程 7-9 票转移 7-10 错误处理:锁票失败 7-11 错误处理:扣费失败 7-12 并发时的错误处理 第8章 分布式事务实现:Event Sourcing模式 详细介绍了分布式事务实现的模式中的Event Sourcing模式,并通过完整实例演示了Event Sourcing模式下,实现微服务系统的分布式事务的完整过程。 8-1 事件溯源模式介绍 8-2 事件溯源模式与Axon框架-1 8-3 事件溯源模式与Axon框架-2 8-4 使用Axon框架的设计过程介绍 8-5 Axon框架-实例(上) 8-6 Axon框架-实例(下) 8-7 Saga模式和Axon Saga 8-8 聚合命令事件(上) 8-9 聚合命令事件(下) 8-10 实现saga 8-11 实现query 8-12 处理超时 8-13 并发测试 8-14 cloud-axon实例:分布式处理介绍 8-15 事件设计 8-16 事件与队列设计 8-17 实现User服务 8-18 实现Ticket服务 8-19 实现Order服务 8-20 实现读写分离 8-21 测试与并发 8-22 事件溯源模式与Axon框架总结 第9章 TCC模式和微服务架构的设计模式 本章介绍TCC模式,也对微服务系统的几种设计模式,以及这些模式下分布式事务的实现模式进行了介绍。 9-1 TCC模式介绍 9-2 微服务架构的设计模式 第10章 课程总

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值