接口幂等性测试

article/2025/10/3 15:42:23

前言

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

我们在开发中主要操作也就是CURD,其中读取操作和删除操作是天然幂等的,我们所关心的就是创建操作、更新操作。

创建操作一定是非幂等的因为要涉及到新数据的产生,而更新操作有可能幂等有可能非幂等,这个要看具体业务场景。

一、幂等性的使用场景

1、前端重复提交

就好比有个新增商品的功能,有个保存按钮,如果前端连续多次点击保存,后端就会收到多次请求接口,如果没做好幂等就会重复创建了多条记录,
就会出现脏数据。

这个也就是我们所说的如何防止前端重复提交的问题。

2、接口超时重试

当我们调取第三方接口的时候,有可能会因为网络等原因导致调用失败,所以我们会对接口调用添加失败重试的机制,Spring可以通过@Retryable注解实现重试机制。

既然重试就可能出现重复调用接口。这时再次调用时如果没有做好幂等,就可能出现脏数据。

3、消息重复消费

这个是无法避免的,因为我们说MQ在生产端和消费端都有重试机制,也就是同一消息很可能会被重复消费。

如果业务保证多次消费的结果是一样的那没问题,但是如果业务无法满足那就需要通过其它方式来保证消费端的幂等。

 

二、初级方式来保证尽量幂等

1、插入前先判断数据是否存在

这种是最基础的,也是我们在开发中必须要做的。我们会在插入或者更新前先判断下,当前这个数据数据库中是否已经存在,如果不存在则不允许重复插入,不存在则可插入。

代码示例如下:

<span style="color:#4b4b4b"><code class="language-java">    <span style="color:#f92672">public</span> <span style="color:#f92672">void</span> <span style="color:#a6e22e">save</span><span style="color:#f8f8f2">(Goods goods)</span> {<span style="color:#75715e">// 1、先通过商品唯一code,查询数据库属否存在   </span><span style="color:#e6db74">Goods</span> <span style="color:#e6db74">goods</span> <span style="color:#ab5656">=</span> findGoods(goods.getCode);<span style="color:#75715e">// 2、如果这条数据在db里已经存在了,此时就直接返回了   </span><span style="color:#f92672">if</span> (goods != <span style="color:#ae81ff">null</span>) {<span style="color:#f92672">return</span>;}<span style="color:#75715e">// 3、如果要是这条数据在db里不存在,此时就会执行数据插入逻辑了   </span>insertGoods(goods);}
</code></span>

2、前端做一些交互控制

好比有个新增商品的功能,有个保存按钮,用户点击保存按钮后,立马按钮置灰,或者页面跳转到商品列表页面,这样可以防止很大部分的前端重复提交。

 

三、高并发下如何保证幂等?

上面两种初级方法,在高并发下显然是无法保证接口幂等的,所以在高并发下,我们来如何保证接口的幂等呢,这里整理几种常见的解决办法。

1、基于悲观锁

定义: 当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。

这里以更新商品订单状态来举例:一般订单有订单创建订单确认订单支付订单完成取消订单等订单流程。

当我更新订单状态为订单完成的时候,我们首先通过判断该订单的状态是否是订单支付,如果是不是则直接返回,否则更新状态为已完成。

伪代码示例如下

<span style="color:#4b4b4b"><code class="language-sql">  <span style="color:#f92672">begin</span>; <span style="color:#75715e">-- 1.开始事务</span><span style="color:#75715e">-- 查询订单,判断状态</span><span style="color:#f92672">select</span> order_no,status <span style="color:#f92672">from</span> <span style="color:#f92672">order</span> <span style="color:#f92672">where</span> order_no<span style="color:#ab5656">=</span><span style="color:#e6db74">'20200524-1'</span> if(status <span style="color:#ab5656">!=</span>订单支付状态){<span style="color:#75715e">-- 非订单支付状态,不能更新为已完成;</span><span style="color:#f92672">return</span> ;}<span style="color:#75715e">-- 更新完成</span><span style="color:#f92672">update</span> <span style="color:#f92672">order</span> <span style="color:#f92672">set</span> status<span style="color:#ab5656">=</span><span style="color:#e6db74">'订单完成'</span> order_no<span style="color:#ab5656">=</span><span style="color:#e6db74">'20200524-1'</span> <span style="color:#f92672">commit</span>; <span style="color:#75715e">-- 2.提交事务</span>
</code></span>

这是我们常见的一种写法,但这种写法在高并发环境下,可能会造成一个业务被执行两次的情况发生:

同时有两个请求过来,大家几乎同时查数据库订单状态,都是订单支付状态,然后就支持接下来一系列操作,这就导致一个业务被执行了两次,如果接下来一系列操作不是幂等的

那么就会出现脏数据。这里我们就可以通过悲观锁实现,也就是添加for update字段。

伪代码示例如下

<span style="color:#4b4b4b"><code class="language-sql">  <span style="color:#f92672">begin</span>;  <span style="color:#75715e">--  1.开始事务</span><span style="color:#75715e">--  查询订单,判断状态</span><span style="color:#f92672">select</span> order_no,status <span style="color:#f92672">from</span> <span style="color:#f92672">order</span> <span style="color:#f92672">where</span> order_no<span style="color:#ab5656">=</span><span style="color:#e6db74">'20200524-1'</span> <span style="color:#f92672">for</span> <span style="color:#f92672">update</span> if(status <span style="color:#ab5656">!=</span>订单支付状态){<span style="color:#75715e">-- 非订单状态,不能更新为已完成;</span><span style="color:#f92672">return</span> ;}<span style="color:#75715e">--  更新完成</span><span style="color:#f92672">update</span> <span style="color:#f92672">order</span> <span style="color:#f92672">set</span> status<span style="color:#ab5656">=</span><span style="color:#e6db74">'完成'</span> order_no<span style="color:#ab5656">=</span><span style="color:#e6db74">'20200524-1'</span> <span style="color:#f92672">commit</span>; <span style="color:#75715e">-- 2.提交事务</span>
</code></span>

1)这里order_no需要添加索引,否则会锁表

2) 悲观锁在同一事务操作过程中,锁住了一行数据。悲观锁性能不佳所以一般不建议用悲观锁做这个事情。

2、基于乐观锁

定义:乐观锁就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制

所谓的乐观锁就是在表中新增一个version(版本号)字段。

通过版本号的方式,来控制update的操作的幂等性,用户查询出要修改的数据,系统将数据返回给页面,将数据版本号放入隐藏域,用户修改数据,点击提交,将版本号一同提交

给后台,后台使用版本号作为更新条件。

<span style="color:#4b4b4b"><code class="language-sql"><span style="color:#f92672">update</span> <span style="color:#f92672">set</span> version <span style="color:#ab5656">=</span> version <span style="color:#ab5656">+</span><span style="color:#ae81ff">1</span> ,count<span style="color:#ab5656">=</span>count<span style="color:#ab5656">+</span><span style="color:#ae81ff">1</span> <span style="color:#f92672">where</span> id <span style="color:#ab5656">=</span>xxx <span style="color:#f92672">and</span> version <span style="color:#ab5656">=</span> ${version};
</code></span>

注意:乐观锁能够保证的是update的操作的幂等性,如果你的update本身就是幂等操作,或者install操作那就不能用乐观锁了。

3、基于状态码

很多业务表,都是有状态的,比如订单表,一般订单有1-订单创建2-订单确认3-订单支付、 4-订单完成5-取消订单等订单流程,当我们更新订单状态

<span style="color:#4b4b4b"><code class="language-sql"><span style="color:#f92672">update</span> order_table <span style="color:#f92672">set</span> status<span style="color:#ab5656">=</span><span style="color:#ae81ff">3</span> <span style="color:#f92672">where</span> order_no<span style="color:#ab5656">=</span><span style="color:#e6db74">'20200524-1'</span> <span style="color:#f92672">and</span> status<span style="color:#ab5656">=</span><span style="color:#ae81ff">2</span>;
</code></span>

第一个请求时,成功把 订单确认 状态修改成 订单支付,sql执行结果的影响行数是1。

第二个请求时,同样想把 订单确认 状态修改成 订单支付,但是sql执行结果的影响行数为0。如果是0,那么我们直接可以返回成功了。而不需要做接下来的业务操作,以此来保证保证

接口的幂等性。

4、基于唯一索引

一般来讲悲观锁、乐观锁、状态码作用于update操作来实现幂等,而唯一索引是针对install操作来保证幂等。

1) 创建订单时,前端先通过接口获取订单号,再请求后端时带入订单号,订单表中订单号添加唯一索引,如果存在插入相同订单号则直接报错。

2) 消费MQ消息时,messageId是唯一的,我们可以新添加一种消费记录表,将messageId作为主键,如果重复消费那么就会存在相同的messageId,插入直接报错。

5、基于分布式锁

分布式锁实现幂等性的逻辑就是,请求过来时,先去尝试获得分布式锁,如果获得成功,就执行业务逻辑,反之获取失败的话,就舍弃请求直接返回成功。

其实前面介绍过的悲观锁,本质是使用了数据库的分布式锁,都是将多个操作打包成一个原子操作,保证幂等。但由于数据库分布式锁的性能不太好,

我们可以改用:redis或zookeeper来实现分布式锁。

6、基于 Token

token方案的特点就是:需要两次请求才能完成一次业务的操作。

一般包括两个请求阶段:

1)客户端请求申请获取token,服务端生成token返回。

2)第二次请求带着这个token,服务端验证token,完成业务操作。

注意:,在验证token是否存在,不要用redis.get(token)之后,在用redis.del(token),这样不是原子操作在高并发情况下依然会存在幂等问题。

我们可以直接用redis.del(token)的方式:

<span style="color:#4b4b4b"><code class="language-sql">redis<span style="color:#ab5656">></span> <span style="color:#f92672">SET</span> key1 "Hello"
OK
redis<span style="color:#ab5656">></span> <span style="color:#f92672">SET</span> key2 "World"
OK
redis<span style="color:#ab5656">></span> DEL key1 key2 key3
(<span style="color:#e6db74">integer</span>) <span style="color:#ae81ff">2</span>
redis<span style="color:#ab5656">></span> 
</code></span>

我们看返回是否大于0,就知道是否有数据了,而且因为redis命令操作是单线程的,所以不会出现同时返回1,所以是能够保证幂等的。

这种方式最大的缺点需要两次请求,其实简单点我们可以进行一次请求,那就是前端生成唯一token,而不通过后端获取。

Setnx 命令

在指定的 key 不存在时,为 key 设置指定的值。设置成功,返回1。 设置失败,返回 0。

实例

<span style="color:#4b4b4b"><code class="language-sql">redis<span style="color:#ab5656">></span> <span style="color:#f92672">EXISTS</span> job      <span style="color:#75715e">--  job 不存在</span>
(<span style="color:#e6db74">integer</span>) <span style="color:#ae81ff">0</span>redis<span style="color:#ab5656">></span> SETNX job "programmer"  <span style="color:#75715e">--  job 设置成功</span>
(<span style="color:#e6db74">integer</span>) <span style="color:#ae81ff">1</span>redis<span style="color:#ab5656">></span> SETNX job "code-farmer" <span style="color:#75715e">--  尝试覆盖 job ,失败</span>
(<span style="color:#e6db74">integer</span>) <span style="color:#ae81ff">0</span>redis<span style="color:#ab5656">></span> <span style="color:#f92672">GET</span> job                 <span style="color:#75715e">--  没有被覆盖</span>
"programmer"
</code></span>

如果返回1则说明第一次请求,如果返回0则说明不是第一次请求,直接返回。

这里需要注意的是Setnx命令key值不会自动过期的,所以不清除会一直占用内存,我们可以借助Expire命令来设置有效时间。

<span style="color:#4b4b4b"><code class="language-sql">redis<span style="color:#ab5656">></span> SETNX mykey "programmer"  <span style="color:#75715e">--  job 设置成功</span>
(<span style="color:#e6db74">integer</span>) <span style="color:#ae81ff">1</span>
<span style="color:#75715e">--  如果设置成功,那么设置将该键的超时设置为 10 秒</span>
redis<span style="color:#ab5656">></span> expire mykey <span style="color:#ae81ff">10</span> 
</code></span>


http://chatgpt.dhexx.cn/article/AQdoJW1j.shtml

相关文章

接口幂等性是什么?如何设计?

接口幂等 什么是接口幂等&#xff1f;为什么接口需要幂等性设计前端重复提交表单黑客恶意攻击接口超时重复提交消息重复消费 哪些接口需要幂等&#xff1f;如何实现幂等前端拦截数据库唯一索引实现数据库乐观锁实现数据库悲观锁实现JVM锁实现分布式锁实现Token实现 总结 接口幂…

开发中是如何保证接口幂等性的?

文章目录 一、定义二、业务场景三、保证幂等性常用方法方案1: insert前先select&#xff08;基于mysql的分布式锁&#xff09;方案2&#xff1a;加悲观锁 select * from table where id 1 for update&#xff08;基于mysql的分布式锁&#xff09;方案3&#xff1a;加乐观锁 增…

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

什么是幂等性 幂等用于表示任意多次请求均与一次请求执行的结果相同&#xff0c;也就是说对于一个接口而言&#xff0c;无论调用了多少次&#xff0c;最终得到的结果都是一样的。 如何保证接口的幂等性 1、前端拦截 2、使用数据库实现幂等性 3、使用 JVM 锁实现幂等性 4、…

接口幂等性常见的解决方案

一、什么是接口幂等性? 接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的&#xff0c;不会因为多次点击而产生了副作用。 举个最简单的例子&#xff0c;那就是支付&#xff0c;用户购买商品后支付&#xff0c;支付扣款成功&#xff0c;但是返回结果的…

接口幂等性解决方案

一、分布式锁解决方案 先说这种方案&#xff0c;在网上有一些文章说可以通过分布式锁来保证幂等性。但是我认为这种方案不能保证幂等性&#xff0c;不可取。看下面分析 ①、方案流程介绍 用户通过浏览器发起请求&#xff0c;服务端接收请求数据&#xff0c;并且从请求数据中…

什么是接口的幂等性,如何实现接口幂等性?一文搞定

微信搜索《Java鱼仔》&#xff0c;每天一个知识点不错过 每天一个知识点 什么是接口的幂等性&#xff0c;如何实现接口幂等性&#xff1f; &#xff08;一&#xff09;幂等性概念 幂等性原本是数学上的概念&#xff0c;用在接口上就可以理解为&#xff1a;同一个接口&#x…

什么是幂等性?四种接口幂等性方案详解

幂等性在我们的工作中无处不在&#xff0c;无论是支付场景还是下订单等核心场景都会涉及&#xff0c;也是分布式系统最常遇见的问题&#xff0c;除此之外&#xff0c;也是大厂面试的重灾区。 知道了幂等性的重要性&#xff0c;下面我就详细介绍幂等性以及具体的解决方案&#…

接口的幂等性

1、接口调用存在的问题 现如今我们的系统大多拆分为分布式 SOA&#xff0c;或者微服务&#xff0c;一套系统中包含了多个子系统服务&#xff0c;而一个子系统服务往往会去调用另一个服务&#xff0c;而服务调用服务无非就是使用 RPC 通信或者 restful。既然是通信&#xff0c;…

如何保证接口的幂等性?

什么是幂等性?所谓幂等&#xff0c;就是任意多次执行所产生的影响均与一次执行的影响相同。 为什么会产生接口幂等性问题 在计算机应用中&#xff0c;可能遇到网络抖动&#xff0c;临时故障&#xff0c;或者服务调用失败&#xff0c;尤其是分布式系统中&#xff0c;接口调用…

什么是接口的幂等性以及如何实现接口幂等性

目录 1、接口调用存在的问题 2、什么是接口的幂等性 3、不做接口的幂等性会产生什么影响 4、什么情况下需要保证接口的幂等性 4.1 select&#xff1a;查询操作 4.2 insert&#xff1a;新增操作 4.3 delete&#xff1a;删除操作 4.3.1 绝对删除 具有幂等性 4.3.2 相对删…

Python安装pygame教程

Python安装pygame教程 1、版本说明 由于python3.8与pygame存在不兼容的问题&#xff0c;因此在下载Python的时候&#xff0c;需要下载python3.8以下版本的&#xff0c;我下载的是python3.7.5 2、具体步骤 1、在本机控制台通过命令安装pygame pip install pygame 出现错误 …

Pygame 教程(2):重要的概念及对象

本章&#xff0c;将介绍 Pygame 中的重要概念及对象。 导航 上一章&#xff1a;创建第一个应用程序 下一章&#xff1a;绘制图形 文章目录 导航像素坐标Rect 对象Color 对象Surface 对象实例&#xff1a;矩形生成器创建初始窗口创建生成矩形的函数调用函数完整代码 结语 像素…

Pygame教程系列二:MoviePy视频播放篇

【前言】 在pygame 2.0.0版本之前&#xff0c;播放视频可以使用pygame.movie.Movie(xxxx.mpg)播放(只支持.mpg格式的视频)&#xff0c;但是在pygame 2.0.0之后&#xff0c;作者因为觉得视频模块维护成本太高就给抛弃了&#xff0c;假如你使用pygame 2.0.0&#xff0c;还调用上述…

Pygame 教程(4):图像传输和绘制文本

本章&#xff0c;你将学会如何传输图像和绘制文本。 导航 上一章&#xff1a;绘制图形 下一章&#xff1a;监测游戏时间 文章目录 导航加载图像导出图像绘制文本实例&#xff1a;画板添加常量限制坐标定义属性绘制色板更改线条粗细处理鼠标事件处理键盘事件调用事件处理方法完…

Pygame基础教程(一)

写在前面的话&#xff1a; 本系列教程仅有一些在本机调试通过的代码&#xff08;如代码中发现bug&#xff0c;恳请包涵&#xff09;。除代码中出现的一些主要注释外&#xff0c;不会出现太多其他文字解释&#xff0c;但是&#xff0c;文章中会给出主要模块的官方文档地址。再次…

Pygame 教程(5):监测游戏时间

本章&#xff0c;你将学习如何监测游戏时间。 导航 上一章&#xff1a;图像传输和绘制文本 下一章&#xff1a;努力更新中…… 文章目录 导航监测时间游戏帧速率实例&#xff1a;绘图性能对比结语 监测时间 在游戏程序中&#xff0c;时常需要随着时间的流逝而做出不同的动作…

Pygame教程系列四:播放音频篇

【前言】 pygame播放音频文件这部分相对来说比较简单&#xff0c;主要是用到pygame.mixer模块&#xff0c;不过也有一些地方需要注意的&#xff0c;咱们直接先看看案例 1、案例效果图 2、案例代码 import pygame from mutagen.mp3 import MP3 # 标识是否退出循环 exitFlag Fa…

python3.8安装pygame_Python3.8安装Pygame教程

注&#xff1a;因为最近想用一下Python做一些简单小游戏的开发作为项目练手之用&#xff0c;而Pygame模块里面提供了大量的有用的方法和属性。今天我们就在之前安装过PyCharm的基础上&#xff0c;安装Pygame&#xff0c;下面是安装的步骤&#xff0c;希望能够帮到大家。 第一步…

Pygame 教程(3):绘制图形

本章&#xff0c;你将学习如何在 Pygame 中绘制图形。 导航 上一章&#xff1a;重要的概念及对象 下一章&#xff1a;图像传输和绘制文本 文章目录 导航抗锯齿draw 模块实例&#xff1a;跟随鼠标的图形创建初始窗口添加变量捕捉鼠标事件绘制图形完整代码 结语 抗锯齿 抗锯齿…

Mac Pycharm导入Pygame教程(超细)

首先先新建一个想要使用Pygame的项目 进入项目后&#xff0c;点击文件&#xff08;File&#xff09;——新项目设置&#xff08;settings&#xff09; 点击新项目的偏好设置&#xff08;Preferences for new project &#xff09; 随后可以看到 点击Python 编译器&#xff0…