老生常谈:接口幂等性,防止并发插入重复数据

article/2025/9/17 7:30:24

分布式系统中,接口幂等性问题,对于开发人员来说,是一个跟语言无关的公共问题。不知道你有没有遇到过这些场景:

有时我们在填写某些form表单时,保存按钮不小心快速点了两次,表中竟然产生了两条重复的数据,只是id不一样。
我们在项目中为了解决接口超时问题,通常会引入了重试机制。第一次请求接口超时了,请求方没能及时获取返回结果(此时有可能已经成功了),为了避免返回错误的结果(这种情况不可能直接返回失败吧?),于是会对该请求重试几次,这样也会产生重复的数据。
mq消费者在读取消息时,有时候会读取到重复消息,如果处理不好,也会产生重复的数据。
没错,这些都是幂等性问题。

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

这类问题多发于接口的:

insert操作,这种情况下多次请求,可能会产生重复数据。
update操作,如果只是单纯的更新数据,比如:update user set status=1 where id=1,是没有问题的。如果还有计算,比如:update user set status=status+1 where id=1,这种情况下多次请求,可能会导致数据错误。
那么我们要如何保证接口幂等性?本文将会告诉你答案。

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

 

该方案可能是我们平时在防止产生重复数据时,使用最多的方案。但是该方案不适用于并发场景,在并发场景中,要配合其他方案一起使用,否则同样会产生重复数据。我在这里提一下,是为了避免大家踩坑。

2. 加悲观锁
在支付场景中,用户A的账号余额有150元,想转出100元,正常情况下用户A的余额只剩50元。一般情况下,sql是这样的:

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


 
 
如果出现多次相同的请求,可能会导致用户A的余额变成负数。这种情况,用户A来可能要哭了。于此同时,系统开发人员可能也要哭了,因为这是很严重的系统bug。

为了解决这个问题,可以加悲观锁,将用户A的那行数据锁住,在同一时刻只允许一个请求获得锁,更新数据,其他的请求则等待。

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

select * from user id=123 for update;


 
 
具体流程如下:

 

具体步骤:

多个请求同时根据id查询用户信息。
判断余额是否不足100,如果余额不足,则直接返回余额不足。
如果余额充足,则通过for update再次查询用户信息,并且尝试获取锁。
只有第一个请求能获取到行锁,其余没有获取锁的请求,则等待下一次获取锁的机会。
第一个请求获取到锁之后,判断余额是否不足100,如果余额足够,则进行update操作。
如果余额不足,说明是重复请求,则直接返回成功。
需要特别注意的是:如果使用的是mysql数据库,存储引擎必须用innodb,因为它才支持事务。此外,这里id字段一定要是主键或者唯一索引,不然会锁住整张表。
悲观锁需要在同一个事务操作过程中锁住一行数据,如果事务耗时比较长,会造成大量的请求等待,影响接口性能。此外,每次请求接口很难保证都有相同的返回值,所以不适合幂等性设计场景,但是在防重场景中是可以的使用的。在这里顺便说一下,防重设计 和 幂等设计,其实是有区别的。防重设计主要为了避免产生重复数据,对接口返回没有太多要求。而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果。

3. 加乐观锁
既然悲观锁有性能问题,为了提升接口性能,我们可以使用乐观锁。需要在表中增加一个timestamp或者version字段,这里以version字段为例。

在更新数据之前先查询一下数据:

select id,amount,version from user id=123;


 
 
如果数据存在,假设查到的version等于1,再使用id和version字段作为查询条件更新数据:

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


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

由于第一次请求version等于1是可以成功的,操作成功后version变成2了。这时如果并发的请求过来,再执行相同的sql:

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


该update操作不会真正更新数据,最终sql的执行结果影响行数是0,因为version已经变成2了,where中的version=1肯定无法满足条件。但为了保证接口幂等性,接口可以直接返回成功,因为version值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。

具体流程如下:

 

具体步骤:

先根据id查询用户信息,包含version字段
根据id和version字段值作为where条件的参数,更新用户信息,同时version+1
判断操作影响行数,如果影响1行,则说明是一次请求,可以做其他数据操作。
如果影响0行,说明是重复请求,则直接返回成功。
4. 加唯一索引
绝大数情况下,为了防止重复数据的产生,我们都会在表中加唯一索引,这是一个非常简单,并且有效的方案。

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


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

虽说抛异常对数据来说没有影响,不会造成错误数据。但是为了保证接口幂等性,我们需要对该异常进行捕获,然后返回成功。

如果是java程序需要捕获:DuplicateKeyException异常,如果使用了spring框架还需要捕获:MySQLIntegrityConstraintViolationException异常。

具体流程图如下:

 

具体步骤:

用户通过浏览器发起请求,服务端收集数据。
将该数据插入mysql
判断是否执行成功,如果成功,则操作其他数据(可能还有其他的业务逻辑)。
如果执行失败,捕获唯一索引冲突异常,直接返回成功。
5. 建防重表
有时候表中并非所有的场景都不允许产生重复的数据,只有某些特定场景才不允许。这时候,直接在表中加唯一索引,显然是不太合适的。

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

该表可以只包含两个字段:id 和 唯一索引,唯一索引可以是多个字段比如:name、code等组合起来的唯一标识,例如:susan_0001。

具体流程图如下:

 

具体步骤:

用户通过浏览器发起请求,服务端收集数据。
将该数据插入mysql防重表
判断是否执行成功,如果成功,则做mysql其他的数据操作(可能还有其他的业务逻辑)。
如果执行失败,捕获唯一索引冲突异常,直接返回成功。
需要特别注意的是:防重表和业务表必须在同一个数据库中,并且操作要在同一个事务中。
6. 根据状态机
很多时候业务表是有状态的,比如订单表中有:1-下单、2-已支付、3-完成、4-撤销等状态。如果这些状态的值是有规律的,按照业务节点正好是从小到大,我们就能通过它来保证接口的幂等性。

假如id=123的订单状态是已支付,现在要变成完成状态。

update `order` set status=3 where id=123 and status=2;


 
 
第一次请求时,该订单的状态是已支付,值是2,所以该update语句可以正常更新数据,sql执行结果的影响行数是1,订单状态变成了3。

后面有相同的请求过来,再执行相同的sql时,由于订单状态变成了3,再用status=2作为条件,无法查询出需要更新的数据,所以最终sql执行结果的影响行数是0,即不会真正的更新数据。但为了保证接口幂等性,影响行数是0时,接口也可以直接返回成功。

具体流程图如下:

 

具体步骤:

用户通过浏览器发起请求,服务端收集数据。
根据id和当前状态作为条件,更新成下一个状态
判断操作影响行数,如果影响了1行,说明当前操作成功,可以进行其他数据操作。
如果影响了0行,说明是重复请求,直接返回成功。
主要特别注意的是,该方案仅限于要更新的表有状态字段,并且刚好要更新状态字段的这种特殊情况,并非所有场景都适用。
7. 加分布式锁
其实前面介绍过的加唯一索引或者加防重表,本质是使用了数据库的分布式锁,也属于分布式锁的一种。但由于数据库分布式锁的性能不太好,我们可以改用:redis或zookeeper。

鉴于现在很多公司分布式配置中心改用apollo或nacos,已经很少用zookeeper了,我们以redis为例介绍分布式锁。

目前主要有三种方式实现redis的分布式锁:

setNx命令
set命令
Redission框架
每种方案各有利弊,具体实现细节我就不说了,有兴趣的朋友可以加我微信找我私聊。

具体流程图如下:

 

具体步骤:

用户通过浏览器发起请求,服务端会收集数据,并且生成订单号code作为唯一业务字段。
使用redis的set命令,将该订单code设置到redis中,同时设置超时时间。
判断是否设置成功,如果设置成功,说明是第一次请求,则进行数据操作。
如果设置失败,说明是重复请求,则直接返回成功。
需要特别注意的是:分布式锁一定要设置一个合理的过期时间,如果设置过短,无法有效的防止重复请求。如果设置过长,可能会浪费redis的存储空间,需要根据实际业务情况而定。
最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。

[BAT大佬写的刷题笔记,让我offer拿到手软](这位BAT大佬写的Leetcode刷题笔记,让我offer拿到手软)

8. 获取token
除了上述方案之外,还有最后一种使用token的方案。该方案跟之前的所有方案都有点不一样,需要两次请求才能完成一次业务操作。

第一次请求获取token
第二次请求带着这个token,完成业务操作。
具体流程图如下:

第一步,先获取token。

 

第二步,做具体业务操作。

 

具体步骤:

用户访问页面时,浏览器自动发起获取token请求。
服务端生成token,保存到redis中,然后返回给浏览器。
用户通过浏览器发起请求时,携带该token。
在redis中查询该token是否存在,如果不存在,说明是第一次请求,做则后续的数据操作。
如果存在,说明是重复请求,则直接返回成功。
在redis中token会在过期时间之后,被自动删除。
以上方案是针对幂等设计的。

如果是防重设计,流程图要改改:


 

更新完毕,欢迎一键三连,下方是作者的微信公众号,定期发布chatgpt等技术干货,欢迎免费订阅 。


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

相关文章

c++常见面试问题总结

c和C语言的区别 C语言是面向结构性语言,C是面向对象语言 c语言是c的子集,c包含了c语言的全部词法和语法内容,比c语言多出了类。 程序运行的保存的五个区 堆 栈 常量 全局变量 代码区 什么是面向对象:注重的是对象,当…

SQL语句

DDL 1.DDL 库 定义库:创建数据库 create database 数据库名; (数据库名要求:区分大小写,唯一性 ,不能使用关键字如create select;不能单独使用 的数字和特殊符号) 查看所有数据库:show databases; 选择/进入…

矿山尾矿库倾斜摄影三维建模

尾矿库现状调查是矿山安全生产工作的重要组成部分,也是监管部门关注的焦点。及时对尾矿库的现状进行调查,对存在的问题提出合理的整治方案,是控制尾矿库发生灾害的有效手段之一。本文以中维空间应用无人机倾斜摄影技术和三维激光扫描技术在某…

浙江数字孪生数字化工厂三维激光扫描建模_三维可视化管理平台_吉优赛维_三维建模解决方案_3D模型

作为工业4.0的标志之一,数字化工厂的建设趋势已经不可逆转了,而且很多企业也纷纷加入了这一行列当中。既要打造符合自己行业特色的数字化工厂,而且也要建造起符合自己未来盈利要求的工厂,于是在这种情况下三维扫描真正发挥了它的作…

那些与三维激光扫描有关的建模

文章目录 一、前言 二、正文 建模的方式 正向设计建模 参照点云数据逆向建模 粗略参照式逆向建模 精细参照式逆向建模 基于点云数据直接建模 基于照片建模 建模的目的 提升视觉及感观效果 附加属性信息 适用于承载平台 数据轻量化存储 打印输出 远离建模误区 见…

[数学建模]学习笔记1:初等建模

初等模型: 1.研究对象的机理比较简单 2.用静态,线性,确定性模型即可达到建模的目的 3.可以利用初等数学方法来构造和求解模型 注:尽量用简单的数学工具来建模 2.1 光盘的数据容量 调查和分析 经过编码的数字信息,以…

【三维激光扫描】第五章:基于点云数据的立面图绘制及三维建模

本文讲述CAD中加载点云并绘制立面图,然后在Sketchup中构建三维模型。 目 录 第一节 CAD绘制立面图 第二节 Sketchup三维模型构建

激光SLAM流程

1.激光数据处理(非常重要!!!) 激光运动畸变; 激光去运动畸变详解 轮式里程计的标定; 标定参数:轮子半径,两轮间距; 为什么标定:虽然出厂会给出参…

3D目标检测跟踪:激光雷达+视觉的目标级融合

论文:Visual-LiDAR based 3D Object Detection andTracking for Embedded Systems-IEEE Access 内容主要方法激光雷达地面滤波聚类Bounding box拟合跟踪 视觉雷达和视觉融合 总结 论文中激光检测方法是在原工作基础上改进的,可阅读论文Dynamic Multi-LiDAR Based Mu…

AMCL 激光测量模型

一、似然域模型 likelihood_field model 1、原理 它是一种“特设(ad hoc)”算法,不必计算相对于任何有意义的传感器物理生成模型的条件概率。而且,这种方法在实践中运行效果良好。即使在混乱的空间,得到的后验也更光滑,同时计算更…

Ansys Zemax | 使用OpticStudio进行闪光激光雷达系统建模(下)

在消费类电子产品领域,工程师可利用激光雷达实现众多功能,如面部识别和3D映射等。尽管激光雷达系统的应用非常广泛而且截然不同,而“闪存激光雷达”解决方案适用于在使用固态光学元件的目标场景中生成可检测的点阵列。 凭借在针对小型封装获…

相机+激光雷达重绘3D场景

将激光雷达与相机结合,再通过深度学习的方式获得场景的3D模型——Ouster首席执行官在博客中介绍了相机OS-1,并装有激光雷达。LiveVideoStack对原文进行了摘译。 文 / Angus Pacala. Ouster 译 / 王月美 技术审校 / 田栋 原文 https://medium.com/ouster…

2020年亚太杯数学建模竞赛赛题

https://download.csdn.net/download/Suger_Lover/46133529https://download.csdn.net/download/Suger_Lover/46133529https://download.csdn.net/download/Suger_Lover/46133529

PSpice仿真之建模-以半导体激光器为例

PSpice仿真之建模 第一篇原创博客,来点干货~最近应同学之托,解决一个PSpice建模问题,在解决过程中遇到很多问题,于是想写下来,后来者少走弯路哈。这里以半导体激光器为例,讲PSpice的建模。 PSpice是啥&am…

如何保证三维激光扫描的测量精度?

非接触式扫描是三维扫描技术中的一个重要分支,具有检测速度快、零接触等优势,可以将复杂、不规则的物体三维点云数据采集到电脑中,并快速构建出三维模型。如今,三维激光扫描测量技术在文物、建筑等行业都有了成功的应用案例。 在…

激光雷达应用案例|仓储3D体积量方测量

在物流、仓储等工业行业中,获取物品体积数量、掌握物品出入库情况对生产库存管理具有重要意义。 以煤炭仓储及生产领域煤炭体积测量为例,为了解煤炭出入库情况,通常依靠人力手持全站仪进行人工煤炭体积监测。然而这一传统解决方案始终面对着技…

数学建模——光盘的数据容量

1、背景和问题 (1)20世纪80年代出现激光唱片(CD)与激光视盘(LD),统称为光盘。 (2)20世纪90年代出现数字视频光盘(DVD)。 (3&#x…

管网三维激光扫描建模_BIM建模_可视化平台_吉优赛维数字孪生

这几年我国的能源领域已经得到了飞速的发展基础,基础建设也得到了长效的发展,那么现在在石油天然气的运输过程当中,是否已经做到了没有任何的后患之忧了呢?实际上现在的传统人工管理方式还是存在很大程度上的安全盲区的&#xff0…

LaserMaker激光建模软件V1.6.40 更新说明

尊敬的LaserMaker用户,LaserMaker进行了版本更新,新版本为V1.6.40,欢迎您下载使用 LaserMakerV1.6.40下载地址:LaserMaker 新增功能 1.打断线段 橡皮擦工具下新增打断线段功能,同一图案分别设置不同加工工艺更方便…

自制三维激光扫描建模

看图片就是我做的东西,很炫酷是不是。 好吧,开玩笑,这是电影普罗米修斯的截图。 当初看这个电影的时候就感觉这东西好眩酷,我能不能做出来。最近借着帮做毕业设计的机会我也做了一个。 就是这个丑丑的东西啦~ 首先感谢来自CSK的…