开发者

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. 需要注意的问题

    0. 前言

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

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

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

    1. 网络延迟:由于网络问题,用户在提交订单后页面没有发生变化,而且没有收到通知,用户误以为订单没有提交成功,连续点击提交按钮
    2. 刷新页面:用python户提交订单后刷新页面,再次提交相同的订单
    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 指令

    SpringBoot利用Redis实现防止订单重复提交的解决方案

    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

    SpringBoot利用Redis实现防止订单重复提交的解决方案

    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.sprhttp://www.devze.comingframework.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 busineandroidssId) {
            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 目录下

    SpringBoot利用Redis实现防止订单重复提交的解决方案

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>防止订单重复提交</title>
        <style>
            body, html {
                height: 100%;
                margin: 0;
                font-family: 'Arial', javascriptsans-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. 需要注意的问题

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

    以上就是SpringBoot利用Redis实现防止订单重复提交的解决方案的详细内容,更多关于SpringBoot Redis订单重复提交的资料请关注编程客栈(www.devze.com)其它相关文章!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜