SpringBoot利用Redis實(shí)現(xiàn)防止訂單重復(fù)提交的解決方案
0. 前言
在涉及訂單操作的業(yè)務(wù)中,防止訂單重復(fù)提交是一個常見需求
用戶可能會因誤操作或網(wǎng)絡(luò)延遲而多次點(diǎn)擊提交訂單按鈕,導(dǎo)致訂單重復(fù)提交,造成數(shù)據(jù)冗余,而且訂單通常與庫存緊密關(guān)聯(lián),重復(fù)提交訂單不僅會影響用戶體驗(yàn),還有可能引發(fā)庫存管理上的混亂,甚至導(dǎo)致財務(wù)數(shù)據(jù)出現(xiàn)偏差,帶來一系列潛在的經(jīng)濟(jì)風(fēng)險
1. 常見的重復(fù)提交訂單的場景
- 網(wǎng)絡(luò)延遲:由于網(wǎng)絡(luò)問題,用戶在提交訂單后頁面沒有發(fā)生變化,而且沒有收到通知,用戶誤以為訂單沒有提交成功,連續(xù)點(diǎn)擊提交按鈕
- 刷新頁面:用戶提交訂單后刷新頁面,再次提交相同的訂單
- 用戶誤操作:用戶無意中點(diǎn)擊多次訂單提交按鈕
- 惡意攻擊:大量請求繞過前端頁面直接到達(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)識+商品唯一標(biāo)識+用戶唯一標(biāo)識
形成的字符串為 key、以任意一個字符串作為 value,將鍵值對保存到 Redis 中,并為鍵值對設(shè)置一個合理的過期時間(過期時間可以根據(jù)業(yè)務(wù)需求來設(shè)定,以確保在用戶完成訂單操作之前,鍵值對始終有效) - 訂單處理完成后的操作:一旦訂單成功支付或者被取消,我們就從 Redis 中刪除對應(yīng)的鍵,釋放占用的內(nèi)存資源,防止在鍵值對過期之前對訂單狀態(tài)產(chǎn)生誤判
key 的形式不唯一,但要確保一個 key 對應(yīng)一個訂單
當(dāng)客戶端發(fā)起提交訂單的請求時,后端會檢查 Redis 中是否存在對應(yīng)的鍵
- 如果存在,表明該訂單已經(jīng)被提交過,這是一個重復(fù)的提交請求,系統(tǒng)將拒絕此次請求,不會生成新的訂單
- 如果不存在,說明這是一個新的訂單提交請求,系統(tǒng)將繼續(xù)執(zhí)行訂單生成的流程,并存儲新的鍵值對到 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ù)提交,請勿重復(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 = '提交失敗,請重試。' console.error('error =', error); }) clearTimeout(timer) }, 500) } </script> </body> </html>
4. 需要注意的問題
- 如果在訂單生成過程中出現(xiàn)錯誤,要確保有一個機(jī)制能夠回滾之前的操作,比如刪除已經(jīng)插入 Redis 的鍵
- 避免因意外情況導(dǎo)致鍵未被及時清理,影響后續(xù)請求
- 如果處理的邏輯比較復(fù)雜,我們可以考慮使用通過切面(AOP)來解決,在切面中編寫防止訂單重復(fù)提交的代碼
以上就是SpringBoot利用Redis實(shí)現(xiàn)防止訂單重復(fù)提交的解決方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot Redis訂單重復(fù)提交的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Springboot使用POI實(shí)現(xiàn)導(dǎo)出Excel文件示例
本篇文章主要介紹了Springboot使用POI實(shí)現(xiàn)導(dǎo)出Excel文件示例,非常具有實(shí)用價值,需要的朋友可以參考下。2017-02-02淺談Java中Int、Integer、Integer.valueOf()、new Integer()之間的區(qū)別
本文主要介紹了淺談Java中Int、Integer、Integer.valueOf()、new Integer()之間的區(qū)別,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11SpringCloud實(shí)現(xiàn)灰度發(fā)布的方法步驟
本文主要介紹了SpringCloud實(shí)現(xiàn)灰度發(fā)布的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05Spring Boot Jar 包部署腳本的實(shí)例講解
在本篇文章里小編給大家整理的是一篇關(guān)于Spring Boot Jar 包部署腳本的實(shí)例講解內(nèi)容,對此有興趣的朋友們可以跟著學(xué)習(xí)下。2021-12-12spring-gateway filters添加自定義過濾器實(shí)現(xiàn)流程分析(可插拔)
這篇文章主要介紹了spring-gateway filters添加自定義過濾器實(shí)現(xiàn)流程分析(可插拔),本文通過實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2025-05-05