0%

幂等性的解决方案

在编程开发中,对于幂等的定义为:无论对某一个资源操作了多少次,其影响都应是相同的。 换句话说就是:在接口重复调用的情况下,对系统产生的影响是一样的,但是返回值允许不同。幂等关注的是是否对资源产生影响,而不关注结果。

防止重复提交

在谈幂等性的实现方案之前,先谈一下防止重复提交。通常有两种模式:

  • 前端防止重复提交
  • POST-REDIRECT-GET 模式

前端防止重复提交

可通过防抖、节流函数实现

POST-REDIRECT-GET 模式

流程如下图所示:

Post/Redirect/Get 方式除了能防止 Post 请求的重复提交外,还可以完善网页收藏功能。把 Post 请求直接返回的网页收藏到书签是无效的,因为这个网页的重现依赖于 Post 请求以及当时提前的数据。采用 PRG 方式,用户正常情况下收藏到的是重定向后 GET 方法返回的网页,这样使得收藏有效了。

幂等实现方案

查询/删除

通常查询一次和查询多次,在数据不变的情况下,查询结果都是幂等的。

删除一次和删除多次都是把数据删除,不同的是返回结果可能不同,重复删除的结果可能提示操作失败。

唯一索引

通过使用唯一索引来防止新增数据请求幂等,例如:通过手机号注册账户,手机号使用唯一索引。

防重表

有时候表中并非所有的场景都不允许产生重复的数据,只有某些特定场景才不允许。这时候,直接在表中加唯一索引,显然是不太合适的。

针对这种情况,我们可以通过建防重表来解决问题。以用户注册为例:该表有 id 字段和一个唯一索引:手机号和请求时间戳(前端产生)。其实也是通过唯一索引来保证新增数据请求幂等。

token机制

token机制来保证幂等性是一种非常常见的解决方案,同时也适合绝大部分场景。

  1. 服务端提供获取token接口,供客户端进行使用。服务端生成token后,如果当前为分布式架构,将token存放于redis中;
  2. 当客户端发起请求前,先获取token,获取到token后会携带着token发起请求;
  3. 服务端接收到客户端请求后,首先会判断该token在redis中是否存在。如果存在,则完成进行业务处理,业务处理完成后,再删除token。如果不存在,代表当前请求是重复请求,直接向客户端返回对应标识。

但这需要注意:如果先执行业务再删除token。在高并发下,很有可能出现第一次访问时token存在,完成具体业务操作。但在还没有删除token时,客户端又携带token发起请求,此时,因为token还存在,第二次请求也会验证通过,执行具体业务操作。

有两种解决方案:

  • 分布式锁,保证线程安全
  • 先删除,删除成功再执行业务

状态机

很多时候业务表是有状态的,比如订单表中有:1-下单、2-已支付、3-完成、4-撤销等状态。如果这些状态的值是有规律的,按照业务节点正好是从小到大,我们就能通过它来保证接口的幂等性。

假如 id=123 的订单状态是已支付,现在要变成完成状态。

1
update `order` set status=3 where id=123 and status=2;

悲观锁

可以加悲观锁,将数据锁住,在同一时刻只允许一个请求获得锁,更新数据,其他的请求则等待。

1
select * from user id=123 for update;

悲观锁需要在同一个事务操作过程中锁住一行数据,如果事务耗时比较长,会造成大量的请求等待,影响接口性能。此外,每次请求接口很难保证都有相同的返回值,所以不适合幂等性设计场景,但是在防重场景中是可以的使用的。在这里顺便说一下,防重设计 和 幂等设计,其实是有区别的。防重设计主要为了避免产生重复数据,对接口返回没有太多要求。而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果。

乐观锁

悲观锁有性能问题,为了提升接口性能,可以使用乐观锁,在表中增加一个 version 字段。

具体步骤:

  1. 先根据id查询用户信息,包含version字段
  2. 根据id和version字段值作为where条件的参数,更新用户信息,同时version+1
  3. 判断操作影响行数,如果影响1行,则说明是一次请求,可以做其他数据操作。
  4. 如果影响0行,说明是重复请求,则直接返回成功。

分布式锁

悲观锁和乐观锁都是基于数据库的行锁来实现,也可以借助 Redis 或 Zookeeper 来实现分布式锁。

坚持原创技术分享,您的支持将鼓励我继续创作!