接口幂等性解决方案

Posted wen-pan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了接口幂等性解决方案相关的知识,希望对你有一定的参考价值。

一、分布式锁解决方案

先说这种方案,在网上有一些文章说可以通过分布式锁来保证幂等性。但是我认为这种方案保证幂等性的缺陷比较多,基本不可取。看下面分析

①、方案介绍

  1. 用户通过浏览器发起请求,服务端会收集数据,并且生成订单号code作为唯一业务字段。
  2. 使用redis的set命令,将该订单code设置到redis中,同时设置超时时间。
  3. 判断是否设置成功,如果设置成功,说明是第一次请求,则进行数据操作。
  4. 如果设置失败,说明是重复请求,则直接返回成功。

②、问题分析

场景一、客户端连续发起两次请求(比如用户快速点击按钮的情况),第一次请求先到达服务端,然后第二次请求由于某些原因过了一会儿才到达服务端。等第二次请求达到服务端的时候,第一次请求已经执行完毕并且释放了锁。此时第二次请求仍然能加锁成功,并且执行业务逻辑。这种情况下幂等性失效。

场景二、客户端发起第一次请求,服务端正常执行完毕并释放了分布式锁,但由于网络原因客户端没有正常收到服务端的响应,此时客户端再次发起请求。由于第一次请求所加的分布式锁已经过期所以第二次请求仍然能够加锁成功,让后执行业务逻辑。此时幂等性失效。

场景三、客户端连续发起多次请求,这多次请求同时到达服务端,此时开始争抢锁,谁抢到锁谁就执行,其他没有抢到锁的请求都统统不执行。这种情况能保证幂等性。

所以我个人感觉这种方案的局限性太大,实用性不怎么强。

二、数据库唯一key解决方案

①、适用操作

  • 插入操作

②、方案介绍

  • 在表设计的时候我们可以规定一些从业务上唯一的字段(比如:身份证号、分布式主键ID),为这些字段建立一个唯一索引。

  • 在接口做插入操作的时候,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会报Duplicate entry 'xxx' for key 'xxxxxxx异常,表示唯一索引有冲突。

  • 虽说抛异常对数据来说没有影响,不会造成错误数据。但是为了保证接口幂等性,我们需要对该异常进行捕获,然后返回成功。

    如果是java程序需要捕获:DuplicateKeyException异常,如果使用了spring框架还需要捕获:mysqlIntegrityConstraintViolationException异常。

具体步骤

  1. 用户通过浏览器发起请求,服务端收集数据。
  2. 将该数据插入mysql
  3. 判断是否执行成功,如果成功,则操作其他数据(可能还有其他的业务逻辑)。
  4. 如果执行失败,捕获唯一索引冲突异常,直接返回成功。

③、优缺点分析

  • 使用起来比较简单,只需要确定好哪个是唯一key,然后建立唯一索引即可
  • 编码上比较麻烦,因为每个需要保证幂等的插入类型的接口都需要去做捕获DuplicateKeyException异常的操作,代码上比较冗余。
  • 适用面不广,只能适用于插入操作
  • 效率不高,基于数据库的唯一key去做防重和保证插入幂等,那么相当于把压力放到了数据库上。在高并发的情况下很可能出现性能问题。

三、乐观锁解决方案

①、适用操作

  • 更新操作

②、方案介绍

数据库乐观锁方案一般只能适用于执行更新操作的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值。

  • 每次查询的时候查询出版本号,每次执行更新的时候就带上这个版本号
  • 服务端接口收到请求后按照版本号去更新数据,每次更新后将版本号 + 1
  • 如果重复发起请求,那么每次请求的version一定是一样的,但是只要有一次更新成功了那么数据库的版本号就+1,可以保证后面的请求更新数据库不会成功。

③、优缺点

实现简单,只需要在表中增加一个字段即可。

适用面不广,只能适用于更新相关的场景。

效率不高,适用数据库来保证幂等性,这样就是把压力放到数据库去了,本来数据库就是很多项目的性能瓶颈。

四、token解决方案1

①、适用操作

  • 更新操作
  • 新增操作
  • 这种方案由于不依赖于接口内部代码进行判断,所以可以通过拦截器或AOP切面 + 注解的方式做的更加通用,仅用一个注解就能让某个接口保证幂等性。

②、方案介绍

  1. 服务端需要提供一个token获取接口(该 Token 可以是一个序列号,也可以是一个分布式 ID 或者 UUID 串,反正要保证唯一性),客户端调用接口获取 Token,这时候服务端会生成一个 Token 串。
  2. 然后将该串存入 Redis 数据库中,以该 Token 作为 Redis 的键(注意设置过期时间)。
  3. 将 Token 返回到客户端,客户端拿到后应存到表单隐藏域中。
  4. 客户端在执行提交表单时,把 Token 存入到 Headers 中,执行业务请求带上该 Headers
  5. 服务端接收到请求后从 Headers 中拿到 Token,然后根据 Token 到 Redis 中查找该 key 是否存在。
  6. 服务端根据 Redis 中是否存该 key 进行判断,如果存在就将该 key 删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。

③、问题分析

  • 这种方案不需要在业务代码里做幂等校验,通过AOP切面 + 注解可以做的非常通用,使用起来很方便。但需要前端多发一次请求去请求token
  • 如果客户端连续发起调用,只要每次使用的token是一样的,那么这些连续的请求只会被处理一次。由此可以正常保证幂等性。
  • 如果某个客户端第一次发起请求,然后服务端收到后将token从Redis中删除,接着去执行业务逻辑,但是业务逻辑执行失败了,此时有两种可能:
    • 此时服务端可能会向客户端返回执行失败,客户端收到该返回后自动重新请求一个token,然后再次发起请求重试。这样也没有任何问题。
    • 如果此时服务端向客户端返回执行失败的过程中,由于网络或其他什么原因导致客户端无法接收到服务端返回的执行失败响应。那么此时客户端会再次使用第一次申请的token再次向服务端发送请求,但是此时服务端返回的确却是重复请求执行成功
  • 总的来说这种方案实用性较强,没有明显的缺陷。

一般来说没有任何一种幂等方案可以适用于所有场景,我们需要按照我们的实际情况来选择合适的方案即可。我们也可以采用多种方案组合使用来保证幂等性(我们可以使用token方案 + 数据库唯一key方案组合使用)。

五、其他问题

1、如果是上下游接口调用如何保证幂等性呢?

比如:在上游系统调用下游系统的接口向下游接口传输数据,上游系统一般都会采用重试机制,重试调用下游接口。那么下游系统如何保证幂等性?

a)、对于新增操作

  • 我们可以考虑使用数据库的唯一key方案来实现

b)、对于修改操作

  • 对于update tbl set age = 20这种类型的场景我们不需要考虑幂等。
  • 对于update tbl set age = age + 1类型的场景我们需要考虑幂等。
方案一

乐观锁方案,通过数据的版本号来解决。即使是上游系统重复发起调用,那么由于数据版本不一样了,也会导致更新失败。不会有幂等性问题。比如:update tbl set age = age + 1,version = version + 1 where version = 3

方案二
  • 对于这种情况我们可以让上游系统每次调用接口时都传输一个唯一的key。
  • 我们下游系统接收到数据以后,先不要直接操作数据库,而是先拿着这个key,通过setNX命令设置到Redis并设置过期时间
    • 如果设置成功表示第一次请求,可以正常执行后面的业务逻辑。
    • 如果设置失败,那么证明这是重复的请求,返回给上游系统重复调用的提示。
{
    "requestId":"3824abcxxddftb9010",
    "data":[
        {
            "name":"zhangsan",
            "age":21
        },
        {
            "name":"zhangsan",
            "age":21
        }
    ]
}

💅 注意:方案二中Redis中的key的过期时间的设置需要仔细考量!!!

以上是关于接口幂等性解决方案的主要内容,如果未能解决你的问题,请参考以下文章

如何保证接口的幂等性?常见的实现方案有哪些?

如何保证接口的幂等性?常见的实现方案有哪些?

如何保证接口的幂等性?常见的实现方案有哪些?

如何保证接口的幂等性?常见的实现方案有哪些?

处理接口幂等性的两种常见方案

处理接口幂等性的两种常见方案