剖析:分布式高并发场景下的幂等问题

01什么是幂等

幂等(idempotent)是一个数学与计算机学概念,常见于抽象代数中。

在程序开发中,如果一个操作其任意多次执行对系统产生的影响均与一次执行的影响相同,则称其是幂等操作。幂等函数(幂等方法),是指使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。


02如何判断一个操作是否幂等

HTTP/1.1中的幂等

A request method is considered “idempotent” if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request.

datatracker.ietf.org/doc/html/rf…

在HTTP/1.1中,每种方法都有其特定的语义,该方法的特点决定了其幂等特性。例如:

  • GET /resource/page HTTP/1.1  其是幂等操作,一次和多次请求的结果是一致的;

  • DELETE /resource/delete HTTP/1.1 其是幂等操作,资源一旦被删除、后续多次相同操作对系统本身产生的影响是一样的,即使多次操作的返回值可能不同;

  • PUT /resource/put HTTP/1.1 其是幂等操作,资源不存在则新增,存在则更新,即使多次操作,对于系统来说都是将某个资源新增/更新成已知值;

  • POST /resource/add HTTP/1.1 其是非幂等操作,每次请求均会在服务端产生一条新的记录,将会对系统产生副作用。

针对HTTP/1.1中的方法语义及其规范,归纳如下:

  • 常见幂等方法(idempotent methods): GET, HEAD, PUT, DELETE, OPTIONS, TRACE

  • 常见非幂等方法(non-idempotent methods): POST,PATCH, CONNECT

幂等的一般结论

经过上面的案例,我们可以知道幂等的评判依据:当前操作的任意多次执行是否会对业务系统产生不同的影响,若产生了不同的影响,则该操作就是非幂等的,需要考虑如何保证接口的安全和幂等。如常见的支付业务中的创建订单、支付回调和退款接口等,均需要考虑其幂等性。


03常见幂等问题产生的原因

幂等问题产生的条件

  • 该操作(接口)本身存在幂等性问题;

  • 系统存在重试的潜在可能,造成该操作会被多次执行。

常见幂等问题诱因

  • 网络波动重试机制: 网络不稳定触发中间件的重试机制等;

  • 客户端主动/被动重试: 用户主动重试,或者恶意用户刷接口;

  • 定时调度任务的重试机制: 业务定时调度任务多次执行某个操作;

  • 消息重复消费: 消息队列中可能存在多个重复的消息,导致业务操作的多次执行。


04幂等的解决思路

通过对幂等问题产生原因的分析,我们知道幂等问题的解决思路就是在调用链路上去重。基于此,去重的工作可以分为客户端去重和服务端去重。

客户端方案

  • 按钮仅能点击一次: 客户端在用户点击后禁用按钮和loading遮罩等;

  • 使用Post/Redirect/Get模式: 是一种用来防止表单重复提交数据的一种Web设计模式。

客户端的拦截和去重只能处理正常用户,对于恶意请求仍需要服务端参与。

服务端方案

方案一:数据库唯一索引特性

对于简单的业务场景,比如新增插入的业务,可以利用数据库的主键索引或者唯一索引的特性,对某个业务唯一的字段添加索引。这样后续同样uniqueId的请求将插入失败。如下图:

图片

但是对于一些复杂的业务场景,其一次请求的事务比较繁杂,需要多次操作数据库的情况,就需要设计防重表了,其原理也是利用数据库唯一索引特性。如下图:

图片

步骤说明:

  1. 调用方携带uniqueId请求,Service端首先向防重表插入数据;

  2. 若插入成功,则执行业务操作;

  3. 若插入不成功,则认为是重复请求,已经处理过。

注:uniqueId,这里可以是和业务相关的唯一业务标识,如订单号等。

方案二:token机制

服务端(Service)生成token,客户端(调用方)请求时候需携带token,服务端判断token是否存在和有效。如下图:

图片

步骤说明:

  1. 客户端调用前先获取token,服务端生成一个全局唯一的 ID 作为 token,并将该token保存在缓存中;

  2. 客户端正式调用的时候携带第一步获取的token;

  3. 服务端判断缓存中是否存在该token并校验,若存在且token校验成功,则执行业务;业务执行成功后删除本次请求在缓存中对应的token;

  4. 如果token校验失败,即缓存中不存在对应的token,则表示重复操作,抛出异常。

注:第三步,即校验token、业务操作和清除缓存中的token一系列操作需保证原子性;此处可以加锁。

说明:**全局唯一 ID生成策略:**UUID;Redis原子自增命令实现;雪花算法-Snowflake;百度的 uid-generator;美团的 Leaf 。

方案三:唯一序列号 + 缓存(单机/分布式缓存)

对于一个事务的多次请求,设计一个全局唯一的序列号(uniqueSeqNo)(或业务系统中已经存在类似属性的业务ID),这个序列号将在第一次调用服务的时候,被服务端缓存;待后续重复请求(相同序列号)过来时候,进行重复性校验。如下图:

图片

步骤说明:

  1. 调用方传递该次事务的序列号uniqueSeqNo;

  2. 服务提供方(服务端)判断uniqueSeqNo是否存在,若存在,则认为是重复请求,抛出异常;

  3. 若不存在,则进行业务操作;业务操作成功后缓存uniqueSeqNo等信息;

注:第三步的判断、业务执行和缓存操作需保证原子性;可以加锁解决。

补充:uniqueSeqNo的设计可以采用source + 业务Id组合的形式。source标识调用方的来源等信息。

方案四:业务状态校验 + 锁(单机锁/分布式锁)

业务上根据业务ID的唯一性业务处理的结果去做判断,但是这部分判断的逻辑需要考虑原子性。否则会因为并发问题导致幂等失效。解决并发问题的途径之一就是加锁,根据当前的服务环境选择单机或分布式锁。

方案五:缓冲队列

将请求放入一个缓冲队列中,后续异步线程处理队列中的请求任务,并根据请求中的唯一标识进行过滤。如下图:

图片


05总结

幂等的问题是相对比较简单的问题。幂等处理的核心操作就两点:去重,以及去重的并发问题解决。根据实际的业务场景,可以选择合适的解决方案。据了解,也可以通过数据库乐观锁、状态机等方式实现,这里不再赘述。

笔者推荐基于token的解决方案、防重表设计和基于业务状态 + 锁的解决方案。

往期推荐

漫谈分布式锁

漫谈分布式锁之Redis实现

漫谈分布式锁之ZooKeeper实现

深思:缓存击穿(并发)、缓存穿透和缓存雪崩

众里寻他千百度:Bloom Filter

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享