如何防止重復(fù)提交訂單?
作者:Java后端開發(fā)工程師
一、背景介紹:為什么會(huì)產(chǎn)生重復(fù)提交?
在電商平臺(tái)中,用戶提交訂單是一個(gè)非常敏感的動(dòng)作。這通常涉及:
- 庫存扣減
- 優(yōu)惠券核銷
- 支付下單
- 消息發(fā)送
但用戶總喜歡:
- 點(diǎn)兩次“提交訂單”按鈕
- 網(wǎng)絡(luò)卡頓時(shí)刷新頁面
- 使用瀏覽器回退再次提交
結(jié)果就是:重復(fù)提交訂單,造成資源浪費(fèi),甚至業(yè)務(wù)損失!
二、問題分析:重復(fù)提交的常見場景
場景 | 示例 |
---|---|
用戶行為 | 多次點(diǎn)擊按鈕、瀏覽器刷新 |
接口冪等性差 | 接口無冪等校驗(yàn),每次都生成新訂單 |
網(wǎng)絡(luò)重試 | 客戶端自動(dòng)重發(fā)請(qǐng)求(如超時(shí)) |
分布式系統(tǒng) | 多個(gè)節(jié)點(diǎn)并發(fā)處理同一訂單請(qǐng)求 |
三、防止重復(fù)提交的核心原則
要解決重復(fù)提交問題,必須從接口冪等性 + 請(qǐng)求唯一性 + 服務(wù)端鎖控制三方面入手:
- 控制請(qǐng)求的唯一標(biāo)識(shí)(token/nonce)
- 對(duì)訂單操作進(jìn)行冪等處理
- 引入緩存或分布式鎖限制重復(fù)提交
四、解決方案:基于Token機(jī)制 + Redis鎖的防重復(fù)提交設(shè)計(jì)
? 設(shè)計(jì)思路:
- 前端在創(chuàng)建訂單前從服務(wù)端獲取一個(gè)唯一 token(防重復(fù)提交標(biāo)識(shí))
- 提交訂單時(shí)將 token 附帶傳入
- 后端驗(yàn)證 token 是否存在(Redis)
- 校驗(yàn)通過 → 執(zhí)行下單邏輯 → 刪除 token
- 若 token 已被使用 → 拒絕重復(fù)提交
五、代碼實(shí)現(xiàn)(Spring Boot + Redis)
1. 前端獲取防重復(fù)提交 Token 接口
@RestController
@RequestMapping("/api/order")
public class OrderTokenController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/token")
public ResponseEntity<String> getToken() {
String token = UUID.randomUUID().toString();
String key = "order:token:" + token;
// 設(shè)置有效期5分鐘
redisTemplate.opsForValue().set(key, "valid", Duration.ofMinutes(5));
return ResponseEntity.ok(token);
}
}
2. 提交訂單接口(驗(yàn)證token + 刪除token)
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/submit")
public ResponseEntity<String> submitOrder(@RequestBody OrderRequest request,
@RequestHeader("X-Order-Token") String token) {
boolean success = orderService.submitOrder(request, token);
if (success) {
return ResponseEntity.ok("訂單提交成功");
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("請(qǐng)勿重復(fù)提交訂單");
}
}
}
3. OrderService 實(shí)現(xiàn)防重復(fù)提交邏輯
@Service
public class OrderService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 提交訂單接口,防止重復(fù)提交
*/
public boolean submitOrder(OrderRequest request, String token) {
String redisKey = "order:token:" + token;
// 利用 Redis 的 delete + check 保證冪等性(原子性)
Boolean result = redisTemplate.delete(redisKey);
if (Boolean.TRUE.equals(result)) {
// token存在并刪除 → 第一次提交
// 執(zhí)行正常訂單創(chuàng)建邏輯
createOrder(request);
return true;
} else {
// token 不存在 → 重復(fù)提交
return false;
}
}
private void createOrder(OrderRequest request) {
// 實(shí)際業(yè)務(wù)處理:生成訂單號(hào)、校驗(yàn)庫存、扣減庫存、寫庫、發(fā)MQ等
System.out.println("處理訂單:" + request);
}
}
4. 請(qǐng)求對(duì)象 OrderRequest 示例
@Data
public class OrderRequest {
private Long userId;
private List<Long> productIds;
private BigDecimal totalAmount;
}
六、進(jìn)階優(yōu)化建議
1. 使用 Lua 腳本保證 Redis 操作原子性
Redis
delete
操作不是強(qiáng)原子性的,建議使用 Lua 腳本執(zhí)行 “判斷 + 刪除” 邏輯。
// Lua 腳本實(shí)現(xiàn)原子刪除
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), "valid");
2. 給訂單接口加限流或熔斷保護(hù)(如 Sentinel)
- 防止惡意刷接口
- 降低重復(fù)提交帶來的系統(tǒng)壓力
3. 數(shù)據(jù)庫層冪等校驗(yàn)(雙保險(xiǎn))
即便應(yīng)用層失效,也可以通過數(shù)據(jù)庫約束(如訂單號(hào)唯一)+ INSERT IGNORE
或 ON DUPLICATE KEY
防止重復(fù)插入。
七、總結(jié)
面對(duì)用戶重復(fù)提交訂單的問題,我們不能只靠前端“禁用按鈕”了,而是應(yīng)該從后端保障:
- 請(qǐng)求唯一性
- 接口冪等性
- 服務(wù)端鎖機(jī)制
? 實(shí)戰(zhàn)建議:
- Redis 是處理冪等控制的利器
- token機(jī)制簡單實(shí)用,適用于下單、支付、秒殺等場景
- 多層防御更安全:應(yīng)用層 + 數(shù)據(jù)庫層
?? 最后
這篇文章分享了我在實(shí)際項(xiàng)目中防止訂單重復(fù)提交的完整方案,希望對(duì)你有所幫助!
如果你也在做訂單系統(tǒng)、支付系統(tǒng),歡迎留言交流你的經(jīng)驗(yàn)。