別再讓接口亂跑!SpringBoot實現接口冪等性的四大實戰方案,徹底告別重復提交!
在分布式系統中,重復請求是最隱蔽的業務炸彈。 用戶手抖、網絡抖動、支付回調、消息隊列重試…… 任意一次“重復操作”,都有可能導致 重復扣款、重復發貨、數據異常。
本文將帶你深入拆解 Spring Boot 實現接口冪等性的 4 種主流方案, 覆蓋從“輕量級本地防重”到“分布式高并發控制”,并結合 實戰級代碼 展示落地細節。
接下來,我們將逐步拆解四大方案:
- Token令牌機制 —— 經典且穩
- 數據庫唯一索引 —— 簡潔又強一致
- 分布式鎖機制 —— 并發場景的核心武器
- 請求內容摘要 —— 最通用、最透明
Token 令牌機制:最經典的防重手段
核心思想:
“先拿令牌 → 再執行業務 → 用完即焚”
通過在請求前生成一次性令牌(Token),在執行接口時驗證并原子刪除,保證每個請求只被處理一次。
代碼示例
路徑:/src/main/java/com/icoderoad/order/OrderController.java
package com.icoderoad.order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.time.Duration;
import java.util.UUID;
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private StringRedisTemplate redis;
// ① 預生成 Token,供前端使用
@GetMapping("/token")
public String getToken() {
String token = UUID.randomUUID().toString();
redis.opsForValue().set("tk:" + token, "1", Duration.ofMinutes(10));
return token;
}
// ② 下單接口,Header 中攜帶令牌
@PostMapping
public Result create(@RequestHeader("Idempotent-Token") String token,
@RequestBody OrderReq req) {
String key = "tk:" + token;
Boolean first = redis.delete(key);
if (Boolean.FALSE.equals(first)) {
return Result.fail("請勿重復下單");
}
Order order = orderService.create(req);
return Result.ok(order);
}
}要點解析:
UUID生成全局唯一 Token;- Redis 設置 TTL(10分鐘)避免緩存堆積;
delete()是原子操作,可安全防重;- Header 傳遞令牌,保持接口語義清晰。
數據庫唯一索引:最低成本的冪等保證
核心思想:
“唯一鍵 + 異常即冪等”
通過數據庫層面的 唯一索引,讓重復請求在插入時直接報錯,天然具備冪等特性。
代碼示例
路徑:/src/main/java/com/icoderoad/payment/PayService.java
package com.icoderoad.payment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import javax.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "t_payment", uniqueConstraints = @UniqueConstraint(columnNames = "transaction_id"))
class Payment {
@Id
private Long id;
@Column(name = "transaction_id")
private String txId;
private BigDecimal amount;
private String status;
}
@Service
public class PayService {
@Autowired
private PaymentRepo repo;
public Result pay(PayReq req) {
try {
Payment p = new Payment();
p.setTxId(req.getTxId());
p.setAmount(req.getAmount());
p.setStatus("SUCCESS");
repo.save(p);
return Result.ok("支付成功");
} catch (DataIntegrityViolationException e) {
Payment exist = repo.findByTxId(req.getTxId());
return Result.ok("已支付", exist.getId());
}
}
}要點解析:
uniqueConstraints確保事務級防重;- 異常捕獲后直接返回冪等響應;
- 無需外部依賴,兼容老舊系統。
分布式鎖機制:高并發下的“互斥利器”
核心思想:
“對關鍵資源加鎖,誰搶到誰執行”
在并發操作中通過 Redisson 或 Zookeeper 實現互斥訪問,保障同一用戶或訂單只被處理一次。
代碼示例
路徑:/src/main/java/com/icoderoad/stock/StockService.java
package com.icoderoad.stock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class StockService {
@Autowired
private RedissonClient redisson;
@Autowired
private StockRepo repo;
public Result deduct(DeductCmd cmd) {
String lockKey = "lock:stock:" + cmd.getProductId();
RLock lock = redisson.getLock(lockKey);
try {
if (!lock.tryLock(3, 5, TimeUnit.SECONDS)) {
return Result.fail("處理中,請稍后");
}
if (repo.existsByRequestId(cmd.getRequestId())) {
return Result.ok("已扣減");
}
repo.deductStock(cmd);
return Result.ok("扣減成功");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Result.fail("系統繁忙");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}要點解析:
tryLock避免線程永久阻塞;- Redisson 自動續期機制防止死鎖;
requestId與唯一索引配合,形成“雙保險”;- 適合秒殺、庫存、并發下單等高頻場景。
請求內容摘要:最透明的零侵入方案
核心思想:
“以請求內容為冪等標識,天然適配所有接口”
將請求體生成 MD5/SHA256摘要 作為冪等鍵,通過 Redis 進行原子性驗證,真正做到“客戶端無感”。
代碼示例
路徑:/src/main/java/com/icoderoad/common/aop/IdempotentAspect.java
package com.icoderoad.common.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.apache.commons.io.IOUtils;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
int expire() default 3600; // 秒
}
@Aspect
@Component
public class IdempotentAspect {
@Autowired
private StringRedisTemplate redis;
@Around("@annotation(idem)")
public Object around(ProceedingJoinPoint pjp, Idempotent idem) throws Throwable {
HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String body = IOUtils.toString(req.getReader());
String digest = DigestUtils.md5DigestAsHex(body.getBytes(StandardCharsets.UTF_8));
String key = "idem:digest:" + digest;
Boolean absent = redis.opsForValue().setIfAbsent(key, "1", Duration.ofSeconds(idem.expire()));
if (Boolean.FALSE.equals(absent)) {
return Result.fail("重復請求");
}
try {
return pjp.proceed();
} catch (Exception e) {
redis.delete(key);
throw e;
}
}
}使用示例:
@RestController
@RequestMapping("/transfer")
public class TransferController {
@PostMapping
@Idempotent(expire = 7200)
public Result transfer(@RequestBody TransferCmd cmd) {
return Result.ok(transferSvc.doTransfer(cmd));
}
}要點解析:
- 使用 MD5 壓縮請求體,確保唯一性;
setIfAbsent保證 Redis 原子操作;- 異常回滾防止誤判;
- 注解 + AOP 實現零侵入式冪等控制。
方案對比與落地建議
方案類型 | 實現復雜度 | 外部依賴 | 典型場景 |
Token令牌 | 中等 | Redis | 下單、支付、表單提交 |
唯一索引 | 低 | 無 | 注冊、支付回調 |
分布式鎖 | 中高 | Redis/ZK | 秒殺、庫存扣減 |
內容摘要 | 中 | Redis | 轉賬、接口回調 |
結語:冪等性不是裝飾,而是底線
冪等控制是后端架構中防止業務災難的安全閥。 選擇方案時請遵循以下三條原則:
- 先業務分析,再加鎖 —— 能靠唯一鍵解決的,不必上分布式鎖;
- 核心路徑必防重 —— 特別是支付、庫存、轉賬等資金相關接口;
- 冪等監控要同步上線 —— 及時發現、告警、自動恢復。
記住:冪等性不是性能開銷,而是系統穩定的基石。
從 Token 到摘要,每一種方案都有其價值, 真正的架構師,懂得“用最小的代價,守住最大的安全”。
































