SpringBoot利用Redis实现防止订单重复提交的解决方案
作者:聂 可 以
在涉及订单操作的业务中,防止订单重复提交是一个常见需求,用户可能会因误操作或网络延迟而多次点击提交订单按钮,导致订单重复提交,所以本文给大家介绍了SpringBoot利用Redis实现防止订单重复提交的解决方案,需要的朋友可以参考下
0. 前言
在涉及订单操作的业务中,防止订单重复提交是一个常见需求
用户可能会因误操作或网络延迟而多次点击提交订单按钮,导致订单重复提交,造成数据冗余,而且订单通常与库存紧密关联,重复提交订单不仅会影响用户体验,还有可能引发库存管理上的混乱,甚至导致财务数据出现偏差,带来一系列潜在的经济风险
1. 常见的重复提交订单的场景
- 网络延迟:由于网络问题,用户在提交订单后页面没有发生变化,而且没有收到通知,用户误以为订单没有提交成功,连续点击提交按钮
- 刷新页面:用户提交订单后刷新页面,再次提交相同的订单
- 用户误操作:用户无意中点击多次订单提交按钮
- 恶意攻击:大量请求绕过前端页面直接到达后端
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. 需要注意的问题
- 如果在订单生成过程中出现错误,要确保有一个机制能够回滚之前的操作,比如删除已经插入 Redis 的键
- 避免因意外情况导致键未被及时清理,影响后续请求
- 如果处理的逻辑比较复杂,我们可以考虑使用通过切面(AOP)来解决,在切面中编写防止订单重复提交的代码
以上就是SpringBoot利用Redis实现防止订单重复提交的解决方案的详细内容,更多关于SpringBoot Redis订单重复提交的资料请关注脚本之家其它相关文章!