幂等问题

516 阅读6分钟

一、什么是幂等接口?

幂等:简单来说就是一个操作执行多次和仅仅执行一次,其产生的效果是相同。例如常见的get请求,在只有get请求执行的情况下,无论发起多少次get请求,得到的结果都是相同。

二、导致幂等问题的可能原因

1 网络波动不稳定
网络通信中的丢包、延迟等情况可能导致客户端未收到服务端的响应或服务端未收到客户端的请求,此时客户端可能会重试发送请求,导致接口被重复调用。

2 用户操作
用户快速重复点击导致,例如用户在等待响应时,由于不确定是否操作成功,可能会多次点击提交按钮,进而发送多次相同的请求。再比如页用户频繁刷新页面,尤其是在某些提交操作尚未完成时,刷新页面可能会重新发送请求。还有用户可能在浏览器上点击回退然后再重复之间的提交操作,这都可能会导致重新发送请求。

3 重试机制
在高可用性设计中,客户端常常设置有重试机制,当请求失败或超时时会自动重新发起请求。而在分布式系统中,服务间调用也可能有重试策略,以应对临时故障。比如Nginx重试,RPC重试,或者调用方业务层中进行重试。

4 定时任务或异步处理
在定时任务中如果定时任务调度或逻辑设计不当,可能会导致同一任务被执行多次。或者在消息队列中,消息可能会因为异常等原因被重复消费。

5 并发控制
缺乏有效的并发控制手段,导致在并发环境下,针对同一资源的操作被多次执行。

综上,幂等问题的原因可以分为两类,一类是前端交互操作不当导致的;另一类是后端接口调用导致的。因此,要解决幂等问题,也应该从这两个分类出发。

三、如何解决幂等问题

可以只从前端限制,也可以前后端双管齐下。具体选用什么方法解决,需要根据ROI等因素自行评估。

1 从前端解决

①页面调用 对于通过按钮触发的请求,例如,表单的提交等,在用户发起请求时,将按钮置灰,禁止用户多次点击按钮而产生多次重复的请求。

②使用PRG模式 PRG(post/redirect/get)模式是一种前端交互策略,用于解决用户刷新界面时可能导致的表单数据重复提交问题[1]。它巧妙利用了HTTP协议的特性,其交互流程如下:

image.png

  1. 用户在网页表单中填写数据,并通过POST请求将其发送至服务器进行处理,例如创建新资源或更新现有数据。
  2. 服务器接收到POST请求后,对提交的数据进行有效处理和持久化存储,并在操作成功后不直接返回处理结果,而是通过HTTP响应码302或303实现重定向,指示客户端发起一个新的GET请求去访问一个特定的URL。
  3. 客户端遵照服务器的重定向指示,自动发送GET请求访问新的URL,此时返回的页面将展示之前POST操作处理完毕的结果。
  4. 当用户在此后刷新页面时,浏览器只会按照常规方式重新发起GET请求,而非重新提交POST数据,因此有效地避免了重复提交引发的潜在问题

③使用token 请求时携带上服务端颁发的token,服务器验证token的有效性,有效执行请求;无效则丢弃请求。

2 从后端解决

从后端来看,解决幂等问题的关键在于如何区分出请求是否重复。

①使用唯一标识标记请求 请求携带一个全局的唯一标识,服务通过这个唯一标识判断请求是否有效。

//上游服务
createOrder() {
    //获取全局唯一id
    String id = getSingleId();
    executeOrder(id, xxx, xxx);
}

//下游服务
executeOrder(id, xxx, xxx) {
    //校验id的有效性
    boolean valid = checkId(id);
    if(!valid) {
        //id无效丢弃请求
        return;
    }
    //执行创建订单的请求
    ...
}

②状态机 假设一个完整的订单状态需要经过以下流程: 创建-> 支付-> 审核-> 完结等流程

那么当订单状态处于支付后的节点,不允许再次执行支付操作

③乐观锁 在更新数据时,可以通过版本号或时间戳等机制判断数据是否已被修改,防止因并发请求导致的多次更新问题。具体做法:

  1. 在数据库表中增加一个版本号字段(version)或者时间戳字段(timestamp)。

  2. 客户端第一次请求时获取数据的版本号或时间戳。

  3. 客户端发起更新操作时,将上次读取的版本号或时间戳一起发送回服务器。

  4. 服务器在执行更新操作前,首先检查当前数据库中的版本号或时间戳是否与客户端提交的一致。

    • 如果一致,说明在这期间数据没有被其他事务修改过,于是更新数据并递增版本号或更新时间戳。
    • 如果不一致,说明数据已经被修改过,此时服务器拒绝本次更新请求,返回错误提示,客户端可以根据错误信息决定是否重新获取最新数据再尝试更新。

通过这种方式,即使客户端因为网络原因或其他因素导致同一请求被多次发送,乐观锁机制能确保只有在数据未被其他事务修改的前提下,才会执行更新操作,从而达到接口幂等的效果。

④请求参数 对请求参数进行一系列的hash算法生产对应的hash值,如果hash值重复,则判定为重复请求。使用该方法,需要格外注意请求的参数是否真的不会重复,如果无法确定,建议直接丢弃重复的请求。

update(Map<String, Object> params) {
    //使用MD5算法取hash值
    String hashCode = MD5(params);
    if(null != cache.get(hashCode)) {
        //参数重复,丢弃请求
        return;
    }
    //执行请求
    ...

}