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

五、實(shí)戰(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;
// 校驗(yàn)token是否存在
Boolean exists = redisTemplate.hasKey(key);
if (Boolean.FALSE.equals(exists)) {
throw new BusinessException("請勿重復(fù)提交");
}
// 刪除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("請勿重復(fù)點(diǎn)擊");
}
try {
// TODO: 創(chuàng)建訂單邏輯
return "下單成功";
} finally {
redisTemplate.delete(lockKey);
}
}
}
六、最佳實(shí)踐經(jīng)驗(yàn)總結(jié)
| 項(xiàng)目 | 建議 |
|---|---|
| Token有效期 | 建議 5~10 分鐘 |
| 鎖粒度 | 以用戶ID 或 業(yè)務(wù)單號為單位 |
| 冪等Key傳遞方式 | 推薦放在 Header 中,例如 Idempotency-Token |
| 數(shù)據(jù)庫層兜底 | 唯一約束防止極端情況 |
| 日志 | 建議記錄 Token 與鎖狀態(tài),方便排查 |
一句話總結(jié):
Redis防快點(diǎn),數(shù)據(jù)庫防慢點(diǎn),前端防亂點(diǎn)。
七、總結(jié)
| 層級 | 手段 | 優(yōu)點(diǎn) | 備注 |
|---|---|---|---|
| 前端 | 按鈕防抖 | 用戶體驗(yàn)好 | 僅表層防護(hù) |
| Redis | Token + Lock | 性能高 | 推薦主力方案 |
| 數(shù)據(jù)庫 | 唯一約束 | 穩(wěn)定 | 最終兜底 |
最終結(jié)論:
沒有銀彈,只有多層防御。
前端防抖 + 后端冪等 + 數(shù)據(jù)庫約束 = 才是真正的生產(chǎn)級防重復(fù)方案。
寫在最后
防重復(fù)提交這事,說簡單也簡單,說復(fù)雜也復(fù)雜。
它考驗(yàn)的不是寫代碼的能力,而是你對業(yè)務(wù)一致性和系統(tǒng)冪等性的理解。
下次再有人問你“為什么我點(diǎn)兩次下單按鈕扣了兩次錢”,
你可以淡定地說一句——“兄弟,我們系統(tǒng)現(xiàn)在有冪等鎖,不怕你點(diǎn)三次。”
到此這篇關(guān)于SpringBoot結(jié)合Redis實(shí)現(xiàn)防止重復(fù)提交訂單的實(shí)戰(zhàn)指南的文章就介紹到這了,更多相關(guān)SpringBoot防止重復(fù)提交訂單內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot項(xiàng)目中定時器的實(shí)現(xiàn)示例
在Spring?Boot項(xiàng)目中,你可以使用Spring框架提供的@Scheduled注解來編寫定時任務(wù),本文就來介紹一下SpringBoot項(xiàng)目中定時器的實(shí)現(xiàn),感興趣的可以了解一下2023-11-11
eclipse中沒有SERVER的解決辦法(超詳細(xì))
使用eclipse進(jìn)行tomcat配置時,經(jīng)常會發(fā)現(xiàn)一個重要的問題就是打開eclipse之后沒有了server選項(xiàng),所以本給大家詳細(xì)介紹了eclipse中沒有SERVER的解決辦法,文中有詳細(xì)的圖文講解,需要的朋友可以參考下2023-12-12
MyBatis環(huán)境資源配置實(shí)現(xiàn)代碼詳解
這篇文章主要介紹了MyBatis環(huán)境資源配置實(shí)現(xiàn)代碼解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-08-08
java實(shí)現(xiàn)微信小程序加密數(shù)據(jù)解密算法
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)微信小程序加密數(shù)據(jù)解密算法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-09-09
SpringBoot數(shù)據(jù)脫敏的實(shí)現(xiàn)示例
數(shù)據(jù)脫敏主要應(yīng)用在客戶安全數(shù)據(jù)或商業(yè)性敏感數(shù)據(jù)的情況,本文主要介紹了SpringBoot數(shù)據(jù)脫敏的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-05-05

