SpringBoot利用Redis實(shí)現(xiàn)防止訂單重復(fù)提交的解決方案
0. 前言
在涉及訂單操作的業(yè)務(wù)中,防止訂單重復(fù)提交是一個(gè)常見需求
用戶可能會(huì)因誤操作或網(wǎng)絡(luò)延遲而多次點(diǎn)擊提交訂單按鈕,導(dǎo)致訂單重復(fù)提交,造成數(shù)據(jù)冗余,而且訂單通常與庫存緊密關(guān)聯(lián),重復(fù)提交訂單不僅會(huì)影響用戶體驗(yàn),還有可能引發(fā)庫存管理上的混亂,甚至導(dǎo)致財(cái)務(wù)數(shù)據(jù)出現(xiàn)偏差,帶來一系列潛在的經(jīng)濟(jì)風(fēng)險(xiǎn)
1. 常見的重復(fù)提交訂單的場(chǎng)景
- 網(wǎng)絡(luò)延遲:由于網(wǎng)絡(luò)問題,用戶在提交訂單后頁面沒有發(fā)生變化,而且沒有收到通知,用戶誤以為訂單沒有提交成功,連續(xù)點(diǎn)擊提交按鈕
- 刷新頁面:用戶提交訂單后刷新頁面,再次提交相同的訂單
- 用戶誤操作:用戶無意中點(diǎn)擊多次訂單提交按鈕
- 惡意攻擊:大量請(qǐng)求繞過前端頁面直接到達(dá)后端
2. 防止訂單重復(fù)提交的解決方案
2.1 前端(禁用按鈕)
用戶點(diǎn)擊提交訂單按鈕后,在成功跳轉(zhuǎn)到支付頁面之前,禁用提交訂單按鈕,防止用戶多次執(zhí)行提交訂單
禁用提交訂單按鈕只能避免一部分訂單重復(fù)提交的情況,如果用戶點(diǎn)擊支付按鈕之后刷新頁面,依然是可以重復(fù)下單的,要想完全解決訂單重復(fù)提交的問題,后端也要做相應(yīng)的處理
2.2 后端
我們可以借助 Redis 實(shí)現(xiàn)防止訂單重復(fù)提交的功能
- 生成訂單前的操作:在訂單生成之前,我們以
業(yè)務(wù)名+商家唯一標(biāo)識(shí)+商品唯一標(biāo)識(shí)+用戶唯一標(biāo)識(shí)形成的字符串為 key、以任意一個(gè)字符串作為 value,將鍵值對(duì)保存到 Redis 中,并為鍵值對(duì)設(shè)置一個(gè)合理的過期時(shí)間(過期時(shí)間可以根據(jù)業(yè)務(wù)需求來設(shè)定,以確保在用戶完成訂單操作之前,鍵值對(duì)始終有效) - 訂單處理完成后的操作:一旦訂單成功支付或者被取消,我們就從 Redis 中刪除對(duì)應(yīng)的鍵,釋放占用的內(nèi)存資源,防止在鍵值對(duì)過期之前對(duì)訂單狀態(tài)產(chǎn)生誤判
key 的形式不唯一,但要確保一個(gè) key 對(duì)應(yīng)一個(gè)訂單
當(dāng)客戶端發(fā)起提交訂單的請(qǐng)求時(shí),后端會(huì)檢查 Redis 中是否存在對(duì)應(yīng)的鍵
- 如果存在,表明該訂單已經(jīng)被提交過,這是一個(gè)重復(fù)的提交請(qǐng)求,系統(tǒng)將拒絕此次請(qǐng)求,不會(huì)生成新的訂單
- 如果不存在,說明這是一個(gè)新的訂單提交請(qǐng)求,系統(tǒng)將繼續(xù)執(zhí)行訂單生成的流程,并存儲(chǔ)新的鍵值對(duì)到 Redis 中,以防止后續(xù)的重復(fù)提交
3. 在SpringBoot項(xiàng)目中利用Redis實(shí)現(xiàn)防止訂單重復(fù)提交
本次演示的后端環(huán)境為: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 單機(jī))
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("訂單重復(fù)提交,請(qǐng)勿重復(fù)操作,您可以確認(rèn)一下有沒有未支付的相同訂單");
}
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>防止訂單重復(fù)提交</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; /* 設(shè)置最大寬度 */
padding: 50px 0;
display: flex;
flex-direction: column;
align-items: center;
}
.button-container, .result-container {
width: 100%;
max-width: 300px; /* 按鈕和結(jié)果顯示文本同寬 */
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 = () => {
// 點(diǎn)擊按鈕后有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 = '提交失敗,請(qǐng)重試。'
console.error('error =', error);
})
clearTimeout(timer)
}, 500)
}
</script>
</body>
</html>
4. 需要注意的問題
- 如果在訂單生成過程中出現(xiàn)錯(cuò)誤,要確保有一個(gè)機(jī)制能夠回滾之前的操作,比如刪除已經(jīng)插入 Redis 的鍵
- 避免因意外情況導(dǎo)致鍵未被及時(shí)清理,影響后續(xù)請(qǐng)求
- 如果處理的邏輯比較復(fù)雜,我們可以考慮使用通過切面(AOP)來解決,在切面中編寫防止訂單重復(fù)提交的代碼
以上就是SpringBoot利用Redis實(shí)現(xiàn)防止訂單重復(fù)提交的解決方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot Redis訂單重復(fù)提交的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
MyBatis常用的jdbcType數(shù)據(jù)類型
這篇文章主要介紹了MyBatis常用的jdbcType數(shù)據(jù)類型的相關(guān)資料,需要的朋友可以參考下2016-12-12
Mybatis-plus如何查詢返回對(duì)象內(nèi)有List<String>屬性
在使用Mybatis-Plus進(jìn)行開發(fā)時(shí),我們經(jīng)常會(huì)遇到需要處理一對(duì)多關(guān)系映射的情況,例如,查詢用戶數(shù)據(jù)時(shí),可能需要同時(shí)獲取該用戶管理的所有小區(qū)名稱列表,這要求我們?cè)诜祷氐膶?shí)體類中包含一個(gè)List<String>屬性,用于存放小區(qū)名稱,實(shí)現(xiàn)這一功能2024-10-10
SSH框架網(wǎng)上商城項(xiàng)目第21戰(zhàn)之詳解易寶支付的流程
這篇文章主要為大家詳細(xì)介紹了SSH框架網(wǎng)上商城項(xiàng)目第21戰(zhàn)之易寶支付的流程,感興趣的小伙伴們可以參考一下2016-06-06
解決SpringBoot打成jar運(yùn)行后無法讀取resources里的文件問題
這篇文章主要介紹了解決SpringBoot打成jar運(yùn)行后無法讀取resources里的文件問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08

