接口的幂等性判断及其主流解决方案

幂等性相关问题探究及其解决方案

相关概念

防重复设计(Idempotent Design)的目的是保证一次请求的执行只对系统产生一次影响,即使在网络错误或者其他异常情况下多次请求产生了多次重复请求,对系统的影响也仅仅是一次。

幂等设计(Idempotent Design)的目的是保证多次请求对系统产生的影响完全相同,无论请求的次数和顺序如何。这在很多场景中都非常重要,如资金转账、库存管理等业务场景。

项目中遇到的问题
  1. 网络延迟、故障等原因,导致接口超时;而由于重试机制,可能会导致同一请求被重复发送,从而对系统产生多次影响。
  2. 用户的连续提交表单导致发送多个相同的请求,此时表中可能会出现两个除了id以外完全相同的数据。
  3. 恶意攻击。
insert相关解决方案
1.先select再insert

此方案是最简单最常用的方案

  • 在请求发送前,先通过select语句查询数据库中是否已经存在相同的数据,判断该请求是否已经执行过。
  • 如果数据库中没有对应的数据,则通过insert语句将请求内容插入数据库。
  • 如果数据库中已经存在对应的数据,说明该请求已经被执行过,直接返回成功或执行update。

问题:

  1. 数据库性能问题:先查询再插入的方式可能对数据库性能产生较大影响,特别是在并发量较大的情况下。
  2. 数据不一致问题:在高并发情况下,如果查询和插入操作之间存在时间差,可能导致数据不一致问题。
  3. 并发控制问题:如果不加以限制,并发的请求可能同时执行select和insert操作,造成接口幂等性缺失。

使用先select再insert解决接口幂等性需要加入额外的处理,以保证方法的正确性和高效性。

2.加唯一索引

此方案通过给相关字段增加唯一索引以达到防止重复数据的产生。

  • 在数据库中,为相关的字段添加唯一索引(电话、邮箱等)。
  • 在请求发送前,通过insert语句尝试将请求内容插入数据库。
  • 如果数据库插入成功,说明该请求是第一次执行,正常返回。
  • 如果数据库插入失败,说明该请求已经被执行过,抛出唯一索引冲突的异常。
  • 在java程序中捕获异常DuplicateKeyException,直接返回成功

问题:

  1. 请求过于频繁:当请求频率高时,唯一索引冲突的异常会频繁出现,影响服务性能。
  2. 可能缺乏灵活性:如果需要对相同的请求进行不同的处理,则需要对每个请求进行单独的逻辑判断,代码编写复杂。
  3. 不支持并发执行:当多个请求并发执行时,仍然可能出现唯一索引冲突的异常,无法保证幂等性。

因此,如果对性能、灵活性以及并发执行有更高要求,建议使用其他方法来解决接口幂等性。

3.建去/防重表

将业务中有唯一标识的字段保存到去重表,如果表中存在,则表示已经处理过了。并且可以将多个唯一字段组合,比如:姓名与电话号 - li_123123123,以此作为唯一标识保存到字段中。

  • 建立一个新的数据表,用来存储每一次请求的唯一标识
  • 开启事务
  • 先向防重表中插入该请求的唯一标识
  • 成功则继续处理,执行其他相关操作
  • 失败则在java程序中捕获异常DuplicateKeyException,直接返回成功(返回请求已经处理的结果)。

问题:

  1. 数据冗余:建立防重表会导致数据冗余,增加数据存储空间的消耗。
  2. 增加数据库的压力:建立防重表会增加数据库的读写压力,影响数据库的性能。
  3. 增加代码复杂度:需要改变原有代码,并实现对防重表的操作,增加了代码的复杂度。
  4. 容易出错:如果不注意操作顺序和事务的隔离性,很容易出现漏插入或重复插入的情况。

因此,使用建防重表解决接口幂等性的过程需要结合业务场景,综合考虑各种因素,再进行适当的调整和优化。

update相关解决方案
1.状态控制

根据订单状态(初始化、未支付、已支付)的前置后续保证接口的幂等性。

update xx_order set status = 2 where id = 1 and status = 1;
  • 根据相关业务处理得到要修改的状态
  • 获取订单当前状态(若公司规定了对应的状态流转顺序,不需要此操作)
  • 执行update操作,将上一步获取的订单状态当做条件传入
  • 返回影响行数,若>0,则执行成功,进行其他业务处理
  • 若=0,则直接返回成功(返回请求已经处理的结果)
2.乐观锁

在表中增加一个version版本字段保证接口的幂等性,每次操作数据时将版本号+1。与状态控制的方式类似,但是状态控制方式无法解决ABA问题(A改B后B再改A)。

select id,version from xx_order; -- 5
update xx_order set amount = 10,version = version+1 
where id = 1 and version = 5;
  • 根据相关业务处理得到要修改的字段及数据
  • 获取订单当前版本号
  • 执行update操作,将上一步获取的版本号当做条件传入
  • 返回影响行数,若>0,则执行成功,进行其他业务处理
  • 若=0,则直接返回成功(返回请求已经处理的结果)

问题:

  1. 并发冲突:如果多个请求同时对同一个资源进行修改,容易造成并发冲突,乐观锁需要对冲突的情况进行处理。
  2. 性能问题:频繁的乐观锁操作会对性能产生影响,需要考虑如何降低乐观锁的影响。

如果数据更新的频率很高,并且冲突的概率很大,可能会导致资源浪费,影响系统性能。

3.悲观锁

对于账户余额这种数据需要保证数据的准确性和实效性,通常使用锁住某行(可能是某间隔)的方式来保证同一时刻只有一个请求得到锁。

select aa,bb,cc from xx_money where id = 1 for update;
  • 普通查询,判断用户余额是否充足,不足直接返回
  • 余额充足则使用for update查询获取行锁;获取失败则等待下一次获取锁的机会
  • 再次判断余额是否充足,充足则进行相关操作
  • 余额不足说明是重复的请求,直接返回成功(返回请求已经处理的结果)
通用办法
1.redis分布式锁

在分布式系统中解决接口幂等性的一种常见方法是加分布式锁。实现redis分布式锁的方式一般有:setsetNxRedisssion

  • 首先客户端请求接口并发送所需的参数。
  • 客户端在redis中请求一个锁,锁的ID与该接口的唯一标识相关,并设置超时时间。
  • 如果该锁可用,则客户端可以获取锁,并执行该接口的业务逻辑。
  • 若设置失败,说明是重复请求,直接返回成功(返回请求已经处理的结果)

这种方法的优点是比较简单易懂,可以有效避免接口幂等性问题。但存在的问题是,需要维护一个分布式锁系统,并保证该系统的高可用性和性能。此外,如果在获取锁的过程中出现异常,还可能导致锁的死锁和超时等问题。

2.token

与其他方案有些区别,此方案需要两次请求:先获取token,再带着token执行相关业务逻辑。

  • 服务端生成一个全局唯一的token,存入redis中并返回。
  • 客户端在请求接口时,将这个token作为参数发送到服务端。
  • 服务端接收到请求后,首先验证该token是否已经被使用过。
  • 如果该token未被使用过,服务端执行该请求并且将该token标记为已使用(或删除)。
  • 如果该token已被使用过,则视为该请求重复,直接返回成功。

这种方式的优点是简单易用,不需要额外的数据库支持,但是在服务端的token存储机制不当的情况下会导致token被重复使用或者token丢失等问题。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值