在SpringBoot项目中利用Redis实现防止订单重复提交

文章目录

  • 0. 前言
  • 1. 常见的重复提交订单的场景
  • 2. 防止订单重复提交的解决方案
    • 2.1 前端(禁用按钮)
    • 2.2 后端
  • 3. 在SpringBoot项目中利用Redis实现防止订单重复提交
    • 3.1 引入依赖
    • 3.2 编写配置文件
    • 3.3 OrderService.java
    • 3.4 OrderController.java
    • 3.5 index.html
  • 4. 需要注意的问题

阅读本文前可以先阅读我的另一篇博文: Windows环境下安装Redis并设置Redis开机自启

0. 前言

在涉及订单操作的业务中,防止订单重复提交是一个常见需求

用户可能会因误操作或网络延迟而多次点击提交订单按钮,导致订单重复提交,造成数据冗余,而且订单通常与库存紧密关联,重复提交订单不仅会影响用户体验,还有可能引发库存管理上的混乱,甚至导致财务数据出现偏差,带来一系列潜在的经济风险

1. 常见的重复提交订单的场景

  1. 网络延迟:由于网络问题,用户在提交订单后页面没有发生变化,而且没有收到通知,用户误以为订单没有提交成功,连续点击提交按钮
  2. 刷新页面:用户提交订单后刷新页面,再次提交相同的订单
  3. 用户误操作:用户无意中点击多次订单提交按钮
  4. 恶意攻击:大量请求绕过前端页面直接到达后端

2. 防止订单重复提交的解决方案

2.1 前端(禁用按钮)

用户点击提交订单按钮后,在成功跳转到支付页面之前,禁用提交订单按钮,防止用户多次执行提交订单

禁用提交订单按钮只能避免一部分订单重复提交的情况,如果用户点击支付按钮之后刷新页面,依然是可以重复下单的,要想完全解决订单重复提交的问题,后端也要做相应的处理

2.2 后端

我们可以借助 Redis 实现防止订单重复提交的功能

  • 生成订单前的操作:在订单生成之前,我们以业务名+商家唯一标识+商品唯一标识+用户唯一标识形成的字符串为 key、以任意一个字符串作为 value,将键值对保存到 Redis 中,并为键值对设置一个合理的过期时间(过期时间可以根据业务需求来设定,以确保在用户完成订单操作之前,键值对始终有效)
  • 订单处理完成后的操作:一旦订单成功支付或者被取消,我们就从 Redis 中删除对应的键,释放占用的内存资源,防止在键值对过期之前对订单状态产生误判

key 的形式不唯一,但要确保一个 key 对应一个订单

当客户端发起提交订单的请求时,后端会检查 Redis 中是否存在对应的键

  • 如果存在,表明该订单已经被提交过,这是一个重复的提交请求,系统将拒绝此次请求,不会生成新的订单
  • 如果不存在,说明这是一个新的订单提交请求,系统将继续执行订单生成的流程,并存储新的键值对到 Redis 中,以防止后续的重复提交

3. 在SpringBoot项目中利用Redis实现防止订单重复提交

本次演示的后端环境为:JDK 17.0.7 + SpringBoot 3.0.2

3.1 引入依赖

Redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Web

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3.2 编写配置文件

application.yml(Redis 单机)

spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: 123456
      timeout: 5000ms
      database: 0

server:
  port: 10016

application.yml(Redis 集群)

spring:
  data:
    redis:
      cluster:
        nodes: 127.0.0.1:6379

server:
  port: 10016

3.3 OrderService.java

利用 Redis 提供的 setnx 指令

在这里插入图片描述

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class OrderService {

    private final StringRedisTemplate stringRedisTemplate;

    public OrderService(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void generateToken(String key) {
        stringRedisTemplate.opsForValue().setIfAbsent(key, "uniqueTokenForOrder", 10, TimeUnit.MINUTES);
    }

    public boolean isOrderDuplicate(String token) {
        return Boolean.TRUE.equals(stringRedisTemplate.hasKey(token));
    }

}

3.4 OrderController.java

在这里插入图片描述

import cn.edu.scau.pojo.SubmitOrderDto;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/order")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping("/pay")
    public ResponseEntity<String> pay(@RequestBody SubmitOrderDto submitOrderDto) {
        String key = "order:" + submitOrderDto.getBusinessId() + ":" + submitOrderDto.getGoodsId() + ":" + submitOrderDto.getUserId();
        if (orderService.isOrderDuplicate(key)) {
            return ResponseEntity.ok("订单重复提交,请勿重复操作,您可以确认一下有没有未支付的相同订单");
        }

        orderService.generateToken(key);

        // 处理订单逻辑

        return ResponseEntity.ok("订单提交成功");
    }

}

SubmitOrderDto.java

public class SubmitOrderDto {

    private String businessId;

    private String goodsId;

    private String userId;

    public String getBusinessId() {
        return businessId;
    }

    public void setBusinessId(String businessId) {
        this.businessId = businessId;
    }

    public String getGoodsId() {
        return goodsId;
    }

    public void setGoodsId(String goodsId) {
        this.goodsId = goodsId;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    @Override
    public String toString() {
        return "SubmitOrderDto{" +
                "businessId='" + businessId + '\'' +
                ", goodsId='" + goodsId + '\'' +
                ", userId='" + userId + '\'' +
                '}';
    }

}

3.5 index.html

简单起见,本次演示前后端不分离,index.html 文件存放在 resources/static 目录下

在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>防止订单重复提交</title>
    <style>
        body, html {
            height: 100%;
            margin: 0;
            font-family: 'Arial', sans-serif;
            background-color: #f4f4f9;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .container {
            width: 100%;
            max-width: 400px; /* 设置最大宽度 */
            padding: 50px 0;
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        .button-container, .result-container {
            width: 100%;
            max-width: 300px; /* 按钮和结果显示文本同宽 */
            margin-bottom: 20px; /* 添加底部外边距 */
        }

        button {
            width: 276px;
            height: 67px;
            padding: 20px;
            font-size: 18px;
            color: #ffffff;
            background-color: #6a8eff;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            outline: none;
            transition: background-color 0.3s ease;
        }

        button:hover {
            background-color: #527bff;
        }

        #result {
            padding: 20px;
            font-size: 18px;
            color: #333333;
            background-color: #ffffff;
            border: 1px solid #e1e1e1;
            border-radius: 8px;
            text-align: center;
            box-sizing: border-box;
            width: 276px;
            height: 67px;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="button-container">
        <button onclick="submitOrder()">提交订单</button>
    </div>
    <div class="result-container" id="result"></div>
</div>

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
    const submitOrder = () => {
        // 点击按钮后有0.5秒的加载效果
        document.getElementById('result').innerText = '正在提交订单...'
        let timer = setTimeout(() => {
            axios
                .post('/order/pay', {
                    businessId: '123456',
                    goodsId: '123456',
                    userId: '123456'
                })
                .then((response) => {
                    console.log('response =', response);
                    document.getElementById('result').innerText = response.data
                })
                .catch((error) => {
                    document.getElementById('result').innerText = '提交失败,请重试。'
                    console.error('error =', error);
                })

            clearTimeout(timer)
        }, 500)
    }
</script>
</body>
</html>

4. 需要注意的问题

  1. 如果在订单生成过程中出现错误,要确保有一个机制能够回滚之前的操作,比如删除已经插入 Redis 的键
  2. 避免因意外情况导致键未被及时清理,影响后续请求
  3. 如果处理的逻辑比较复杂,我们可以考虑使用通过切面(AOP)来解决,在切面中编写防止订单重复提交的代码

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

HTML快速入门--第一节--五个基本标签

一、网络编程的三大基石 1.1 url 统一资源定位符&#xff1a; 网址:整个互联网中可以唯一且准确的确定一个资源的位置 (url项目外) 网址:https://www.baidu.com/ https://www.baidu.com/ 协议://ip端口/项目名/页面名 协议:交通法规获取资源 ip端口 &#xff08;域名&…

Java | Leetcode Java题解之第479题最大回文数乘积

题目&#xff1a; 题解&#xff1a; class Solution {public int largestPalindrome(int n) {if (n 1) {return 9;}int upper (int) Math.pow(10, n) - 1;int ans 0;for (int left upper; ans 0; --left) { // 枚举回文数的左半部分long p left;for (int x left; x >…

Redis实现全局ID生成器

全局ID生成器 为什么要用全局ID生成器 1.当我们使用数据库自增来实现id的生成时,规律过于明显,会给用户暴露很多信息 2.当我们订单量过大时无法用数据库的一张表来存放订单,如果两张表的id都是自增的话,id就会出现重复 什么是全局ID生成器 全局ID生成器,是一种在分布式系统…

LabVIEW提高开发效率技巧----用户权限控制

在LabVIEW开发中&#xff0c;用户权限控制是一个重要的设计模块&#xff0c;尤其在多用户系统中&#xff0c;它可以确保数据安全并控制不同用户的操作权限。为了实现用户权限控制&#xff0c;可以通过角色与权限管理模块来进行设计和实施。以下将从多个角度详细说明如何在LabVI…

房子,它或许是沃土

刚成家&#xff0c;来客时&#xff0c;它是客房 成家后&#xff0c;没小孩&#xff0c;它是书房 有小孩&#xff0c;未分房&#xff0c;它暂且是书房 孩子大些&#xff0c;它是孩子们埋下梦想种子&#xff0c;生根发芽的地方

基于SSM果蔬经营系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;商品信息管理&#xff0c;类型管理&#xff0c;系统管理&#xff0c;订单管理 前台账号功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;商品信息&#xff0c;广告…

微信小程序:miniprogram-ci自动打包工具使用介绍以及支持配置环境变量、jekins打包、taro、uni-app三方工具

微信小程序&#xff1a;miniprogram-ci自动打包工具使用介绍以及支持配置环境变量、jekins打包、taro、uni-app三方工具 背景介绍 一直都是本地电脑运行微信开发者工具打包上传。多项目中新老版本对node版本要求不一致&#xff0c;老是切来切去。而且同一个人开发上传需要打包…

揭秘Map与Set的键值奥秘与集合魅力,解锁高效数据魔法

文章目录 前言➰一、关联式容器1.1 关联式容器的概述1.2 关联式容器的工作原理1.3 关联式容器的核心特性 ➰二、键值对2.1 键值对的基本概念2.2 键值对在C中的实现 ➰三、树形结构的关联式容器3.1 树形结构的特点3.2 使用场景 ➰四、set的使用与定义4.1 set的基本特性4.2 set的…

centOS实用命令

一、查看进程&#xff0c;端口占用 netstat命令(window和linux通用&#xff0c;细节不同) 查看端口占用(linux) netstat -ano |grep 8080查看端口占用(window) netstat -ano |findstr 8080ps命令 可以直接使用ps aux查看所有用户的进程信息 一些参数 参数解释-p根据进程P…

【D3.js in Action 3 精译_034】4.1 D3 中的坐标轴的创建(中篇):定义横纵坐标轴的比例尺

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第一部分 D3.js 基础知识 第一章 D3.js 简介&#xff08;已完结&#xff09; 1.1 何为 D3.js&#xff1f;1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践&#xff08;上&#xff09;1.3 数据可…

企业资产安全之数据防泄密要领

在数字化时代&#xff0c;数据已成为企业最宝贵的资产之一。然而&#xff0c;随着数据价值的增加&#xff0c;数据泄露的风险也随之上升。从内部员工的无意泄露到外部黑客的恶意攻击&#xff0c;企业数据安全面临着前所未有的挑战。SDC沙盒数据防泄密解决方案&#xff0c;正是为…

用 Python 构建高级配对交易策略

作者&#xff1a;老余捞鱼 原创不易&#xff0c;转载请标明出处及原作者。 写在前面的话&#xff1a; 本文阐述通过分析加密货币和传统金融工具之间的相关性和协整性&#xff0c;以及实施 Z-score 方法来生成交易信号&#xff0c;然后介绍如何使用 Python 构建配对交易策…

无人机搭载激光雷达在地形测绘中的多元应用

一、高精度地形测量 无人机激光雷达能够发射激光脉冲并接收其回波&#xff0c;通过精确计算激光脉冲的往返时间来确定目标物的距离。这一特性使得无人机激光雷达在地形测绘中能够实现高精度的三维地形测量。通过快速获取大量地形数据&#xff0c;可以生成高精度的数字高程模型…

VScode背景更改

效果 实现方法 第0步 以管理员身份运行VScode 首先 需要安装这个扩展 然后 接下来 找到配置文件 再后来 在配置文件的下面但不超过最后一个大括号的地方加入以下内容 "update.enableWindowsBackgroundUpdates": true,"background.fullscreen": {…

Gee引擎配置微端后登录游戏黑屏怎么办?

GEE引擎配置微端后登录游戏黑屏怎么办&#xff1f;今天飞飞和你们分享GEE引擎配置微端后游戏黑屏的解决办法&#xff0c;希望可以帮助到你~ 1、端口不对 微端没有更新&#xff0c;玩家进入游戏是地图跟装备都看不见&#xff0c;是漆黑的&#xff0c;微端显示连接失败&#xff…

Leecode刷题之路第26天之删除有序数组中的重复项

题目出处 26-删除有序数组中的重复项-题目出处 题目描述 给你一个 非严格递增排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元…

鸿蒙网络编程系列31-使用RCP调用OpenAI接口实现智能助手

简介 在OpenAI推出GPT系列大模型以后&#xff0c;市场上各种类似的大模型也层出不穷&#xff0c;这些大模型也基本都会兼容OpenAI的接口&#xff0c;在开发基于大模型的应用时&#xff0c;选择使用OpenAI接口作为和后端大模型通讯的标准&#xff0c;可以更好的适配不同厂家的模…

2024年五一杯数学建模C题煤矿深部开采冲击地压危险预测求解全过程论文及程序

2024年五一杯数学建模 C题 煤矿深部开采冲击地压危险预测 原题再现&#xff1a; “煤炭是中国的主要能源和重要的工业原料。然而&#xff0c;随着开采深度的增加&#xff0c;地应力增大&#xff0c;井下煤岩动力灾害风险越来越大&#xff0c;严重影响着煤矿的安全高效开采。在…

一个人如何开发一款App软件

个人开发软件和公司开发软件不一样&#xff0c;其中就是收费上&#xff0c;个人开发的费用低&#xff0c;售后服务态度好啊。一个人负责开发也负责售后&#xff0c;客户就你一个。一般都是工作室和个人接单的多&#xff0c;不是太大的项目就建议是个人开发吧&#xff0c;因为能…

网络编程(21)——通过beast库快速实现http服务器

目录 二十一、day21 1. 头文件和作用域重命名 2. reponse时调用的一些函数 3. http_connection a. 构造函数 b. start() c. process_request() d. create_response() e. create_post_response() f. write_response() 4. Server 5. 主函数 6. 测试 1&#xff09;测…