SpringBoot結合Redis實現(xiàn)防止重復提交訂單的實戰(zhàn)指南
一次點擊,下單成功;兩次點擊,客服崩潰。
重復提交訂單的問題,幾乎每個后端都遇到過。今天咱們就從底層邏輯到實戰(zhàn)代碼,聊聊這個老生常談卻又暗藏玄機的話題。
一、背景:為什么會重復提交?
先說場景。
用戶點了一次“立即支付”,發(fā)現(xiàn)沒反應,再點一次,結果悲劇發(fā)生:
- 生成了兩筆訂單;
- 扣了兩次庫存;
- 財務對賬炸了鍋。
這還只是正常用戶,有的還會:
- 前端按鈕沒禁用,點個爽;
- 支付網(wǎng)關超時自動重試;
于是,后端同學開始加鎖、加冪等、加約束,最后一看,代碼又胖了一圈。
二、常見的防重復提交方案
其實防重復提交的核心目標就一句話:
在一定時間內,確保同一個請求只被處理一次。
從架構層次看,可以分為:
| 層次 | 方案 | 優(yōu)點 | 缺點 |
|---|---|---|---|
| 前端層 | 按鈕防抖 / 禁用 | 簡單直觀 | 不可靠,易被跳過 |
| 網(wǎng)關層 | 冪等性Key | 通用 | 實現(xiàn)復雜 |
| 服務層 | Redis分布式鎖 | 高性能 | 需要良好鎖管理 |
| 數(shù)據(jù)庫層 | 唯一索引 / 狀態(tài)判斷 | 最穩(wěn) | 執(zhí)行慢,影響性能 |
實際生產中,一般會前后端雙保險:
前端防抖 + 后端冪等控制。
三、服務端防重復提交的常見思路
來點干貨。
后端層面常用的方案主要有三種。
方案一:冪等性 Token 機制(推薦)
核心邏輯:
- 前端先調用
/token接口,獲取一個隨機token; - 請求下單時攜帶該 token;
- 后端校驗 token 是否已使用;
- 沒用過 → 處理業(yè)務并標記“已使用”;用過 → 拒絕請求。
非常適合「表單類接口」或「下單支付類接口」。
偽流程
前端請求 token -> 緩存保存 token -> 請求接口時攜帶 token -> 校驗通過一次后刪除 token
方案二:Redis 分布式鎖機制
使用 Redis 的 SETNX(或 Redisson)實現(xiàn)業(yè)務級互斥鎖:
boolean lock = redis.setIfAbsent(key, "1", 5, TimeUnit.SECONDS);
if (!lock) {
throw new BusinessException("請勿重復提交");
}
try {
createOrder();
} finally {
redis.delete(key);
}
比如 key 可設計為:
lock:order:create:userId:123
優(yōu)點:
- 性能高
- 實現(xiàn)簡單
- 適合短時間的防重需求
缺點:
需要注意鎖過期與釋放問題。
方案三:數(shù)據(jù)庫層冪等約束
老實人的方案——數(shù)據(jù)庫唯一索引。
ALTER TABLE t_order ADD UNIQUE KEY uk_order_no (order_no);
或者邏輯判斷:
if (orderRepository.existsByOrderNo(orderNo)) {
log.info("訂單重復提交: {}", orderNo);
return;
}
優(yōu)點:穩(wěn)如老狗。
缺點:性能不高,適合作為兜底。
四、綜合方案流程圖
下面是一個比較推薦的方案:
冪等Token + Redis鎖 雙層防護

五、實戰(zhàn)代碼(Spring Boot)
1. 獲取冪等 Token 接口
@RestController
@RequestMapping("/api/token")
public class TokenController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/generate")
public String generateToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("token:" + token, "1", 10, TimeUnit.MINUTES);
return token;
}
}
2. 下單接口(冪等 + Redis鎖)
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Autowired
private StringRedisTemplate redisTemplate;
@PostMapping("/create")
public String createOrder(@RequestHeader("Idempotency-Token") String token,
@RequestBody OrderRequest request) {
String key = "token:" + token;
// 校驗token是否存在
Boolean exists = redisTemplate.hasKey(key);
if (Boolean.FALSE.equals(exists)) {
throw new BusinessException("請勿重復提交");
}
// 刪除token,確保一次性
redisTemplate.delete(key);
// 加鎖防并發(fā)
String lockKey = "lock:order:create:" + request.getUserId();
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(locked)) {
throw new BusinessException("請勿重復點擊");
}
try {
// TODO: 創(chuàng)建訂單邏輯
return "下單成功";
} finally {
redisTemplate.delete(lockKey);
}
}
}
六、最佳實踐經(jīng)驗總結
| 項目 | 建議 |
|---|---|
| Token有效期 | 建議 5~10 分鐘 |
| 鎖粒度 | 以用戶ID 或 業(yè)務單號為單位 |
| 冪等Key傳遞方式 | 推薦放在 Header 中,例如 Idempotency-Token |
| 數(shù)據(jù)庫層兜底 | 唯一約束防止極端情況 |
| 日志 | 建議記錄 Token 與鎖狀態(tài),方便排查 |
一句話總結:
Redis防快點,數(shù)據(jù)庫防慢點,前端防亂點。
七、總結
| 層級 | 手段 | 優(yōu)點 | 備注 |
|---|---|---|---|
| 前端 | 按鈕防抖 | 用戶體驗好 | 僅表層防護 |
| Redis | Token + Lock | 性能高 | 推薦主力方案 |
| 數(shù)據(jù)庫 | 唯一約束 | 穩(wěn)定 | 最終兜底 |
最終結論:
沒有銀彈,只有多層防御。
前端防抖 + 后端冪等 + 數(shù)據(jù)庫約束 = 才是真正的生產級防重復方案。
寫在最后
防重復提交這事,說簡單也簡單,說復雜也復雜。
它考驗的不是寫代碼的能力,而是你對業(yè)務一致性和系統(tǒng)冪等性的理解。
下次再有人問你“為什么我點兩次下單按鈕扣了兩次錢”,
你可以淡定地說一句——“兄弟,我們系統(tǒng)現(xiàn)在有冪等鎖,不怕你點三次。”
到此這篇關于SpringBoot結合Redis實現(xiàn)防止重復提交訂單的實戰(zhàn)指南的文章就介紹到這了,更多相關SpringBoot防止重復提交訂單內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
MyBatis環(huán)境資源配置實現(xiàn)代碼詳解
這篇文章主要介紹了MyBatis環(huán)境資源配置實現(xiàn)代碼解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-08-08
java實現(xiàn)微信小程序加密數(shù)據(jù)解密算法
這篇文章主要為大家詳細介紹了java實現(xiàn)微信小程序加密數(shù)據(jù)解密算法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-09-09
SpringBoot數(shù)據(jù)脫敏的實現(xiàn)示例
數(shù)據(jù)脫敏主要應用在客戶安全數(shù)據(jù)或商業(yè)性敏感數(shù)據(jù)的情況,本文主要介紹了SpringBoot數(shù)據(jù)脫敏的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-05-05

