接口幂等性


目录

  • 接口幂等性
  • 接口幂等性定义
  • 定义
  • 接口分类
  • 接口幂等性的使用场景
  • 业务场景
  • 数据库场景
  • 解决方案
  • 非并发场景
  • 高并发/分布式场景
  • 加锁(性能不佳,不推荐)
  • 建立防重表(本质上是基于MySQL的分布式锁,可用锁机制替代)
  • token 机制(推荐)
  • CAS 保证接口幂等性(只针对update场景)
  • 使用必要性评估



接口幂等性定义

定义

多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。

接口分类

  1. web接口
  2. 服务间调用api接口

接口幂等性的使用场景

业务场景

1、前端重复提交
就好比有个新增商品的功能,如果前端连续多次点击保存,后端就会收到多次请求接口
2、接口超时重试
当我们调取第三方接口的时候,有可能会因为网络等原因导致调用失败,所以我们会对接口调用添加失败重试的机制,Spring可以通过@Retryable注解实现重试机制。
3、MQ消息重复消费
MQ在生产端和消费端都有重试机制,也就是同一消息很可能会被重复消费,业务无法保证多次消费的结果一致。

数据库场景

当一个方法内存在非幂等的数据库操作,那这个方法一定不幂等。
select 天然自带幂等性。
每次查询对数据都不会产生副作用。

insert 看是否自增
第一种情况:自增主键,没有幂等性。
eg:insert into product_info (id,name,type,price,tm)
执行多次,会新增多条记录。对结果集产生了副作用。

第二种情况:业务主键,具有幂等。
eg:insert into product_info (orderId,name,type,price,tm) orderId 为主键唯一
无论该sql执行多少次,对结果集产生的效果都是一样只增加了一条数据。

delete 看是否主键删除
第一种情况:绝对删除,具有幂等性。
eg;delete from order where id = 3 。
无论该sql执行多少次,对结果集产生的效果都是一样只删除了一条数据。

第二种情况: 相对删除,不具有幂等性。
eg:delete from order where id > 23 .
该操作每执行一次,对结果集产生的结果,可能都不一样,同一操作多次执行对数据产生了副作用。

update 看是否主键基于原数据变化
第一种情况:绝对更新,具有幂等性。
eg:update good set stock= 586 where goodId = 10;
该操作无论执行多少次操作对结果的影响都是一样。

第二种情况:相对更新,不具有幂等性。
eg:update good set stock = stock+10 where goodid= 10 ;
每次执行该操作库存数量都会加10,所以不具备幂等操作。

解决方案

非并发场景

  1. 前端:前端做一些交互控制
    比如有个新增商品的功能,有个保存按钮,用户点击保存按钮后,立马按钮置灰,或者页面跳转到商品列表页面,这样可以防止很大部分的前端重复提交
  2. 前端:RPG模式
    Post-Redirect-Get,当客户提交表单后,去执行一个客户端的重定向,转到提交成功页面。避免用户按F5刷新致使的重复提交,也能消除按浏览器后退键致使的重复提交问题。
  3. 后端:插入前先判断数据是否存在
public void save(Goods goods) {
  // 1、先通过商品唯一code,查询数据库属否存在   
  Goods goods = findGoods(goods.getCode);
  // 2、如果这条数据在db里已经存在了,此时就直接返回了   
  if (goods != null) {
    return;
  }
  // 3、如果要是这条数据在db里不存在,此时就会执行数据插入逻辑了   
  insertGoods(goods);
}

高并发/分布式场景

加锁(性能不佳,不推荐)
  1. (分布式)悲观锁:锁读又锁写,所有请求依次处理,不会出现。本质是将并发场景转化为非并发场景处理,请求顺序执行,需结合肺病发场景的下的后端验值方案使用
    单机:使用synchronized关键字锁程序,也可以用MYSQL行锁for update 字段锁数据库
    分布式:使用redisson中Fair Lock可实现相似效果
  2. (分布式)乐观锁:锁写不锁读,所有写入操作校验版本号
    单机及分布式的场景方案相同,都是对比数据库中version版本号
建立防重表(本质上是基于MySQL的分布式锁,可用锁机制替代)

以博客点赞为例,要想防止一个人重复点赞,可以设计一张去重表,将博客 id 与用户 id 绑定建立唯一索引,每当用户点赞时就往表中写入一条数据,这样重复点赞的数据就无法写入了。

token 机制(推荐)

token作为一次性门票,用完就销毁

  1. 客户端会先发送一个请求去获取 token,服务端会生成一个全局唯一的 ID 作为 token 保存在 redis 中,同时把这个 ID 返回给客户端
  2. 客户端第二次调用业务请求的时候必须携带这个 token,服务端会校验这个 token,如果校验成功,则执行业务,并删除 redis 中的 token

如果业务请求校验失败,说明 redis 中已经没有对应的 token,则表示重复操作,直接返回指定的结果给客户端

注意:

对 redis 中是否存在 token 以及删除的代码逻辑使用 Lua 脚本实现,保证原子性

Android 防止重复调用两次同样的接口 接口重复调用 后端_java

CAS 保证接口幂等性(只针对update场景)

针对更新操作,将原有状态的查询加入sql中
例如:电商订单,订单支付状态,0 待支付,1 支付中 , 3 支付成功,4 支付失败。
update order set status = 1 where status =0 and orderId = “201251487987”
返回影响说为1 代表修改成功,可以支付,继续执行支付业务代码
返回影响数 0 代表修改失败,该订单已经不是待支付订单了

使用必要性评估

幂等性是为了简化客户端逻辑处理,能防止重复提交,但却增加了服务端的逻辑复杂性和成本:

  1. 把并行执行的功能改为串行执行,降低了执行效率;
  2. 增加了额外控制幂等的业务逻辑,业务功能变得更加复杂;

所以在使用时,需要考虑引入幂等性的必要性,一般在满足以下情况需要考虑幂等性问题解决。

  1. 接口主要负责数据写操作,且其选择的sql天然不满足数据库场景下的幂等性。
  2. 接口暴露在公网上,有被攻击的风险。
  3. 接口会承接大量的重试。