字节一面:事务补偿和事务重试,关系是什么?

说在前面

在尼恩的(50+)读者社区中,经常指导大家面试架构,拿高端offer。最近,小伙伴在面试字节、平安的过程中,遇到一个 非常、非常高频的一个面试题,但是很不好回答,类似如下:

  • 说说分布式中的补偿机制, 补偿和重试有何关系?
  • 「事务补偿」和「重试」,它们之间的关系是什么?
  • 谈谈分布式系统中的补偿机制如何设计

这里尼恩给大家做一下系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”

也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典 PDF》V99版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到公号【技术自由圈】获取

一、为什么要考虑补偿机制?

我们都知道,在分布式环境中运行的应用程序在通信时可能会遇到一个主要问题,即一个业务流程通常需要整合多个服务,而仅一次通信就可能涉及 DNS 服务、网卡、交换机、路由器、负载均衡等设备。

以电商的购物场景为例:

客户端 ---->购物车微服务 ---->订单微服务 ----> 支付微服务。

这种调用链非常普遍。

那么为什么需要考虑补偿机制呢?

正如前面所说,一次跨机器的通信可能会经过DNS 服务,网卡、交换机、路由器、负载均衡等设备,这些设备都不一定是一直稳定的,在数据传输的整个过程中,只要任意一个环节出错,都会导致问题的产生。

而在分布式场景中,一个完整的业务又是由多次跨机器通信组成的,所以产生问题的概率成倍数增加。

这些服务和设备并不总是稳定可靠的。在数据传输过程中,只要任何一个环节出现问题,都可能引发故障。

在微服务环境中,这种情况更加突出,因为业务需要在一致性上得到保障。

也就是说,如果一个步骤出现失败,要么需要持续重试以确保所有步骤都顺利完成,要么将服务调用回滚到之前的状态。

因此,我们可以这样理解业务补偿:当某个操作出现异常时,通过内部机制消除由该异常引发的「不一致」状态。

大家经常看到:「补偿」和「事务补偿」或者「重试」,它们之间的关系是什么?

二、如何进行补偿?

业务补偿设计的实现方式主要可分为两种:回滚(事务补偿)和重试

  • 回滚(事务补偿),这是一种逆向操作,通过回滚业务流程来解决问题,这意味着当前的操作已经失败;

  • 重试,这是一种正向操作,通过不断地尝试来完成业务流程,代表着仍有成功的可能性。

通常情况下,业务事务补偿需要一个工作流引擎的支持。这个事务工作流引擎将各种服务连接在一起,并在工作流上进行业务补偿,以达到最终一致性。

因为「补偿」已经是一个额外的流程,既然能够走额外的流程,说明时效性并不是第一考虑的因素,所以做补偿的核心要点是:宁可慢,不可错。

因此,不能草率地确定补偿方案,需要谨慎评估。虽然错误无法完全避免,但我们应以尽量减少错误为目标。

1、回滚

回滚分为两种形式:

  • 显式回滚(逆向调用接口):通过调用逆向接口,执行上一次操作的反操作,或者取消上一次未完成的操作(需要锁定资源);
  • 隐式回滚(无需逆向调用接口):意味着这个回滚动作无需额外处理,通常由下游提供失败处理机制。

显式回滚

最常见的显示回滚就是做两件事:

  • 首先,确定失败的操作和状态,从而确定回滚范围。一个业务流程在设计之初就已经规划好,因此确定回滚范围相对容易。但需要注意的是,如果在一个业务处理过程中涉及到的服务并非都提供了「回滚接口」,那么在服务编排时应将提供「回滚接口」的服务放在前面,以便在后续服务出错时还有机会进行「回滚」。
    简而言之,要确保回滚接口有机会被调用。最优的选择是将其放在第一个。
  • 其次要提供进行「回滚」操作所需的业务数据。提供的回滚数据越多,越有利于程序的健壮性。因为程序在接收到「回滚」操作时可以进行业务检查,例如检查账户是否相等,金额是否一致等。

在这个过程中,数据结构和大小并不确定。因此,最好将相关数据序列化为 JSON,并存储在 NoSQL 数据库中。

隐式回滚

隐式回滚的使用场景相对较少。它意味着回滚操作无需额外处理,下游服务内部具有类似"预占"和"超时失效"的机制。

例如:

在电商场景中,会将订单中的商品预占库存,等待用户在规定时间内支付。如果用户未在规定时间内支付,则释放库存。

回滚的实现方式

对于跨库的事务,常见的解决方案有:两阶段提交、三阶段提交(ACID)但是这 2 种方式,在高可用的架构中一般都不可取,因为跨库锁表会消耗很大的性能。

在高可用架构中,通常不要求强一致性,而是追求最终一致性。可以考虑使用事务表、消息队列、补偿机制、TCC 模式(占位/确认或取消)和 Sagas 模式(拆分事务 + 补偿机制)来实现最终一致性。

2、重试

“重试”的含义是我们认为故障是暂时的,而不是永久的,所以,我们会去重试。这种方法的最大优势在于无需提供额外的逆向接口,这对于代码维护和长期开发的成本有优势,同时考虑到业务的变化,逆向接口也需要随之变化。因此,在许多情况下,可以考虑使用重试。

使用场景

然而,相较于回滚操作,重试的使用场景较少。

  • 当下游系统返回请求超时,或受到限流等临时状态影响时,我们可以考虑采用重试。
  • 如果返回的结果是余额不足,无权限等明确的业务错误,就不需要重试。
  • 对于一些中间件或 RPC 框架,如果返回的是 503,404 等无法预期恢复时间的错误,也不需要重试。

重试策略

为了实施重试,我们需要制定一个重试策略,主流的重试策略主要包括以下几种:

**1.立即重试:**如果故障是暂时性的,可能是由于网络数据包冲突或硬件组件高峰流量等事件引起的,这种情况下,适合立即重试。但是,立即重试的次数不应超过一次,如果立即重试失败,应改用其他策略。

2.固定间隔: 这个很容易理解,比如每隔 5 分钟重试一次。需要注意的是,策略 1 和策略 2 通常用于前端系统的交互操作。

3.增量间隔: 这个也很简单,比如间隔 15 分钟重试一次。

return (retryCount - 1) * incrementInterval;

其主要目的是让重试失败的任务优先级靠后,让新的重试任务进入队列。

4.指数间隔: 与增量间隔类似,只是增长的幅度更大。

return 2 ^ retryCount;

5.全抖动: 在递增的基础上,增加随机性,适用于在某一时刻有大量请求需要分散压力的场景。

return random(0 , 2 ^ retryCount);

6.等抖动: 在指数间隔和全抖动之间找到一个平衡点,降低随机性的使用。

int baseNum = 2 ^ retryCount;
return baseNum + random(0 , baseNum);

3、4、5、6 策略的表现大致如下所示。(x 轴为重试次数)

为什么说重试有坑呢?

正如之前所提到的,出于对开发成本的考虑,如果重试涉及到接口调用,就需要考虑 幂等性 的问题。

幂等性起源于数学概念,后来被引入到程序设计中。它意味着一个操作可以被多次执行,而不会产生错误。

因此,一旦某个功能支持重试,整个链路上的解耦都需要考虑幂等性的问题,以确保多次调用不会导致业务数据的变化。

实现幂等性的方法是将其过滤掉:

  1. 为每个请求分配一个唯一的标识符。
  2. 在重试过程中,判断该请求是否已经执行过或正在执行。如果是,就丢弃该请求。

对于第一点,可以使用全局 ID 生成器、ID 生成服务,或者简单地使用 Guid、UUID 为每个请求赋值。

对于第二点,可以使用 AOP 在业务代码前后进行校验。

//【方法执行前】
if(isExistLog(requestId)){
    
      //1。判断请求是否已被接收过。对应序号3
    var lastResult = getLastResult();  //2。获取用于判断之前的请求是否已经处理完成。对应序号4
    if(lastResult == null){
    
     
        var result = waitResult();  //挂起等待处理完成
        return result;
    }
    else{
    
    
        return lastResult;
    } 
}
else{
    
    
    log(requestId);  //3。记录该请求已接收
}
//do something。。【方法执行后】
logResult(requestId, result);  //4。将结果也更新一下。

如果 「补偿」 这个过程是通过消息队列(MQ)进行的,那么可以在 MQ 封装的 SDK 中直接实现。在生产端为请求分配全局唯一标识符,在消费端通过唯一标识进行去重。

重试的最佳实践

重试特别适合在高负载情况下进行降级。同时,它也应受到限流和熔断机制的影响。当重试与限流熔断结合使用时,才能达到最佳效果。

在增加补偿机制时,需要权衡投入与产出。对于一些不太重要的问题,应该选择 「快速失败」 而不是 「重试」 。

过度积极的重试策略(例如间隔太短或重试次数过多)可能会对下游服务产生负面影响,这一点需要特别注意。

一定要为 「重试」 设定一个终止策略。当回滚过程困难或代价较大时,可以接受较长的间隔和较多的重试次数。实际上,DDD 中经常提到的「saga」模式也是基于这种思路。但前提是不会因为保留或锁定稀缺资源而阻止其他操作(例如,1、2、3、4、5 个串行操作,由于 2 操作一直未完成,导致 3、4、5 无法继续进行)。

三、业务补偿机制的注意事项

1、ACID 还是 BASE

在分布式系统中,ACID 和 BASE 代表了两种不同层次的一致性理论。

在分布式系统里,ACID 还是 BASE的区别:

  • ACID 的一致性较强,但可扩展性较差,仅在必要时使用;
  • 而 BASE 的一致性相对较弱,但具有很好的可扩展性,并支持异步批量处理,适用于大多数分布式事务。

在重试或回滚的情境下,我们通常不需要强一致性,只需确保最终一致性即可。

2、业务补偿设计的注意事项

业务补偿设计的注意事项:

  • 为了完成一个业务流程,需要涉及到的服务支持幂性,并且上游需要有重试机制;
  • 我们需要仔细维护和监控整个过程的状态,所以最好不要将这些状态分布在不同的组件中,最好是由一个业务流程的控制方来负责,也就是一个工作流引擎。因此,这个工作流引擎需要具有高可用性和稳定性;
  • 补偿的业务逻辑和流程不一定要是严格的反向操作。有时可以并行执行,有时可能会更简单。
    总的来说,在设计业务正向流程时,也需要考虑业务的反向补偿流程;
  • 我们需要明确,业务补偿的业务逻辑与具体业务紧密相关,很难做到通用;
  • 下层的业务方最好提供短期的资源预留机制。例如在电商中,可以将商品库存预留以便等待用户在 15 分钟内支付。如果没有收到用户的支付,就释放库存,然后回滚到之前的下单操作,等待用户重新下单。

所以,这才是“教科书式” 答案

结合 字节的方案,大家回到前面的面试题:

  • 说说分布式中的补偿机制, 补偿和重试有何关系?
  • 「事务补偿」和「重试」,它们之间的关系是什么?
  • 谈谈分布式系统中的补偿机制如何设计

以上的方案,才是完美的答案,才是“教科书式” 答案。

后续,尼恩会给大家结合行业案例,分析出更多,更加劲爆的答案。

当然,如果遇到这类问题,可以找尼恩求助。

参考文献

https://zhuanlan.zhihu.com/p/258741780

推荐阅读

炸裂,靠“吹牛”过京东一面,月薪40K

太猛了,靠“吹牛”过顺丰一面,月薪30K

炸裂了…京东一面索命40问,过了就50W+

问麻了…阿里一面索命27问,过了就60W+

百度狂问3小时,大厂offer到手,小伙真狠!

饿了么太狠:面个高级Java,抖这多硬活、狠活

字节狂问一小时,小伙offer到手,太狠了!

收个滴滴Offer:从小伙三面经历,看看需要学点啥?

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓

猜你喜欢

转载自blog.csdn.net/crazymakercircle/article/details/132456546
今日推荐