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 个序号。
流程如下:
3.2 例子处理流程
??前文中交易例子,支持幂等性之后,处理流程需要过滤一下已经收到的交易。要做到这个事,我们需要一个存储来记录收到的交易。 ??当收到交易请求的时候,我们就会到这个存储中去查询。如果查找到了,那么就不再做查询了,并把上次做的结果返回。如果没有查到,那么我们就记录下来。
上面这个流程实现效率很低,出现延时响应的请求应该是少部分。让所有请求都到这个存储里去查一下,这会导致处理流程变得很慢。想上面的案例,使用覆盖插入就可以。
我们的服务是分布式,幂等性服务也是分布式,所以,需要的这个存储也是共享的。这样每个服务就变成没有状态的。这个存储就成了一个非常关键的依赖,其扩展性和可用性也成了非常关键的指标。我们可以使用关系型数据库,或是 key-value 的 NoSQL(如 MongoDB)来构建这个存储系统。
4. 注意事项
- 当接口现实了幂等性,出现超时需要重试的时候,需要限制重试的间隔和次数,确保系统不会受到局部故障的影响,导致整体雪崩;重试间隙可以按倍数递增,例如这次间隙是上次间隙时间的2倍。
参考:
- 《极客时间——左耳听风》
- 《极客时间——深入浅出分布式技术原理》
|