跳转至

微服务架构技巧篇:全局幂等

你好,我是华仔。

上一节我们讲解了微服务架构“数据分布式”挑战的应对方式:分布式事务。分布式事务都是基于接口或者消息来协作的,而分布式环境下,接口调用可能会失败,消息可能丢失,我们只能采取重试的方式来尽力保证数据最终能够达到一致性。重试机制就涉及微服务架构设计的另外一个技巧:全局幂等

其中,幂等(Idempotence)是一个数学与计算机科学中的概念,它指的是某个操作或函数可以多次执行并且保持同样的效果,即无论执行多少次,结果都不会改变。具体来说,如果一个操作是幂等的,那么对同一个输入重复应用这个操作将不会产生额外的效果或变化。

举个最简单的例子:当我们在网上购物,使用微信或者支付宝支付的时候,点击确认支付后网络正好断了,你也不知道是否支付成功,于是又重新刷新支付了一次。对于微信和支付宝来说,如果不做幂等判断,就会出现一个订单支付了两次的情况;而有了幂等判断,就能够保证一个订单只支付一次。

你可能会有疑问,为什么我们要在幂等前面加上“全局”两个字呢?其实这也是微服务架构的“服务分布式”特点决定的:同样的接口请求或者消息,并不一定会被同一个微服务实例处理。因此我们需要在全局的多个微服务范围内保证“幂等”,所以叫“全局幂等”。

全局幂等方案总体设计原理是将业务操作理解为一个对象,对象本身具备“全局唯一标识”和“状态”,通过状态控制来实现幂等特性。不同的全局幂等模式,区别主要体现在“全局唯一标识”如何生成、如何判断幂等状态、应用在什么场景这三个方面。

接下来,我们就来详细讲解全局幂等常见的模式和技巧。

事务级同步处理模式

事务级同步处理模式指的是在分布式事务处理中,将消息处理和业务处理同步进行的模式。其基本原理如下:

假设A服务是分布式事务的发起者,B服务是分布式事务的执行者,A服务和B服务都基于数据库实现全局幂等。具体的处理步骤如下:

  1. A服务需要下游B服务配合实现分布式事务,首先为B服务需要执行的分布式事务分配一个全局ID,然后写入一条事件记录相关信息,包括但不限于分布式事务执行状态、发起时间、更新时间、业务参数等。
  2. A服务发送事件消息给B服务。
  3. B服务收到事件消息后,首先提取消息中的全局ID进行幂等判断,如果发现已经处理过了就直接读取本地记录的处理结果返回给A服务;如果发现没有处理过,则进行分布式事务的处理,处理完成后同步记录事务处理结果,包括分布式事务的全局ID、状态(done/fail等)、完成时间等。
  4. B服务处理完成后返回事件处理消息给A服务。
  5. A服务更新本地记录的事件状态为“完成”(注意不一定是“成功”,只要是B服务返回了处理结果,无论“成功”“失败”都是完成)。

上述处理步骤中,如果A服务在指定时间内没有收到B服务返回的执行结果,可能的原因有4种:

  • A服务发出的消息丢失了(对应步骤2)
  • B服务返回的消息丢失了(对应步骤4)
  • 也可能是B服务挂掉了
  • B服务事务处理过程中出异常了

无论是哪种情况,A服务都需要重发事件消息,如果多次重发(一般尝试3次)后还是失败,A服务可能要采取告警等手段来提示人工进行处理。

事务级同步处理模式比较适合需要同步处理、处理简单、处理速度快的场景,一般通过接口调用来交互,例如使用TCC分布式事务Try接口扣库存。

事务级异步处理模式

事务级异步处理模式指的是在分布式事务处理中,将消息处理和业务处理分开进行的模式。其基本原理如下:


可以看到,事务级异步处理模式整体逻辑和“事务级同步处理模式”大体一致,区别在于B服务的处理有所不同。B服务收到事件消息后,先保存事件消息(对应上图步骤3),再异步处理业务操作,完成后再更新本地的事件消息状态。

事务级异步处理模式比较适合需要处理速度慢处理时间长的场景,一般通过消息队列消息来交互,例如使用SAGA分布式事务来处理项目管理过程中的一些流程。

接口级自动幂等

前面我们讲解的两种模式主要应用在分布式事务协作的场景。除此以外,一些关键的业务操作和接口调用也需要保证幂等,例如用户下单、支付等。这就需要我们实现接口级自动幂等的处理。

接口级自动幂等指的是提供接口的微服务在接口处理逻辑里面实现幂等,防止异常情况下,业务重复处理导致用户损失或者用户体验不好。

接口级自动幂等的基本原理如下:


假设A服务是接口调用方,B服务是接口提供方,其处理逻辑如下:

  1. A服务发起接口调用,接口参数中包含全局ID,或者能够从接口参数中组装出全局ID。
  2. B服务收到接口调用后,提取或者组装全局ID,进行幂等判断。如果已经处理过,则直接读取之前保存的处理结果;如果没有处理过,则进行业务处理。
  3. B服务返回接口调用结果给A服务。

接口级自动幂等处理过程中使用的全局ID,可以由调用方生成,例如在前端生成唯一Token,也可以从接口参数中组装出来,例如“用户ID + 订单ID”来标识一次支付请求。

常用幂等判断技术手段

全局幂等方案中的关键技术点就是如何进行“幂等判断”,其核心思想是通过“状态”来进行幂等判断。简单的状态可以用“是”和“否”来代表是否已经处理过了,复杂的状态就需要用“状态机”来进行状态判断了,例如订单状态包括“Pending Payment”“Paid”“Shipped”等。

具体实现的时候,我们可以基于数据库、Redis、ZooKeeper等系统。下面简单介绍一些常用的实现幂等判断的技术手段。

首先是数据库唯一索引。数据库唯一索引可以保证不会插入重复数据,而全局幂等中的“全局唯一标识”就可以用来做唯一索引,通过判断插入数据是否成功就可以实现简单的“是”和“否”的幂等判断。这种方式一般用于创建数据的业务场景,例如用户注册、创建订单等。

其次是Redis SETNX命令。Redis SETNX命令用于在指定的key不存在时,设置key的值。我们可以通过创建名字是“全局唯一标识”的key来实现简单的“是”和“否”的幂等判断。这种方式一般用于分布式事务、定时任务调度等场景。

然后是ZooKeeper创建节点。ZooKeeper创建节点的时候,如果节点已经存在则会失败。我们可以创建名为“全局唯一标识”的节点来实现简单的“是”和“否”的幂等判断。这种方式和Redis SETNX的方式基本类似,但性能上要差一些,且要维护ZooKeeper集群,整体要复杂和麻烦一些。如果系统已经用了Redis,更推荐基于Redis来做。

最后是状态机判断。如果全局幂等的状态比较复杂,则需要使用“状态机”来进行幂等判断。一般使用数据库来保存状态,在更新的时候使用“Update …… Where …… and status = XXX”的方式来进行幂等处理。

小结

这一节课,我们重点讲解全局幂等的常用处理模式和技巧。其中“事务级同步处理模式”适合短时分布式事务的处理,“事务级异步处理模式”适合长时分布式事务的梳理,“接口级自动幂等”适合业务上需要进行防重复处理的场景。常用的简单状态的幂等判断可以基于数据库唯一索引、Redis、ZooKeeper等系统来实现,复杂状态的幂等判断需要结合状态机来实现。

思考题

以上就是今天的全部内容,最后留一道思考题给你吧:你现在做的业务中是否有全局幂等的处理?如果有的话,采用的是什么处理模式和技术手段?

欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。

精选留言(2)
  • 蒹葭残辉 👍(0) 💬(2)

    之前参与的对接外部平台的证券类的交易系统架构和老师说的很类似,有幸参与了架构设计和开发。感觉听都是这么说,全都能串起来了。 在这个系统中,分交易服务、交易网关服务和三方平台交易服务(某官方交易机构)。 用户报价请求由Nginx分发给交易服务,交易服务采用了订单号加订单状态进行幂等(这里通过的是数据库for update加报价状态判断),若幂等校验通过将生成全局唯一操作编号,将携带操作编号字段将这个操作发送给交易网关。 交易网关服务接收到交易服务请求后,通过数据库唯一键的方式,尝试写入操作记录。并生成一个三方系统外部请求编号,并采用同步处理模式,将这个操作请求发送给了三方交易系统服务。 三方交易系统采用的是异步处理模式,处理完成后,回调交易网关服务告知操作结果,交易网关服务根据回传的三方系统外部请求编号获取到之前的操作记录,更新订单状态,并通过交易服务。 因此,在这个系统中,涵盖了老师说的几乎所有种的模式。但这其中有一些异常场景,比如,交易服务发给交易网关,未获取到网关响应结果,可能原因有: 1 交易服务发给交易网关,结果结易网关响应值丢失 2 交易网关未收到三方机构的异步响应报文 3 交易网关在接收到三方机构的响应报文时处理异常 等等 因此,交易服务需要有不断重试的机制。当交易服务触发重试时,为防止重复操作,调整为查询逻辑。交易网关先查询三方机构报文是否已经接收完毕,若完成则直接返回查询结果。若未完毕,则主动调用三方机构查询接口进行同步查询,并返回查询结果。 看似设计非常美好,但在有一天压测时,发现,交易服务给交易网关发送了一个报价的操作,由于网关层数据库压力大,导致在写唯一键的时候阻塞时间过长,导致交易服务请求超时。最终导致了交易服务在10秒后重试调用网关进行报价查询,结果此时写唯一键的操作仍然没有提交,网关查询为空,直接响应交易服务此次操作失败,结果在30秒后,网关操作成功,此时又给了交易服务操作成功的消息,此时交易服务接收到成功报文直接是一脸的懵,对于这个问题,不知道老师可有好的建议?

    2025-02-03

  • Capricornus 👍(0) 💬(1)

    老师,您好。我有个问题,在商城中购买商品,状态只能从下单,购买,付款,发货这样的流程,其中付款只能付款一次。 在业务逻辑中的具体做法,对比数据库的“状态”字段,如果该商品或者订单id(唯一标识)已经付款,就不调用支付宝等支付的接口,直接返回。 这算是保证幂等性了吗?

    2025-01-08