后端怎么防止重复提交订单

article/2025/8/20 11:15:27

前言

接口幂等性问题,对于开发人员来说,是一个跟语言无关的公共问题。本文分享了一些解决这类问题非常实用的办法,绝大部分内容我在项目中实践过的,给有需要的小伙伴一个参考。

不知道你有没有遇到过这些场景:

有时我们在填写某些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操作。

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

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;

具体流程如下:

2ab977f3eb2c4de2a2deb1539c5eb513.png
具体步骤: 

多个请求同时根据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值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。

具体流程如下:

a19bd91e17944ef696ac3b753b74b2cc.png 

具体步骤:

先根据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异常。

具体流程图如下:

f4a63206e0484ddab8d286fc62d19203.png
具体步骤: 

用户通过浏览器发起请求,服务端收集数据。
将该数据插入mysql
判断是否执行成功,如果成功,则操作其他数据(可能还有其他的业务逻辑)。


如果执行失败,捕获唯一索引冲突异常,直接返回成功。


5. 建防重表

有时候表中并非所有的场景都不允许产生重复的数据,只有某些特定场景才不允许。这时候,直接在表中加唯一索引,显然是不太合适的。

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

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

具体流程图如下:

0e9b8263e9bc4ff1a6614403156cbf73.png
具体步骤: 

用户通过浏览器发起请求,服务端收集数据。
将该数据插入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时,接口也可以直接返回成功。

具体流程图如下:

59b8a303b03e47f3a57321f3c36c76af.png
具体步骤: 

用户通过浏览器发起请求,服务端收集数据。


根据id和当前状态作为条件,更新成下一个状态
判断操作影响行数,如果影响了1行,说明当前操作成功,可以进行其他数据操作。


如果影响了0行,说明是重复请求,直接返回成功。
主要特别注意的是,该方案仅限于要更新的表有状态字段,并且刚好要更新状态字段的这种特殊情况,并非所有场景都适用。
7. 加分布式锁

其实前面介绍过的加唯一索引或者加防重表,本质是使用了数据库的分布式锁,也属于分布式锁的一种。但由于数据库分布式锁的性能不太好,我们可以改用:redis或zookeeper。

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

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

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

具体流程图如下:

e6985c78bb554ecfb80728f923ca0c4b.png
具体步骤: 

用户通过浏览器发起请求,服务端会收集数据,并且生成订单号code作为唯一业务字段。


使用redis的set命令,将该订单code设置到redis中,同时设置超时时间。


判断是否设置成功,如果设置成功,说明是第一次请求,则进行数据操作。


如果设置失败,说明是重复请求,则直接返回成功。
需要特别注意的是:分布式锁一定要设置一个合理的过期时间,如果设置过短,无法有效的防止重复请求。如果设置过长,可能会浪费redis的存储空间,需要根据实际业务情况而定。
 

8. 获取token

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

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

第一步,先获取token。

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

e0f20d958ede4febab809a53c501d01e.png
具体步骤: 

用户访问页面时,浏览器自动发起获取token请求。
服务端生成token,保存到redis中,然后返回给浏览器。


用户通过浏览器发起请求时,携带该token。


在redis中查询该token是否存在,如果不存在,说明是第一次请求,做则后续的数据操作。


如果存在,说明是重复请求,则直接返回成功。
在redis中token会在过期时间之后,被自动删除。
以上方案是针对幂等设计的。

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

893caa3fafa844598221bc6a1cb2b1cb.png
需要特别注意的是:token必须是全局唯一的。


最后说一句(求关注,别白嫖我) 


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

相关文章

bootstrap订单提交页面

下载地址 基于bootstrap实现的订单提交页面,常见的电商购物网站订单确认提交页面。 dd:

移动端-确认订单页面

项目准备 lib文件中存放外来的文件,就比如这个项目使用到字体图标,那存放的就是字体图标的文件,css 样式,images 重要的图片,uploads 页面随时更新的图片,其次就是html文件。 base.css *{margin: 0;pad…

confirm-order提交订单

目录 顶部导航条:复用head组件新增收货地址订单收货地址页面顶部导航条:复用head组件无地址地址列表新增地址 增加收货地址add_address顶部导航条:复用head组件地址信息表单其他组件 送达时间商店商品底部弹出消息:复用alertTip组…

订单。。。

一、库存扣减和订单表不一致 1、网络抖动—网速是好是坏,不稳定。最大延迟与最小延迟的时间差,如最大延迟是20毫秒,最小延迟为5毫秒,那么网络抖动就是15毫秒 2、库存数据不一致的原因: 1)事务性的问题 – …

实现提交订单的功能

根据购物车中的商品名称和数量生成了结算信息&#xff0c;并可以填写收货人姓名、联系电话和收货地址&#xff0c;本任务将实现提交订单的功能。 一、创建订单页面order.jsp <% page language"java" import"java.util.*" pageEncoding"UTF-8"…

美多商城项目:结算订单与提交订单

一、结算订单 1. 结算订单逻辑分析 结算订单是从Redis购物车中查询出被勾选的商品信息进行结算并展示。 2. 结算订单接口设计和定义 1.请求方式 选项方案请求方法GET请求地址/orders/settlement/ 3. 结算订单后端逻辑实现 class OrderSettlementView(LoginRequiredMixin, Vie…

电商系统-提交订单并发处理

在多个用户同时发起对一个商品的下单请求时&#xff0c;先查询商品库存&#xff0c;再修改商品库存&#xff0c;会出现资源竞争问题&#xff0c;导致库存的最终结果出现异常。 1、并发下单问题演示 每个不同的用户在程序上&#xff0c;我们可以理解成不同的线程&#xff0c;每…

Android Studio_Toast消息提醒

Android Studio_Toast消息提醒 1、Toast是Android系统提供的一种非常简洁的消息提醒方式&#xff0c;程序中可以使用它实现将短小的消息通知给用户&#xff0c;一点时间后自动消失&#xff0c;且不占用屏幕的任何空间。 2、Toast用法其实非常简单&#xff0c;通过静态方法make…

Vue 消息提示通知的几种方式汇总

Vue 消息提示通知组件&#xff08;Message /Notification&#xff09;是我们日常开发中经常使用的组件&#xff0c;它可用作与用户交互的反馈提示&#xff0c;信息提交成功、错误、操作警告等场景使用。原生JavaScript 提供了 alert、prompt、confirm 等方法 提示框1>Messag…

java信息提醒怎么实现_jsp怎么实现消息提醒

如果你是平台级别的系统,可以考虑消息队列的中间件,例如:阿里巴巴的rocketmq,用这个来做消息订阅与分发。 如果你只是简简单单的需要提示到web(jsp)页面,可以用js定时ajax访问后台,后台来确定是否有数据更新,无论这个数据是哪来的。 推荐课程:Java教程。 这里就使用在JSP页面…

RabbitMQ真延时队列实现消息提醒功能

RabbitMQ真延时队列实现消息提醒功能 一、需求场景 用户可以制定多个计划&#xff0c;同时可给该计划设置是否需要到点提醒&#xff0c;且中途可以取消提醒或修改提醒时间。 二、需要解决的问题 学习过rabbitmq的同学们都知道&#xff0c;通过TTL死信队列可以实现延时队列的…

企业微信 消息 html,企业微信怎么设置消息提醒

企业微信是一款非常不错的办公软件&#xff0c;用户加入企业群就能实时了解企业的动态。而且大家只需设置消息提醒&#xff0c;软件就会在第一时间通知你&#xff0c;不会让你错过任何重要的消息&#xff0c;下面小编为大家带来相关的设置教程。 方法/步骤分享&#xff1a; 1、…

vue websocket 新消息提醒

概述&#xff1a; 不是当前聊天&#xff0c;有其他消息来就通过2种方式接受到提醒。在连接的上下文中判断&#xff0c;符合条件的弹框&#xff0c;显示红点&#xff0c;此处调用了element弹框组件列表点击事件&#xff0c;红点消失列表显示&#xff0c;属性中包含小红点 前提…

html5载入提示音,html5新消息提示声音

【实例简介】 【实例截图】 【核心代码】HTML5手机声音提示 #chatBox{width:400px;border:1px solid #d3d3d3;margin:50px auto;} #chat {max-height:220px;overflow-y:auto;max-width:400px;} #chat > ul > li{padding:3px;clear:both;padding:4px;margin:10px 0px 5px …

【Android】消息提示notification

notification 1、notification消息提示 由Android系统来管理和维护的&#xff0c;因此用户可以随时进入查看。某些信息不需要用户马上处理&#xff0c;可以利用通知&#xff0c;即延迟消息&#xff0c;比如软件的更新、短信、新闻等。 2、消息包含的内容 3、代码 <Button…

消息提醒系统:设计模式与实现方案 (公告(通告)、消息、提醒等基本功能数据库表设计与实现)

参考地址&#xff1a; 公告(通告),消息,提醒等基本功能数据库表设计_DamonREN的博客-CSDN博客 多种消息提醒系统的设计模式、实现方案&#xff08;附功能截图表结构&#xff09;_黑夜的风的博客-CSDN博客_消息提醒 设计一个百万级的消息推送系统 - crossoverJie - 博客园 案…

android开发 app消息提醒功能,APP消息提醒设计:ios和android的最佳设计方案 – 25学堂...

我们都知道APP一项重要功能就是消息推送,那么通知栏的设计极大程度上反应了这个APP是否合理,那如何可以方便地为用户展示各种通知内容。也就将是我们APP设计师跟APP产品经理重点思考的问题?也要关注移动APP布局设计经验之道! 自从去年发布的iOS5中也引入了这一功能,以替代…

vue浏览器消息提示

vue浏览器消息提示 JS部分 //判断浏览器是否支持浏览器消息弹窗 suportNotify() {if (window.Notification) {// 支持console.log("支持" "Web Notifications API");//如果支持Web Notifications API&#xff0c;再判断浏览器是否支持弹出实例this.show…

html如何设置提示收到消息,从零开始实现一个消息提示框

引言 消息提示框在实际应用场景当中比较常见,最常用的就是element ui的消息提示框,我们通常都是直接使用它们,但是我们有没有尝试过去探究其实现原理,并自己动手实现呢?为了提升我们的个人能力和竞争力,我们可以尝试来实现这样一个消息提示框。 实现效果 我们来查看一下最…

多种消息提醒系统的设计模式、实现方案(附功能截图+表结构)

网站需要增加3种消息提醒系统。需要实现的功能如下&#xff1a; 1.评论提醒。 实现功能 他人回复自己后&#xff0c;右上角自动提醒“未阅读的新消息”的数量。 点击后&#xff0c;清空新消息的提示。 思路 这个是最简单的。在数据库查询&#xff1a; select count(id) …