什么是接口的幂等性,如何保证接口的幂等性?

在这里插入图片描述
✅作者简介:大家好,我是Leo哥,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉
🍎个人主页:Leo哥的博客
💞当前专栏: Java
✨特色专栏: MySQL学习
🥭本文内容:什么是接口的幂等性,如何保证接口的幂等性?
📚个人知识库: 知识库,欢迎大家访问

1.前言☕

大家好,我是Leo哥🫣🫣🫣,有半个月没更新了,最近都忙着工作跟其他事情,博客水都没时间水了。这不年前几天,没啥任务了,昨天看了下关于幂等性的一些问题,今天早上抽空写了篇博客,分享给大家。好了,话不多说让我们开始吧😎😎😎。

2.什么是幂等性

幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数或幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。

3.什么是接口幂等性

所谓接口幂等性,就是一次和多次请求某一个资源对于资源本身应该具有同样的结果。

接口的 幂等性(Idempotence) 是指一个操作在执行一次和多次执行时,其结果是一样的。换句话说,无论这个操作被执行多少次,它对系统状态的影响都是相同的。幂等性是分布式系统中的一个重要概念,它有助于确保系统的健壮性和一致性。

在网络通信和 API 设计中,幂等性尤为重要,因为它可以防止由于网络重传、客户端或服务器端的重复请求等问题导致的数据不一致。例如,一个幂等的 HTTP 请求(如 GET 或 PUT 请求)在多次发送时,服务器应该能够正确处理,确保不会对资源造成意外的影响。

4.应用场景

不知道以下场景,朋友们是否遇到过:

  • 前端重复提交表单: 在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。
  • 用户恶意进行刷单: 例如在实现用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票结果与事实严重不符。
  • 接口超时重复提交: 很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
  • 消息进行重复消费: 当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。

没错,这些都是幂等性问题。

接口幂等性是指用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

这类问题多发于接口的:

  • insert操作,这种情况下多次请求,可能会产生重复数据。
  • update操作,如果只是单纯的更新数据,比如:update user set status=1 where id=1,是没有问题的。如果还有计算,比如:update user set status=status+1 where id=1,这种情况下多次请求,可能会导致数据错误。

5.解决方案

5.1 inset之前先select

通常情况下,在保存数据的接口中,我们为了防止产生重复数据,一般会在insert前,先根据name字段select一下数据。如果该数据已存在,则执行update操作,如果不存在,才执行 insert操作。

image-20240202095611459

5.2 数据库唯一索引

大多数情况下,我们为了防止数据重复提交,我们都会在表中添加唯一索引,这个一个非常简单而且有奇效的方案。

alter table `order` add UNIQUE KEY `t_code` (`code`);

加了唯一索引之后,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会报Duplicate entry '002' for key 'order.t_code异常,表示唯一索引有冲突。

具体流程图如下:

image-20240202101415975

5.3 Token机制

针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用 Token 的机制实现防止重复提交。简单的说就是调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求**(Token 最好将其放到 Headers 中)**,后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且 Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的 Key 或 Value 不匹配就返回重复执行的错误信息,这样来保证幂等操作。

该方案跟之前的所有方案都有点不一样,需要两次请求才能完成一次业务操作。

  1. 第一次请求获取token
  2. 第二次请求带着这个token,完成业务操作。

具体流程图如下:

image-20240202095841563

5.4 悲观锁机制

比如某银行有个转账场景,用户A手里有200块钱,想转出100元,正常情况下,用户A转账之后只剩下了100元,SQL如下:

update user amount = amount-100 where id=888;

但是实际情况下,并非如此,如果有个相同的请求进来,用户A的账户就会一直扣减,直到变成负数。这种情况,用户A直接哭死,在业务场景中也是不允许出现的。

通常情况下通过如下sql锁住单行数据:

select * from user id=888 for update;

具体步骤:

  1. 多个请求同时根据id查询用户信息。
  2. 判断余额是否不足100,如果余额不足,则直接返回余额不足。
  3. 如果余额充足,则通过for update再次查询用户信息,并且尝试获取锁。
  4. 只有第一个请求能获取到行锁,其余没有获取锁的请求,则等待下一次获取锁的机会。
  5. 第一个请求获取到锁之后,判断余额是否不足100,如果余额足够,则进行update操作。
  6. 如果余额不足,说明是重复请求,则直接返回成功。

悲观锁需要在同一个事务操作过程中锁住一行数据,如果事务耗时比较长,会造成大量的请求等待,影响接口性能。

此外,每次请求接口很难保证都有相同的返回值,所以不适合幂等性设计场景,但是在防重场景中是可以的使用的。

5.5 乐观锁机制

因为悲观锁是比较消耗的性能的操作,那么我们为了提高接口性能,完全可以使用乐观锁。需要再表中添加一个version字段。

比如:

alter table user add version int2);

每当我们更新数据的时候,需要对我们的版本号+1

update user set amount = amount+100,version=version+1 where id = 888 and version =1  

更新数据的同时version+1,然后判断本次update操作的影响行数,如果大于0,则说明本次更新成功,如果等于0,则说明本次更新没有让数据变更。

等到下一个请求过来的时候,依然回去执行这行SQL,此时发现,根本不可能满足,version= 1 这个条件,因为version值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。

流程图如下:

image-20240202103825041

具体步骤:

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

5.6 建防重表

有时候我们的表中需要一些重复数据,只有一些特殊场景才不需要重复数据,此时我们上面的唯一索引方案可能就不太行了。

这时候,直接在表中加唯一索引,显然是不太合适的。

针对这种情况,我们可以通过建防重表来解决问题。

简单来说就是我们再单独建立一张表,只需要含有id和唯一索引,当然唯一索引可以是多个字段比如:name、code等组合起来的唯一标识

流程图如下:
image-20240202104525417

具体步骤:

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

6.代码实践

以上我们都是给出了一些大概的解决方案跟思路,接下来Leo哥大家以Token机制为例,用代码实现如果解决接口的幂等性。

首先我们来回顾一下Token机制的整个流程。

image-20240202095841563

下面开始直接上代码。

首先准备一个springboot工程项目,只需要添加两个依赖即可。

image-20240202104822062

然后开始编写Redis工具类跟一个简单的Token工具类。

RedisService

package org.javatop.idempotent.token;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;/*** @author : Leo* @version 1.0* @date 2024-02-01 21:03* @description :*/
@Component
public class RedisService {@Autowiredprivate RedisTemplate redisTemplate;public boolean setEx(String key, Object value, Long expireTime) {boolean result = false;try {ValueOperations ops = redisTemplate.opsForValue();ops.set(key, value);redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);result = true;} catch (Exception e) {e.printStackTrace();}return result;}/*** 判断key是否存在* @param key key* @return*/public boolean exists(String key) {return Boolean.TRUE.equals(redisTemplate.hasKey(key));}/*** 删除key* @param key key* @return*/public boolean remove(String key) {if (exists(key)) {return Boolean.TRUE.equals(redisTemplate.delete(key));}return false;}
}

TokenService

主要是生成一个全局唯一不重复的Token,以及前端请求过来被拦截后需要检验token的方法。

package org.javatop.idempotent.token;import io.micrometer.common.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.javatop.idempotent.exception.IdempotentException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.util.UUID;/*** @author : Leo* @version 1.0* @date 2024-02-01 21:01* @description :*/
@Component
public class TokenService {@AutowiredRedisService redisService;public String createToken() {String uuid = UUID.randomUUID().toString();redisService.setEx(uuid, uuid, 10000L);return uuid;}public boolean checkToken(HttpServletRequest request) throws IdempotentException {String token = request.getHeader("token");if (StringUtils.isEmpty(token)) {token = request.getParameter("token");if (StringUtils.isEmpty(token)) {throw new IdempotentException("token 不存在");}}if (!redisService.exists(token)) {throw new IdempotentException("重复的操作");}boolean remove = redisService.remove(token);if (!remove) {throw new IdempotentException("重复的操作");}return true;}
}

自定义幂等注解

我们自定义一个幂等注解,来对我们想要幂等性一致的接口进行标识。

/*** @author : Leo* @version 1.0* @date 2024-02-01 21:17* @description : 幂等注解*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}

添加拦截器

在拦截器中,我们解析出所有的请求,标注有幂等注解的请求,我们去检验他的token,然后来决定下一步操作。

package org.javatop.idempotent.interceptor;import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.javatop.idempotent.annotation.AutoIdempotent;
import org.javatop.idempotent.exception.IdempotentException;
import org.javatop.idempotent.token.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;/*** @author : Leo* @version 1.0* @date 2024-02-01 21:14* @description :*/
@Component
public class IdempotentInterceptor implements HandlerInterceptor {@AutowiredTokenService tokenService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (!(handler instanceof HandlerMethod)) {return true;}HandlerMethod handlerMethod = (HandlerMethod) handler;AutoIdempotent idempotent = handlerMethod.getMethod().getAnnotation(AutoIdempotent.class);if (idempotent != null) {try {return tokenService.checkToken(request);} catch (IdempotentException e) {throw e;}}return true;}
}

测试

最后编写接口进行测试。

首先生成一个Token,然后把这个token放到hello接口的请求头上面。

image-20240202110510677

可以看到,第一次可以正常访问接口

image-20240202110612723

但当你第二次访问该接口的时候,已经提示你操作重复了。因为在我们第一次访问接口之后,就把Redis中的token删除了。

image-20240202110656581

7.总结

以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是Leo,一个在互联网行业的小白,立志成为更好的自己。

如果你想了解更多关于Leo,可以关注公众号-程序员Leo,后面文章会首先同步至公众号。

公众号封面

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/249989.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

贪吃蛇/链表实现(C/C++)

本篇使用C语言实现贪吃蛇小游戏,我们将其分为了三个大部分,第一个部分游戏开始GameStart,游戏运行GameRun,以及游戏结束GameRun。对于整体游戏主要思想是基于链表实现,但若仅仅只有C语言的知识还不够,我们还…

拓扑、监控、展示、流量、资产一体化管理,重庆石柱中医院部署智和信通统一运维平台

县中医院创建于1983年,是集医疗、教学、科研、急救、康复为一体的国家“二级甲等”综合性中医院,其智慧医院建设总体目标是以患者为中心,电子病历为核心,基于医院信息平台,实现全院资源的统一调度与管理,为…

基于SpringBoot+Redis的前后端分离外卖项目-苍穹外卖(十六)

商家端订单管理模块 1. 订单搜索1.1 需求分析和设计1.2 代码实现1.2.1 admin/OrderController1.2.2 OrderService1.2.3 OrderServiceImpl 2. 各个状态的订单数量统计2.1 需求分析和设计2.2 代码实现2.2.1 admin/OrderController2.2.2 OrderService2.2.3 OrderServiceImpl2.2.4 …

基于SSM+MySQL的的新闻发布系统设计与实现

目录 项目简介 项目技术栈 项目运行环境 项目截图 代码截取 源码获取 项目简介 新闻发布系统是一款基于Servletjspjdbc的网站应用程序,旨在提供一个全面且高效的新闻发布平台。该系统主要包括后台管理和前台新闻展示两个平台,涵盖了新闻稿件的撰写…

React + react-device-detect 实现设备特定的渲染

当构建响应式网页应用时,了解用户正在使用的设备类型(如手机、平板或桌面)可以帮助我们提供更优化的用户体验。本文将介绍如何在 React 项目中使用 react-device-detect 库来检测设备类型,并根据不同的设备显示不同的组件或样式。…

使用Docker部署DashDot服务器仪表盘并结合cpolar实现公网监测服务器

最近,我发现了一个超级强大的人工智能学习网站。它以通俗易懂的方式呈现复杂的概念,而且内容风趣幽默。我觉得它对大家可能会有所帮助,所以我在此分享。点击这里跳转到网站。 文章目录 1. 本地环境检查1.1 安装docker1.2 下载Dashdot镜像 2.…

2024美赛数学建模A题思路源码——七鳃鳗性别比例和生态系统关系

赛题目的:分析一个物种根据资源可用性改变其性别比例的能力的利弊。开发一个模型,分析对生态系统中由此产生的相互作用。 问题一.七鳃鳗性别比例对生态系统的影响 问题分析 建立一个简化版的模型,来探讨以下问题: 1.我们假设七鳃鳗种群的增长遵循Logistic生长模型,其中食…

抖音短视频矩阵营销系统源头独立开发搭建

开发背景 抖音短视频矩阵系统源码开发采用模块化设计,包括账号分析、营销活动、数据监控、自动化管理等功能。通过综合分析账号数据,快速发现账号的优势和不足,并提供全面的营销方案,以提高账号曝光率和粉丝数量。同时&#xff0c…

【C++】C++11新特性(lambda表达式)

lambda表达式 引入 在C98中&#xff0c;如果想要对一个数据集合中的元素进行排序&#xff0c;可以使用std::sort方法 #include <algorithm> #include <functional>int main() {int array[] {4,1,8,5,3,7,0,9,2,6};// 默认按照小于比较&#xff0c;排出来结果是升…

自然语言nlp学习 三

4-8 Prompt-Learning--应用_哔哩哔哩_bilibili Prompt Learning&#xff08;提示学习&#xff09;是近年来在自然语言处理领域中&#xff0c;特别是在预训练-微调范式下的一个热门研究方向。它主要与大规模预训练模型如GPT系列、BERT等的应用密切相关。 在传统的微调过程中&a…

LeetCode707.设计链表

707. 设计链表 你可以选择使用单链表或者双链表&#xff0c;设计并实现自己的链表。 单链表中的节点应该具备两个属性&#xff1a;val 和 next 。val 是当前节点的值&#xff0c;next 是指向下一个节点的指针/引用。 如果是双向链表&#xff0c;则还需要属性 prev 以指示链表…

如何在社交平台做好影视宣传?媒介盒子支招

影视宣传和产品营销其实都是一样的&#xff0c;都需要通过找出用户痛点&#xff0c;表现产品/影视的主要两点&#xff0c;提高利润。然而影视宣传还有一个独特的宣发手段&#xff0c;以影视本身的内容制造一种流行趋势&#xff0c;吸引受众&#xff0c;今天媒介盒子就来和大家聊…

Linux:理解信号量以及内核中的三种通信方式

文章目录 共享内存的通信速度消息队列msggetmsgsndmsgrcvmsgctl 信号量semgetsemctl 内核看待ipc资源单独设计的模块ipc资源的维护 理解信号量总结 本篇主要是基于共享内存&#xff0c;延伸出对于消息队列和信号量&#xff0c;再从内核的角度去看这三个模块实现进程间通信 共享…

游戏开发丨基于Panda3D的迷宫小球游戏

文章目录 写在前面Panda3D程序设计程序分析运行结果系列文章写在后面 写在前面 本期内容 基于panda3d的迷宫中的小球游戏 所需环境 pythonpycharm或anacondapanda3d 下载地址 https://download.csdn.net/download/m0_68111267/88792121 Panda3D Panda3D是一种开放源代码…

Transformer|Encoder(未完成)

文章目录 Transformer&#xff5c;Encoder编码器注意力机制残差残差的作用 Transformer&#xff5c;Encoder编码器 注意力机制 比如说已经得到腰围和体重的两份数据&#xff0c;此时给出一个查询——腰围为57的人的体重为多少&#xff1f;可以根据以往的腰围数据对体重进行估计…

Kubernetes WebHook 入门 -- 入门案例: apiserver 接入 github

博客原文 文章目录 k8s 集群配置介绍Admission WebhookWebHook 入门实践: github 认证接入web 服务器Dockerfile 镜像制作amd64x86_64构造镜像检验镜像 Makefilewebhook 接入 apiserverwebhook.yamlapiserver 挂载 webconfig在 github 中创建认证 token将 token 添加到 kubecon…

Android.bp入门指南之浅析Android.bp文件

文章目录 Android.bp文件是什么&#xff1f;Android.bp的主要作用模块定义依赖关系构建规则模块属性插件支持模块的可配置性 为什么会引入Android.bp语法例子 Android.bp文件是什么&#xff1f; Android.bp 文件是 Android 构建系统&#xff08;Android Build System&#xff…

C/C++ C++入门

个人主页&#xff1a;仍有未知等待探索-CSDN博客 专题分栏&#xff1a;C_仍有未知等待探索的博客-CSDN博客 目录 一、C关键字 二、命名空间 1、区别 1. C语言 ​编辑 2. C 2、命名空间定义 3、命名空间的使用 三、C输入&输出 四、缺省参数 五、函数重载 六、引用 …

界面控件DevExpress ASP.NET Data Grid组件 - 可快速处理各类型数据!(二)

由DevExpress开发的快速且功能完整的ASP.NET Web Forms的Data Grid组件&#xff0c;从全面的数据塑造和数据过滤选项到十多个集成数据编辑器&#xff0c;该套件提供了帮助用户构建极佳数据所需的一些&#xff0c;没有限制&#xff01;在上文中&#xff08;点击这里回顾>>…

基于Springboot开发的JavaWeb作业查重系统[附源码]

基于Springboot开发的JavaWeb作业查重系统[附源码] &#x1f345; 作者主页 央顺技术团队 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; &#x1f345; 文末获取源码联系方式 &#x1f4dd; &#x1f345; 查看下方微信号获取联系方式 承接各种定制系统 &a…