软件架构场景之—— 数据一致性:下游服务失败上游服务如何独善其身?

本文主要是介绍软件架构场景之—— 数据一致性:下游服务失败上游服务如何独善其身?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

业务场景

使用微服务时,很多时候我们往往需要跨多个服务去更新多个数据库的数据,类似下图所示的架构

如图所示,如果业务正常运转,3 个服务的数据应该变为 a2、b2、c2,此时数据才一致。但是如果出现网络抖动、服务超负荷或者数据库超负荷等情况,整个处理链条有可能在步骤二失败,这时数据就会变成 a2、b1、c1,当然也有可能在步骤三失败,最终数据就会变成 a2、b2、c1,这样数据就对不上了,即数据不一致

为了针对数据一致性问题给出一个完美解决方案,把数据一致性的问题归类为以下 2 种场景

第一种场景:实时数据不一致不要紧,保证数据最终一致性就行

因为一些服务出现错误,导致图 1 的步骤三失败,此时处理完请求后,数据就变成了 a2、 b2、c1,不过不要紧,我们只需保证最终数据是 a2、b2、 c2 就行

业务场景:零售下单时,一般需要实现在商品服务中扣除商品的库存、在订单服务中生成一个订单、在交易服务中生成一个交易单这三个步骤。 假设交易单生成失败,就会出现库存扣除了、订单生成了、交易单没生成的情况,此时我们只需保证最终交易单成功生成就行,这就是最终一致性

第二种场景:必须保证实时一致性

如果图 1 中的步骤二和步骤三成功了,数据就会变成 b2、c2,但是如果步骤三失败,那么步骤一和步骤二会立即回滚,保证数据变回 a1、b1

业务场景:使用积分换折扣券时,需要实现扣除用户积分、生成一张折扣券给用户这 2 个步骤。如果我们还是使用最终一致性方案的话,有可能出现用户积分扣除了而折扣券还未生成的情况,此时用户进入账户一看,积分没了也没有折扣券,立马就会投诉,此时怎么办呢?我们直接将前面的步骤回滚,并告知用户处理失败请继续重试就行,这就是实时一致性

 

最终一致性方案

对于数据要求最终一致性的场景,实现思路是这样的

  1. 每个步骤完成后,生产一条消息给 MQ,告知下一步处理接下来的数据;
  2. 消费者收到这条消息后,将数据处理完成后,与步骤一一样触发下一步;
  3. 消费者收到这条消息后,如果数据处理失败,这条消息应该保留,直到消费者下次重试

为了方便理解这部分内容,梳理了一个大概的流程图,如下图所示

详细的实现逻辑如下(类似于用责任链模式实现)

  1. 调用端调用 Service A;
  2. Service A 将数据库中的 a1 改为 a2;
  3. Service A 生成一条步骤 2(姑且命名为 Step2)的消息给到 MQ;
  4. Service A 返回成功给调用端;
  5. Service B 监听 Step2 的消息,拿到一条消息
  6. Service B 将数据库中的 b1 改为 b2;
  7. Service B 生成一条步骤 3(姑且命名为 Step3)的消息给到 MQ;
  8. Service B 将 Step2 的消息设置为已消费;
  9. Service C 监听 Step3 的消息,拿到一条消息;
  10. Service C 将数据库中的 c1 改为 c2;
  11. Service C 将 Step3 的消息设置为已消费

接下来考虑下,如果每个步骤失败了该怎么办?

1. 调用端调用 Service A

解决方案:如果这步失败,直接返回失败给用户,用户数据不受影响

2. Service A 将数据库中的 a1 改为 a2

解决方案:如果这步失败,利用本地事务数据直接回滚就行,用户数据不受影响

3. Service A 生成一条步骤 2(姑且命名为 Step2)的消息给到 MQ

解决方案:如果这步失败,利用本地事务数据将步骤 2 直接回滚就行,用户数据不受影响

4. Service A 返回成功给调用端

解决方案:如果这步失败,不做处理

5. Service B 监听 Step2 的消息,拿到一条消息

解决方案:如果这步失败,MQ 有对应机制,我们无须担心

6. Service B 将数据库中的 b1 改为 b2

解决方案:如果这步失败,利用本地事务直接将数据回滚,再利用消息重试的特性重新回到步骤 5 

7. Service B 生成一条步骤 3(姑且命名为 Step3)的消息给到 MQ

解决方案:如果这步失败,MQ 有生产消息失败重试机制。要是出现极端情况,服务器会直接挂掉,因为 Step2 的消息还没消费,MQ 会有重试机制,然后找另一个消费者重新从步骤 5 执行

8. Service B 将 Step2 的消息设置为已消费

解决方案:如果这步失败,MQ 会有重试机制,找另一个消费者重新从步骤 5 执行

9. Service C 监听 Step3 的消息,拿到一条消息

解决方案:如果这步失败,参考步骤 5 的解决方案

10. Service C 将数据库中的 c1 改为 c2

解决方案:如果这步失败,参考步骤 6 的解决方案

11. Service C 将 Step3 的消息设置为已消费

解决方案:如果这步失败,参考步骤 8 的解决方案

 

实时一致性方案

实时一致性,其实就是我们常说的分布式事务

MySQL 其实有一个两阶段提交的分布式事务方案(MySQL XA),但是该方案存在严重的性能问题。比如,一个数据库的事务与多个数据库间的 XA 事务性能可能相差 10 倍。另外,在 XA 的事务处理过程中它会长期占用锁资源,所以一开始我们并不考虑这个方案

 

TCC 模式

在 TCC 模式中,我们会把原来的一个接口分为 Try 接口、Confirm 接口、Cancel 接口

  • Try 接口用来检查数据、预留业务资源
  • Confirm 接口用来确认实际业务操作、更新业务资源
  • Cancel 接口是指释放 Try 接口中预留的资源

比如积分兑换折扣券的例子中需要调用账户服务减积分、营销服务加折扣券这两个服务,那么针对账户服务减积分这个接口,我们需要写 3 个方法,如下代码所示

public boolean prepareMinus(BusinessActionContext businessActionContext, final String accountNo, final double amount) {    //校验账户积分余额    //冻结积分金额
}
public boolean Confirm(BusinessActionContext businessActionContext) {    //扣除账户积分余额    //释放账户 冻结积分金额
}
public boolean Cancel(BusinessActionContext businessActionContext) {    //回滚所有数据变更
}

同样,针对营销服务加折扣券这个接口,也需要写3个方法,而后调用的大体步骤如下

图中绿色代表成功的调用路径,如果中间出错,就会先调用相关服务的回退方法,再进行手工回退。原本只需要在每个服务中写一段业务代码就行,现在需要拆成 3 段来写,而且还涉及以下 5 点注意事项

  1. 我们需要保证每个服务的 Try 方法执行成功后,Confirm 方法在业务逻辑上能够执行成功;
  2. 可能会出现 Try 方法执行失败而 Cancel 被触发的情况,此时我们需要保证正确回滚;
  3. 可能因为网络拥堵出现 Try 方法的调用被堵塞的情况,此时事务控制器判断 Try 失败并触发了 Cancel 方法,后来 Try 方法的调用请求到了服务这里,此时我们应该拒绝 Try 请求逻辑;
  4. 所有的 Try、Confirm、Cancel 都需要确保幂等性;
  5. 整个事务期间的数据库数据处于一个临时的状态,其他请求需要访问这些数据时,我们需要考虑如何正确被其他请求使用,而这种使用包括读取和并发的修改

TCC 模式是一个很麻烦的方案,除了每个业务代码的工作量 X3 之外,出错的概率也高,因为需要通过相应逻辑保证上面的注意事项都被处理

 

Seata 中 AT 模式的自动回滚

对于使用 Seata 的人来说操作比较简单,只需要在触发整个事务的业务发起方的方法中加入@GlobalTransactional 标注,且使用普通的 @Transactional 包装好分布式事务中相关服务的相关方法即可

在 Seata 内在机制中,AT 模式的自动回滚往往需要执行以下步骤

一阶段

  1. 解析每个服务方法执行的 SQL,记录 SQL 的类型(Update、Insert 或 Delete),修改表并更新 SQL 条件等信息;
  2. 根据前面的条件信息生成查询语句,并记录修改前的数据镜像;
  3. 执行业务的 SQL;
  4. 记录修改后的数据镜像;
  5. 插入回滚日志:把前后镜像数据及业务 SQL 相关的信息组成一条回滚日志记录,插入 UNDO_LOG 表中;
  6. 提交前,向 TC 注册分支,并申请相关修改数据行的全局锁 ;
  7. 本地事务提交:业务数据的更新与前面步骤生成的 UNDO LOG 一并提交;
  8. 将本地事务提交的结果上报给事务控制器

二阶段-回滚

收到事务控制器的分支回滚请求后,会开启一个本地事务,并执行如下操作

  1. 查找相应的 UNDO LOG 记录;
  2. 数据校验:拿 UNDO LOG 中的后镜像数据与当前数据进行对比,如果存在不同,说明数据被当前全局事务之外的动作做了修改,此时我们需要根据配置策略进行处理;
  3. 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成回滚语句并执行;
  4. 提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报事务控制器

二阶段-提交

  1. 收到事务控制器的分支提交请求后,我们会将请求放入一个异步任务队列中,并马上返回提交成功的结果给事务控制器
  2. 异步任务阶段的分支提交请求将异步地、批量地删除相应 UNDO LOG 记录

这篇关于软件架构场景之—— 数据一致性:下游服务失败上游服务如何独善其身?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/335078

相关文章

Linux创建服务使用systemctl管理详解

《Linux创建服务使用systemctl管理详解》文章指导在Linux中创建systemd服务,设置文件权限为所有者读写、其他只读,重新加载配置,启动服务并检查状态,确保服务正常运行,关键步骤包括权... 目录创建服务 /usr/lib/systemd/system/设置服务文件权限:所有者读写js,其他

Linux下利用select实现串口数据读取过程

《Linux下利用select实现串口数据读取过程》文章介绍Linux中使用select、poll或epoll实现串口数据读取,通过I/O多路复用机制在数据到达时触发读取,避免持续轮询,示例代码展示设... 目录示例代码(使用select实现)代码解释总结在 linux 系统里,我们可以借助 select、

Java服务实现开启Debug远程调试

《Java服务实现开启Debug远程调试》文章介绍如何通过JVM参数开启Java服务远程调试,便于在线上排查问题,在IDEA中配置客户端连接,实现无需频繁部署的调试,提升效率... 目录一、背景二、相关图示说明三、具体操作步骤1、服务端配置2、客户端配置总结一、背景日常项目中,通常我们的代码都是部署到远程

vue监听属性watch的用法及使用场景详解

《vue监听属性watch的用法及使用场景详解》watch是vue中常用的监听器,它主要用于侦听数据的变化,在数据发生变化的时候执行一些操作,:本文主要介绍vue监听属性watch的用法及使用场景... 目录1. 监听属性 watch2. 常规用法3. 监听对象和route变化4. 使用场景附Watch 的

C#使用iText获取PDF的trailer数据的代码示例

《C#使用iText获取PDF的trailer数据的代码示例》开发程序debug的时候,看到了PDF有个trailer数据,挺有意思,于是考虑用代码把它读出来,那么就用到我们常用的iText框架了,所... 目录引言iText 核心概念C# 代码示例步骤 1: 确保已安装 iText步骤 2: C# 代码程

Pandas处理缺失数据的方式汇总

《Pandas处理缺失数据的方式汇总》许多教程中的数据与现实世界中的数据有很大不同,现实世界中的数据很少是干净且同质的,本文我们将讨论处理缺失数据的一些常规注意事项,了解Pandas如何表示缺失数据,... 目录缺失数据约定的权衡Pandas 中的缺失数据None 作为哨兵值NaN:缺失的数值数据Panda

C++中处理文本数据char与string的终极对比指南

《C++中处理文本数据char与string的终极对比指南》在C++编程中char和string是两种用于处理字符数据的类型,但它们在使用方式和功能上有显著的不同,:本文主要介绍C++中处理文本数... 目录1. 基本定义与本质2. 内存管理3. 操作与功能4. 性能特点5. 使用场景6. 相互转换核心区别

Java 缓存框架 Caffeine 应用场景解析

《Java缓存框架Caffeine应用场景解析》文章介绍Caffeine作为高性能Java本地缓存框架,基于W-TinyLFU算法,支持异步加载、灵活过期策略、内存安全机制及统计监控,重点解析其... 目录一、Caffeine 简介1. 框架概述1.1 Caffeine的核心优势二、Caffeine 基础2

python库pydantic数据验证和设置管理库的用途

《python库pydantic数据验证和设置管理库的用途》pydantic是一个用于数据验证和设置管理的Python库,它主要利用Python类型注解来定义数据模型的结构和验证规则,本文给大家介绍p... 目录主要特点和用途:Field数值验证参数总结pydantic 是一个让你能够 confidentl

JAVA实现亿级千万级数据顺序导出的示例代码

《JAVA实现亿级千万级数据顺序导出的示例代码》本文主要介绍了JAVA实现亿级千万级数据顺序导出的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面... 前提:主要考虑控制内存占用空间,避免出现同时导出,导致主程序OOM问题。实现思路:A.启用线程池