幂等性设计

1. 什么是幕等

  幂等是一个数学上的概念,它的含义是多次执行同一个操作和执行一次操作,最终得到的结果是相同的。 数学表达:f(x) = f(f(x))。
例子:比如,男生和女生吵架,女生抓住一个点不放,传递“你不在乎我了吗?”(生产消息)的信息。那么当多次抱怨“你不在乎我了吗?”的时候(多次生产相同消息),她不知道的是,男生的耳朵(消息处理)会自动把 N 多次的信息屏蔽,就像只听到一次一样,这就是幂等性。

  可以这么理解“幂等”:一件事无论做多少次都和做一次产生的结果是一样的,那么这件事儿就具有幂等性。

2. 为什么不能保证幕等

  在单机系统中,模块之间的通信都是进程内的本地函数调用,在这个整体失败和同步通信的模型中,要么进程整体崩溃,要么调用完成,不会存在其他的情况,但是在分布式系统中,程序不能保证 Exactly-once 的原因主要有以下两个:一、网络方面的原因;二、远端服务故障。

2.1 网络原因

  在分布式系统中,服务和服务之间都是通过网络来进行通信的,而这个网络是一个异步网络。在这个网络中,经过中间的路由器等网络设备的时候,会出现排队等待或者因为缓冲区溢出,导致消息被丢弃的情况,那么将一个消息从一个节点发送到另一个节点的时延是没有上界的,有可能非常快,比如 1 ms,也有可能是 1 分钟,甚至无穷大,这个时候就是出现消息丢失的情况。

  在服务间进行远程调用的时候,如果迟迟没有收到响应结果,为了系统整体的可用性,我们不能无限等待下去,只能通过超时机制来快速获得一个结果。其实这样做是将无界时延的异步网络模型,通过超时机制转化成了有界时延,这个方式大大减轻了我们在写程序时的心智负担。同时也引起了新的问题。
  我们在收到响应为“请求超时”的时候,无法判断是请求发送的过程中延迟了,远端服务没有收到请求;还是远端服务收到请求并且正确处理了,却在响应发送的过程中延迟了。

2.2 远端服务故障

  如果远端服务在收到请求之前发生了故障,我们会收到“网络地址不可达”的错误,对于这个错误,我们能明确判断请求没有被远端服务执行过。但是,如果远端服务是在收到请求之后发生了故障,导致无法响应而引起“请求超时”,我们无法判断请求是否被远端服务执行过,或者被部分执行过。

2.3 例子

  • 订单创建接口,第一次调用超时了,然后调用方重试了一次。是否会多创建一笔订单?
  • 订单创建时,我们需要去扣减库存,这时接口发生了超时,调用方重试了一次。是否会多扣一次库存?
  • 当这笔订单开始支付,在支付请求发出之后,在服务端发生了扣钱操作,接口响应超时了,调用方重试了一次。是否会多扣一次钱?

案例分析:
因为系统超时,而调用方重试一下,会给我们的系统带来不一致的副作用。在这种情况下,一般有两种处理方式。

  • 一种是需要下游系统提供相应的查询接口。上游系统在 timeout 后去查询一下。如果查到了,就表明已经做了,成功了就不用做了,失败了就走失败流程。 失败流程中需要考虑消息可能堵塞还未达的情况。
  • 另一种是通过幂等性的方式。也就是说,把这个查询操作交给下游系统,我上游系统只管重试,下游系统保证一次和多次的请求结果是一样的。

3. 如何保证幕等

  请求的消息带上一个全局唯一的ID,在首次调用和重试的时候,这个唯一的 ID 都保持不变,服务器收到带有相同的ID的消息只执行一次。新的问题:全局唯一的ID怎样生成?

3.1 全局ID

  在前面例子中,要做到幂等性的交易接口,需要有一个唯一的标识,来标志交易是同一笔交易。而这个交易 ID 由谁来分配是一件比较头疼的事。因为这个标识要能做到全局唯一。
  如果由一个中心系统来分配,那么每一次交易都需要找那个中心系统来。 这样增加了程序的性能开销。如果由上游系统来分配,则可能会出现 ID 分配重复的问题。因为上游系统可能会是一个集群,它们同时承担相同的工作。

3.1.1 方法一:UUID

  为了解决分配冲突的问题,我们需要使用一个不会冲突的算法。使用 UUID 算法,可以生成冲突非常消息的全局唯一ID。UUID 存在的问题:

  • 字符串占用的空间比较大,索引的效率非常低;
  • 生成的 ID 太过于随机,不易阅读;
  • 创建的UUID不是有序,无法排序。

UUID 生成算法,后续单独介绍。

3.1.2 方法二:Snowflake

Twitter 的开源项目 Snowflake 可以生成全局唯一 ID。Snowflake是一个分布式 ID 的生成算法。它的核心思想是,产生一个 long 型的 ID,其中:

  • 41bits 作为毫秒数。大概可以用 69.7 年。
  • 10bits 作为机器编号(5bits 是数据中心,5bits 的机器 ID),支持 1024 个实例。
  • 12bits 作为毫秒内的序列号。一毫秒可以生成 4096 个序号。

流程如下:
幂等性设计_第1张图片

3.2 例子处理流程

  前文中交易例子,支持幂等性之后,处理流程需要过滤一下已经收到的交易。要做到这个事,我们需要一个存储来记录收到的交易。
  当收到交易请求的时候,我们就会到这个存储中去查询。如果查找到了,那么就不再做查询了,并把上次做的结果返回。如果没有查到,那么我们就记录下来。
幂等性设计_第2张图片

上面这个流程实现效率很低,出现延时响应的请求应该是少部分。让所有请求都到这个存储里去查一下,这会导致处理流程变得很慢。想上面的案例,使用覆盖插入就可以。

我们的服务是分布式,幂等性服务也是分布式,所以,需要的这个存储也是共享的。这样每个服务就变成没有状态的。这个存储就成了一个非常关键的依赖,其扩展性和可用性也成了非常关键的指标。我们可以使用关系型数据库,或是 key-value 的 NoSQL(如 MongoDB)来构建这个存储系统。

4. 注意事项

  • 当接口现实了幂等性,出现超时需要重试的时候,需要限制重试的间隔和次数,确保系统不会受到局部故障的影响,导致整体雪崩;重试间隙可以按倍数递增,例如这次间隙是上次间隙时间的2倍。

参考:

  • 《极客时间——左耳听风》
  • 《极客时间——深入浅出分布式技术原理》

你可能感兴趣的:(分布式,分布式,系统架构)