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

文章目录

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

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Web

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3.2 编写配置文件

application.yml(Redis 单机)

yaml 复制代码
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: 123456
      timeout: 5000ms
      database: 0

server:
  port: 10016

application.yml(Redis 集群)

yaml 复制代码
spring:
  data:
    redis:
      cluster:
        nodes: 127.0.0.1:6379

server:
  port: 10016

3.3 OrderService.java

利用 Redis 提供的 setnx 指令

java 复制代码
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

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

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 目录下

html 复制代码
<!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)来解决,在切面中编写防止订单重复提交的代码
相关推荐
爱笑的源码基地1 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
二宝哥2 小时前
CentOS 7.9 系统下 Redis 5.0.4 单机安装与源码编译安装指南
redis·centos
莫逸风3 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
隔窗听雨眠4 小时前
Spring Boot在云原生时代的编程范式革新研究
spring boot·后端·云原生
z123456789864 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
yaoxin5211235 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python
做个文艺程序员5 小时前
Linux第24篇:Java应用监控体系搭建:Prometheus+Grafana可视化运维
java·grafana·prometheus
小钻风33666 小时前
Spring Boot 文件上传详解:深入理解 MultipartFile 的使用与原理
java·开发语言
其美杰布-富贵-李6 小时前
Spring Boot 工程开发全流程说明
java·spring boot·后端
前端双越老师7 小时前
如何以前端视角(非0基础)学 Java ?
java·node.js·全栈