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

article/2025/10/3 17:40:48

目录

1、接口调用存在的问题

2、什么是接口的幂等性

3、不做接口的幂等性会产生什么影响

4、什么情况下需要保证接口的幂等性

4.1 select:查询操作

4.2 insert:新增操作

4.3 delete:删除操作

4.3.1 绝对删除 具有幂等性

4.3.2 相对删除 不具有幂等性

4.4 update:更新操作

4.4.1 绝对更新 具有幂等性

4.4.2 相对更新 不具有幂等性

5、使用幂等的业务场景

5.1 前端重复提交

5.2 接口超时重试

5.3 MQ消息重复消费

6、幂等性的解决方案

6.1 唯一索引

6.2 乐观锁

6.3 悲观锁

6.4 CAS思想保证接口幂等性

6.5 分布式锁

6.6 基于token+redis机制实现(通用性强)

6.7 基于redis命令setnx实现

6.8 通过业务代码逻辑判断实现

99、参考

1、接口调用存在的问题

在大多数情况下,一个大系统都会拆分为多个微服务组成。也就是说,一个大系统的完整功能往往是由多个子系统的小功能构建而成的,而一个子系统服务往往会调用另外一个子系统提供出来的服务,而服务调用无非就是使用RPC接口通信,既然是通信,那么就有可能在服务器处理数据完毕后返回结果的时候挂掉,这个时候客户端发现已经过了很久 但还是没能从服务器端拿到正确的响应,那么,客户端就有可能会多次点击按钮以触发多次接口请求,那么,处理数据的结果是否要统一呢?答案是肯定的,尤其是在支付场景。

2、什么是接口的幂等性

接口幂的等性,就是指 用户对于同一操作发起的一次请求或者多次请求,其操作的结果都是一致的,不会因为进行多次请求而产生副作用。这里的副作用,可以认为在多次请求操作时,每一次请求对数据的状态都会产生影响。注意,这里并没有要求接口返回的结果是一致的,而是要求被数据的状态是一致的。例如:update order set moeny = 100 where orderId = 2029282312;  该操作无论执行多少次,被操作数据的状态都是一致的。

3、不做接口的幂等性会产生什么影响

支付场景:用户购买商品后,发起支付操作,支付系统处理支付成功后,由于网络原因没有及时返回操作成功的信息给用户,其实这个时候订单已经扣过款,相应的支付流水也都已经生成。这个时候,用户又点击支付操作,此时会进行第二次扣款,扣款成功后吧操作成功的信息返回给了用户。用户去查看支付订单和流水时 会发现自己支付了两次,完蛋了,该系统要被用户投诉了。这就是没有保证接口的幂等性而造成的不良后果。

4、什么情况下需要保证接口的幂等性

在【增删改查】4个SQL操作中,尤为需要注意的就是增加和修改操作。

4.1 select:查询操作

查询操作不会对数据产生副作用。查询一次或者查询多次,在数据不变的情况下,查询结果都是一样的,所以,select 操作是天然的幂等操作

4.2 insert:新增操作

新增操作在重复提交的场景下会出现幂等性问题,比如以上的支付场景。

insert into product_info (id, price);

上述 insert SQL,ID是自增主键,执行多次就会新增多条记录,对结果集产生了副作用,所以,insert 操作天然不具有幂等性。

4.3 delete:删除操作

删除操作可以分为两种:绝对删除和相对删除。其中,绝对删除不会对数据产生副作用,具有幂等性;相对删除会对数据产生副作用,不具有幂等性。

4.3.1 绝对删除 具有幂等性

delete from order where id = 3;

无论该SQL执行多少次,对结果集产生的效果都是一样的,只删除了一条数据,不会对数据产生副作用,所以,它具有幂等性。

4.3.2 相对删除 不具有幂等性

delete from order where id > 23;

该SQL每执行一次,对结果集产生的结果可能都不一样,同一操作执行多次对数据产生了副作用,所以,它不具有幂等性。

4.4 update:更新操作

更新操作可以分为两种:绝对更新和相对更新。其中,绝对更新不会对数据产生副作用,具有幂等性;相对更新会对数据产生副作用,不具有幂等性。

4.4.1 绝对更新 具有幂等性

update Goods set stock = 586 where goodId = 10;

无论该SQL执行多少次,对结果集产生的效果都是一样的,只更新了一条数据,不会对数据产生副作用,所以,它具有幂等性。

4.4.2 相对更新 不具有幂等性

update Goods set stock = stock + 1 where goodid = 10;

该SQL每执行一次,对结果集产生的结果都不一样,库存数量都会增加10,同一操作执行多次对数据产生了副作用,所以,它不具有幂等性。

5、使用幂等的业务场景

5.1 前端重复提交

用户注册、用户创建商品订单等操作,前端都会提交一些数据给后台服务,后台需要根据用户提交的数据在数据库中创建记录。如果用户不小心多点了几次,后端就收到了好几次提交,这时,就会在数据库中重复创建了多条记录。这就是接口没有幂等性带来的 bug。

5.2 接口超时重试

对于给第三方调用的接口,有可能会因为网络原因而调用超时失败,这时,一般在设计的时候会对接口调用加上超时/失败重试机制。如果第一次调用已经执行了一半业务逻辑时,发生了网络异常,这时,再次调用时就会因为脏数据的存在而出现调用异常。

5.3 MQ消息重复消费

在使用消息中间件来处理消息队列,且手动 ACK 确认消息被正常消费时,如果消费者突然断开连接,那么已经执行了一半的消息就会被重新放回队列。当消息被其他消费者重新消费时,如果没有幂等性,就会导致消息重复消费时结果异常,如数据库重复数据、数据库数据冲突、资源重复等。

6、幂等性的解决方案

6.1 唯一索引

使用唯一索引可以避免脏数据的 insert,当插入重复数据时数据库会抛出异常,保证了数据的唯一性。

6.2 乐观锁

这里的乐观锁指的是用乐观锁的原理去实现:为表增加一个 version 字段,当数据需要更新 update 时,先去表中获取此时的 version 版本号。

select version from tableName where Id = 1;

更新数据时,首先和最新的版本号作比较,如果不相等,则说明已经有其他的请求去更新数据了,则本次提示更新会失败,让用户重试即可。

update tableName set count = count + 1, version = version + 1 where version = #{version};

6.3 悲观锁

乐观锁可以实现的,往往使用悲观锁也能实现:即在获取被操作数据的时候进行加锁。当同时有多个重复请求过来时,其他请求都会因无法获得被操作数据的锁而阻塞住,因此,其他请求都无法对被操作数据进行操作。

6.4 CAS思想保证接口幂等性

状态机制来实现接口幂等性(一个事务的状态是不可逆的)。

针对更新操作,例如 电商订单的支付状态:0=待支付,1=支付中,2=支付成功,3=支付失败。

update Orders set status = 1 where status = 0 and orderId = “201251487987”;
update Orders set status = 2 where status = 1 and orderId = “201251487987”;
update Orders set status = 3 where status = 1 and orderId = “201251487987”;

该SQL语句利用【订单状态的CAS】来保证该操作的幂等性。比如,要进行订单支付,先用CAS思想做更新订单状态的操作,然后再去做实际支付的操作:

(1)返回影响行数=1,则代表订单状态修改成功,可以继续执行后面的支付业务代码。

(2)返回影响行数=0,则代表订单状态修改失败,该订单已经不是待支付订单了,不可以继续执行后面的支付业务代码。(其实这里的解释有待商榷)。

注释:实际这里是利用CAS原理。

6.5 分布式锁

幂等的本质是分布式锁的问题,分布式锁正常可以通过 redis 或 zookeeper 来实现。

在分布式环境下,锁定全局唯一资源,使多个请求串行化,实际表现为互斥锁,可以防止重复,以此来解决幂等性问题。

6.6 基于token+redis机制实现(通用性强)

token 机制的核心思想:是为每一次操作都生成一个唯一性的凭证,也就是 token。一个token 在操作的每一个阶段只有一次执行权,一旦执行成功,则保存执行结果并且删除该 token。对重复的请求,因为没有了先前的那个 token 而返回指定的同一个结果给客户端。

通过【 token+redis 机制】实现接口的幂等性,这是一种比较通用性的实现方法。示意图如下:

具体流程步骤:

(1)客户端会先发送一个请求去获取 token,服务端会生成一个全局唯一的 ID 作为 token,并且保存在 redis 中,同时会把这个 ID 返回给客户端。

(2)客户端第二次调用业务请求的时候,必须携带这个 token。

(3)服务端会校验这个 token:如果校验成功,则执行业务,并删除 redis 中的 token。如果校验失败,说明 redis 中已经没有了对应的 token,则表示是重复操作,直接返回指定的结果给客户端。

注意:对 redis 中是否存在 token 以及删除的代码逻辑建议用 Lua 脚本实现,以此来保证多个操作的原子性。全局唯一 ID 可以用百度的 uid-generator、美团的 Leaf 去生成。

6.7 基于redis命令setnx实现

(1)这种实现方式是基于Redis的一个命令 setnx 实现的。

(2)setnx key value:当且仅当 key 不存在时,将 key 的值设为 value,并返回 1。若给定的 key 已经存在,则 setnx 不做任何动作,并返回 0。注意:该命令在设置成功时返回 1,设置失败时返回 0。

(3)通过【 redis的命令 setnx 】实现接口的幂等性,示意图如下:

具体流程步骤:

(1)客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段 。

(2)将该字段以 setnx 的方式存入 redis 中,并根据业务设置相应的超时时间 timeout。

(3)如果设置成功,则证明这是第一次请求,则执行后续的业务逻辑。

(4)如果设置失败,则证明这不是第一次请求,已经执行过当前请求,直接返回即可。

6.8 通过业务代码逻辑判断实现

通过【业务代码逻辑判断】实现接口幂等性,只能针对一些满足判断的业务逻辑实现,具有一定局限性。比如:用户购买商品的订单系统与支付系统

订单系统负责记录用户的购买记录以及订单的流转状态(orderStatus)。支付系统用于付款,提供如下接口。订单系统与支付系统通过分布式交互。

boolean pay ( int accountid, BigDecimal amount );  // 用于付款,扣除用户余额

这种情况下,支付系统已经扣款,但是,因为网络原因,订单系统没有获取到支付系统返回的确切结果,因此,订单系统需要重试。

由上图可见,支付系统并没有做到接口的幂等性,订单系统第一次调用和第二次调用,用户分别被扣了两次钱,不符合幂等性原则(同一个订单,无论是调用了多少次,用户都只会扣款一次)。

如果需要支持幂等性,付款接口需要修改为以下接口:

boolean pay ( int orderId, int accountId, BigDecimal amount);

通过 orderId 来标定订单的唯一性,支付系统只要检测到该订单已经支付过,则第二次调用就不会扣款,而是会直接返回结果:

在不同的业务中,不同接口需要有不同的幂等性,特别是在分布式系统中,因为网络原因而未能得到确定的结果,往往需要支持接口做幂等性校验。

随着分布式系统及微服务的普及,因为网络原因而导致调用系统未能获取到确切结果而导致重试,这就需要被调用系统具有幂等性。

例如上文所阐述的支付系统,针对同一个订单保证支付的幂等性,一旦订单的支付状态确定之后,以后的操作都会返回相同的结果,对用户的扣款也只会有一次。

这种接口的幂等性,简化到数据层面的操作就是:

update userAmount set amount = amount - 'value', paystatus = 'paid' where orderId= 'orderid' and paystatus = 'unpay';

其中:value 是用户要减少的金额,paystatus 代表支付状态( paid 代表已经支付,unpay 代表未支付 ),orderid 是唯一的订单号。

在上文中提到的订单系统,订单具有自己的状态(orderStatus),订单状态存在一定的单向流转。

订单首先有:订单提交(0),订单付款中(1),订单付款成功(2),订单付款失败(3),简化之后:

(1)当 orderStatus = 1 时,其前置状态只能是 0,也就是说:将 orderStatus 由【 0 -> 1】是需要幂等性的,SQL如下:

update Order set orderStatus = 1 where OrderId = 'orderid' and orderStatus = 0;

(2)当 orderStatus = 0、1 两种状态时,对订单执行【 0 -> 1】的状态流转操作应该是具有幂等性的。这时候,需要在执行 update 操作之前检测 orderStatus 是否已经 = 1,如果已经 = 1,则直接返回 true 即可。

(3)当 orderStatus = 2 时,再进行订单状态【 0 -> 1】操作就无法成功。但是,幂等性是针对同一个请求的,也就是针对同一个 requestId 保持幂等,这时候再执行:

update Order set orderStatus = 1 where OrderId = 'orderid' and orderStatus = 0;

接口就会返回失败,系统没有产生修改,如果再发一次,requestId 还是相同的,对系统同样没有产生修改。

 

这几种实现幂等的方式其实都是大同小异的,类似的还有使用【状态机、悲观锁、乐观锁】的方式来实现,都是比较简单的。总之,当你去设计一个接口的时候,接口的幂等性都是首要考虑的问题,特别是当你负责设计转账、支付这种涉及到 money 的接口,要格外注意。

99、参考

(1)https://javayz.blog.csdn.net/article/details/109684180

(2)https://blog.csdn.net/qq_29978863/article/details/107739744

(3)https://www.cnblogs.com/huaixiaonian/p/9577567.html

 


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

相关文章

Python安装pygame教程

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

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

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

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

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

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

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

Pygame基础教程(一)

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

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

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

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

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

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

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

Pygame 教程(3):绘制图形

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

Mac Pycharm导入Pygame教程(超细)

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

mac python3.8上怎么安装pygame 第三方库_Python3.8安装Pygame教程步骤详解

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

Pygame教程系列一:快速入门篇

【简介】 Pygame 是python用来开发视频游戏的游戏引擎,底层主要是SDL库实现,算是目前利用python开发小游戏的一个性能比较高的一个游戏框架 一、安装pygame 使用pip下载安装 pip install pygame二、入门案例详析 1、示例效果 2、示例代码 import os …

pygame教程3

目录 做个小游戏精灵类精灵类介绍使用精灵类 做个小游戏 这个小游戏使用了pygame教程2的知识。 #↓初始化 import pygame,sys from pygame.locals import * pygame.init()#初始化pygame。 screen pygame.display.set_mode((800,600)) pygame.display.set_caption("Hell…

pygame教程2

目录 响应键盘上的事件让画面动起来了解x轴和y轴画圆形圆形动起来 响应键盘上的事件 import pygame#导入pygame。 import sys#导入sys from pygame.locals import *#导入pygame所有的常量,方便以后使用。 pygame.init()#初始化pygame。 screen pygame.display.set…

pygame教程笔记

pygame教程 安装pygameGame Development 1-1: Getting Started with PygameGame Development 1-2: Working with SpritesGame Development 1-3: More About SpritesPygame Shmup Part 1: Player Sprite and ControlsPygame Shmup Part 2: Enemy SpritesPygame Shmup Part 3: Co…

pygame基础教程

pygame简介 pygame可以实现python游戏的一个基础包。 pygame实现窗口 初始化pygame,init()类似于java类的初始化方法,用于pygame初始化。 pygame.init() 设置屏幕,(500,400)设置屏幕初始大小为500 * 400的大小, 0和32 是比较高…

Python pygame(GUI编程)模块最完整教程(1)

提示:下滑文章左侧可以查看目录!本教程分为多篇,总目录如下。 总目录: README.md Python-ZZY/Python-Pygame最完整教程 - Gitee.com 1 初识pygame 1.1 简介 pygame是python中一个流行的GUI编程模块,是专门为了开发游…

Pygame教程(非常详细)

文章目录 教程特点阅读条件 Pygame是什么扩展知识 Pygame下载和安装1) pip包管理器安装2) 二进制安装包安装 第一个Pygame程序初始化程序创建Surface对象事件监听游戏循环 Pygame Display显示模块详解Pygame Surface创建图像Pygame Transform图像变形Pygame Time时间控制详解1)…

GeoWave0.9.8开发人员指南

GeoWave0.9.8开发人员指南 官方英文地址:http://s3.amazonaws.com/geowave/0.9.8/docs/devguide.html 介绍 什么是GeoWave GeoWave是一个开源库,用于在排序的键值数据存储和流行的大数据框架之上存储,索引和搜索多维数据。GeoWave包含特定的…

行人检测之DPM

基于可识别训练的部件模型的目标检测 Object Detection with Discriminatively Trained Part Based Models 摘要 基于多尺度可变形的部件模型我们来描述一个目标检测系统。我们的系统能够表示高度可变的对象类并在PASCAL目标检测挑战达到最先进的结果。虽然可变形部件模型变…