大师兄

94 | 项目实战二:设计实现一个通用的接口幂等框架(设计)

上一节课,我们介绍了幂等框架的一个重要需求场景,接口超时重试。为了避免同一业务被多次重复执行,接口需要支持幂等特性。同时,我们还对功能性需求和非功能性需求做了梳理。今天,我们来讲解幂等框架的设计思路。

跟限流框架类似,幂等框架的功能性需求也比较简单,但要考虑处理的异常情况有很多,比如业务代码异常、业务系统宕机、幂等框架异常。今天,我们重点讲解如何应对这些异常情况,设计一个高度容错的幂等框架。

话不多说,让我们正式开始今天的学习吧!

幂等处理正常流程

调用方从发起接口请求到接收到响应,一般要经过三个阶段。第一个阶段是调用方发送请求并被实现方接收,第二个阶段是执行接口对应的业务逻辑,第三个阶段是将执行结果返回给调用方。为了实现接口幂等,我们需要将幂等相关的逻辑,添加在这三个阶段中。

正常情况下,幂等号随着请求传递到接口实现方之后,接口实现方将幂等号解析出来,传递给幂等框架。幂等框架先去数据库(比如Redis)中查找这个幂等号是否已经存在。如果存在,说明业务逻辑已经或者正在执行,就不要重复执行了。如果幂等号不存在,就将幂等号存储在数据库中,然后再执行相应的业务逻辑。

正常情况下,幂等处理流程是非常简单的,难点在于如何应对异常情况。在这三个阶段中,如果第一个阶段出现异常,比如发送请求失败或者超时,幂等号还没有记录下来,重试请求会被执行,符合我们的预期。如果第三个阶段出现异常,业务逻辑执行完成了,只是在发送结果给调用方的时候,失败或者超时了,这个时候,幂等号已经记录下来,重试请求不会被执行,也符合我们的预期。也就是说,第一、第三阶段出现异常,上述的幂等处理逻辑都可以正确应对。

但是,如果第二个阶段业务执行的过程出现异常,处理起来就复杂多了。接下来,我们就看下幂等框架该如何应对这一阶段的各种异常。我分了三类异常来讲解,它们分别是业务代码异常、业务系统宕机、幂等框架异常。

业务代码异常处理

当业务代码在执行过程中抛出异常的时候,我们是否应该认定为业务处理失败,然后将已经记录的幂等号删除,允许重新执行业务逻辑呢?

对于这个问题,我们要分业务异常和系统异常来区分对待。那什么是业务异常?什么是系统异常呢?我举个例子解释一下。比如,A用户发送消息给B用户,但是查询B用户不存在,抛出UserNotExisting异常,我们把这种业务上不符合预期叫做业务异常。因为数据库挂掉了,业务代码访问数据库时,就会报告数据库异常,我们把这种非业务层面的、系统级的异常,叫做系统异常。

遇到业务异常(比如UserNotExisting异常),我们不删除已经记录的幂等号,不允许重新执行同样的业务逻辑,因为再次重新执行也是徒劳的,还是会报告异常。相反,遇到系统异常(比如数据库访问异常),我们将已经记录的幂等号删除,允许重新执行这段业务逻辑。因为在系统级问题修复之后(比如数据库恢复了),重新执行之前失败的业务逻辑,就有可能会成功。

实际上,为了让幂等框架尽可能的灵活,低侵入业务逻辑,发生异常(不管是业务异常还是系统异常),是否允许再重试执行业务逻辑,交给开发这块业务的工程师来决定是最合适的了,毕竟他最清楚针对每个异常该如何处理。而幂等框架本身不参与这个决定,它只需要提供删除幂等号的接口,由业务工程师来决定遇到异常的时候,是否需要调用这个删除接口,删除已经记录的幂等号。

业务系统宕机处理

刚刚分析的是代码异常,我们再来看下,如果在业务处理的过程中,业务系统宕机了(你可以简单理解为部署了业务系统的机器宕机了),幂等框架是否还能正确工作呢?

如果幂等号已经记录下了,但是因为机器宕机,业务还没来得及执行,按照刚刚的幂等框架的处理流程,即便机器重启,业务也不会再被触发执行了,这个时候该怎么办呢?除此之外,如果记录幂等号成功了,但是在捕获到系统异常之后,要删除幂等号之前,机器宕机了,这个时候又该怎么办?

如果希望幂等号的记录和业务的执行完全一致,我们就要把它们放到一个事务中。执行成功,必然会记录幂等号;执行失败,幂等号记录也会被自动回滚。因为幂等框架和业务系统各自使用独立的数据库来记录数据,所以,这里涉及的事务属于分布式事务。如果为了解决这个问题,引入分布式事务,那幂等框架的开发难度提高了很多,并且框架使用起来也复杂了很多,性能也会有所损失。

针对这个问题,我们还有另外一种解决方案。那就是,在存储业务数据的业务数据库( 比如MySQL)中,建一张表来记录幂等号。幂等号先存储到业务数据库中,然后再同步给幂等框架的Redis数据库。这样做的好处是,我们不需要引入分布式事务框架,直接利用业务数据库本身的事务属性,保证业务数据和幂等号的写入操作,要么都成功,要么都失败。不过,这个解决方案会导致幂等逻辑,跟业务逻辑没有完全解耦,不符合我们之前讲到的低侵入、松耦合的设计思想。

实际上,做工程不是做理论。对于这种极少发生的异常,在工程中,我们能够做到,在出错时能及时发现问题、能够根据记录的信息人工修复就可以了。虽然看起来解决方案不优雅,不够智能,不够自动化,但是,这比编写一大坨复杂的代码逻辑来解决,要好使得多。所以,我们建议业务系统记录SQL的执行日志,在日志中附加上幂等号。这样我们就能在机器宕机时,根据日志来判断业务执行情况和幂等号的记录是否一致。

幂等框架异常处理

我们前面提到,限流框架本身的异常,不能导致接口响应异常。那对于幂等框架来说,是否也适用这条设计原则呢?

对于限流来说,限流框架执行异常(比如,Redis访问超时或者访问失败),我们可以触发服务降级,让限流功能暂时不起作用,接口还能正常执行。如果大量的限流接口调用异常,在具有完善监控的情况下,这些异常很快就会被运维发现并且修复,所以,短暂的限流失效,也不会对业务系统产生太多影响。毕竟限流只是一个针对突发情况的保护机制,平时并不起作用。如果偶尔的极个别的限流接口调用异常,本不应该被放过的几个接口请求,因为限流的暂时失效被放过了,对于这种情况,绝大部分业务场景都是可以接受的。毕竟限流不可能做到非常精确,多放过一两个接口请求几乎没影响。

对于幂等来说,尽管它应对的也是超时重试等特殊场景,但是,如果本不应该重新执行的业务逻辑,因为幂等功能的暂时失效,被重复执行了,就会导致业务出错(比如,多次执行转账,钱多转了)。对于这种情况,绝大部分业务场景都是无法接受的。所以,在幂等逻辑执行异常时,我们选择让接口请求也失败,相应的业务逻辑就不会被重复执行了。毕竟接口请求失败(比如转钱没转成功),比业务执行出错(比如多转了钱),修复的成本要低很多。

重点回顾

好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。

今天,我们讲解了幂等框架的设计思路。在正常情况下,幂等框架的处理流程是比较简单的,调用方生成幂等号,传递给实现方,实现方记录幂等号或者用幂等号判重。但是,幂等框架要处理的异常情况很多,这也是设计的复杂之处和难点之处。

我们针对三种不同类型的异常,讲解了幂等框架的应对思路。

对于业务代码异常,为了让幂等框架尽可能的灵活,低侵入业务逻辑,发生异常(不管是业务异常还是系统异常),是否允许再重试执行业务逻辑,交给开发这块业务的工程师来决定。

对于业务系统宕机,对于这种极少发生的异常,在工程中,我们能够做到,在出错时能及时发现问题、能够根据记录的信息人工修复,就可以了。所以,我们建议业务系统记录SQL的执行日志,在日志中附加上幂等号。这样我们就能在机器宕机时,根据日志来判断业务执行情况和幂等号的记录是否一致。

对于幂等框架异常,跟限流框架异常处理对策不同,在幂等逻辑执行异常时,我们选择让接口请求也失败,相应的业务逻辑就不会被重复执行了,业务就不会出错。毕竟接口请求失败,比业务执行出错,修复的成本要低很多。

虽然幂等框架要处理的异常很多,但考虑到开发成本以及简单易用性,我们对某些异常的处理在工程上做了妥协,交由业务系统或者人工介入处理。这样就大大简化了幂等框架开发的复杂度和难度。

课堂讨论

我常说,异常情况考虑是否全面,处理是否得当,很能体现一个程序员的逻辑思维能力、工程能力。除了我们今天讲到的异常,在幂等框架中,你还能想到有哪些其他异常情况会发生?又该如何应对呢?

欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。