当前位置: 首页 > article >正文

在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)来解决,在切面中编写防止订单重复提交的代码

http://www.kler.cn/news/365948.html

相关文章:

  • 隨筆 20241024 Kafka 数据格式解析:批次头与数据体
  • Go 语言中的 for range 循环教程
  • 万字图文实战:从0到1构建 UniApp + Vue3 + TypeScript 移动端跨平台开源脚手架
  • 学习webservice的心得
  • 安全见闻---清风
  • 浅谈c#编程中的异步编程
  • java springboot项目如何计算经纬度在围栏内以及坐标点距离
  • SMT 生产可视化:提升电子组装流程效率
  • 常用排序算法总结
  • GPS/北斗时空安全隔离装置(卫星时空防护装置)使用手册
  • 计算机视觉篇---图像分类实战+理论讲解(6)Mobilenet
  • 数据结构入门之复杂度
  • 数据结构与算法:贪心与相关力扣题455.分发饼干、376.摆动序列、53.最大子数组和(贪心+动态规划dp)、122.买卖股票的最佳时机Ⅱ
  • 25届电信保研经验贴(自动化所)
  • 基于 STM32 单片机的智能门禁系统创新设计
  • Java从List中删除元素的几种方式
  • 【C语言刷力扣】441.排列硬币
  • 基于行业分类的目标检测与跟踪系统
  • .NET 8 Web API从基础到提高全面示例
  • 电脑技巧:Rufus——最佳USB启动盘制作工具指南
  • 【代码随想录Day51】图论Part03
  • 第五十五章 安全元素的详细信息 - ReferenceList 详情
  • 可能是NextJs(使用ssr、api route)打包成桌面端(nextron、electron、tauri)的最佳解决方式
  • 浅谈人工智能之基于LLaMA-Factory进行Llama3微调
  • 2024.7最新子比主题zibll7.9.2开心版源码+授权教程
  • 08 设计模式-结构型模式-过滤器模式