Redis+唯一序列號的方案實現接口冪等性
前言
接口冪等性是指同一接口被多次調用時,產生的業務結果是一致的,不會因為重復調用而導致非預期的副作用。在分布式系統中,由于網絡延遲、重試機制、負載均衡等因素,接口重復調用幾乎是不可避免的,因此保證接口冪等性是系統設計中至關重要的一環。
冪等性實現方案對比
方案 | 優點 | 缺點 | 適用場景 |
Redis + 唯一序列號 | 高性能,支持分布式 | 依賴 Redis,需客戶端生成序列號 | 大部分分布式接口場景 |
數據庫唯一索引 | 實現簡單,不依賴其他組件 | 性能較低,對數據庫有壓力 | 數據插入類接口 |
令牌機制 | 服務端生成令牌,安全性高 | 增加一次請求交互,流程復雜 | 安全性要求高的場景 |
狀態機控制 | 符合業務邏輯,天然冪等 | 僅適用于有狀態流轉的業務 | 訂單狀態變更等場景 |
悲觀鎖 | 實現簡單,安全性高 | 并發性能差 | 并發量低的場景 |
樂觀鎖 | 并發性能好 | 實現相對復雜 | 并發量高的場景 |
Redis + 唯一序列號方案介紹
Redis+唯一序列號方案是實現接口冪等性的常用方案之一,其核心思想是:
- 客戶端發起請求時,生成一個全局唯一的序列號(
Serial Number) - 服務端接收到請求后,先檢查該序列號在
Redis中是否存在 - 如果不存在,將序列號存入
Redis,并執行業務邏輯 - 如果已存在,說明是重復請求,直接返回成功或相應結果,不重復執行業務邏輯
該方案利用Redis的高性能、原子操作和過期時間特性,能夠高效地實現分布式環境下的接口冪等性控制。
效果圖

實現細節
唯一序列號需要滿足以下特性:
- 全局唯一性:確保不同請求不會產生相同的序列號
- 不可預測性:防止惡意猜測序列號
- 高效生成:生成過程不能成為系統瓶頸
- 包含業務含義(可選):便于問題排查和追蹤
常用的生成方式:
UUID/GUID:通用唯一識別碼,本地生成,性能高- 雪花算法(
Snowflake):生成64位全局唯一ID,包含時間戳信息 - 數據庫自增
ID:通過數據庫生成唯一ID,性能較低 - 業務信息 + 隨機數:結合用戶
ID、時間戳等業務信息生成
Redis 存儲設計
鍵(Key)設計:
idempotent:{業務類型}:{序列號}值(Value)設計:
可以存儲請求相關信息,如用戶ID、請求時間等,便于后續分析和問題排查。
過期時間(TTL):
根據業務場景設置合理的過期時間,避免Redis內存溢出。例如:
- 支付類接口:
24小時 - 訂單類接口:
7天 - 一般查詢接口:
1小時
處理流程
客戶端 -> 生成唯一序列號 -> 攜帶序列號發起請求 -> 服務端
服務端 -> 檢查Redis中是否存在該序列號
-> 不存在:存儲序列號到Redis,執行業務邏輯,返回結果
-> 存在:直接返回之前的處理結果實現示例
生成唯一序列號工具類
public class SerialNumberGenerator {
/**
* 生成UUID作為唯一序列號
*/
public static String generateUUID() {
return UUID.randomUUID().toString();
}
/**
* 生成包含業務前綴的唯一序列號
*/
public static String generateWithPrefix(String prefix) {
return prefix + ":" + UUID.randomUUID().toString().replaceAll("-", "");
}
}Redis 操作工具類
@Component
public class RedisIdempotentUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 檢查并設置序列號
* @param key 序列號鍵
* @param value 存儲的值
* @param expireTime 過期時間
* @param timeUnit 時間單位
* @returntrue-首次請求,false-重復請求
*/
public boolean checkAndSet(String key, Object value, long expireTime, TimeUnit timeUnit) {
// 使用Redis的setIfAbsent實現原子操作,確保線程安全
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, timeUnit);
return Boolean.TRUE.equals(result);
}
/**
* 獲取序列號對應的值
*/
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 構建Redis鍵
*/
public String buildKey(String businessType, String serialNumber) {
return"idempotent:" + businessType + ":" + serialNumber;
}
}接口冪等性控制實現
@RestController
public class PaymentController {
@Resource
private RedisIdempotentUtils redisIdempotentUtils;
@Resource
private PaymentService paymentService;
/**
* 支付接口
* @param serialNumber 唯一序列號,從請求頭獲取
* @param paymentRequest 支付請求參數
*/
@PostMapping("/api/payment")
public Result<PaymentResponse> processPayment(
@RequestHeader("Idempotent-Serial") String serialNumber,
@RequestBody PaymentRequest paymentRequest) {
// 1. 構建Redis鍵
String redisKey = redisIdempotentUtils.buildKey("payment", serialNumber);
// 2. 檢查是否為重復請求
boolean isFirstRequest = redisIdempotentUtils.checkAndSet(
redisKey,
paymentRequest.getUserId(),
24,
TimeUnit.HOURS);
if (!isFirstRequest) {
// 重復請求,返回之前的處理結果或提示
Object userId = redisIdempotentUtils.get(redisKey);
PaymentResponse cachedResponse = getCachedPaymentResponse(serialNumber);
return Result.success("重復請求,已處理", cachedResponse);
}
try {
// 3. 首次請求,執行業務邏輯
PaymentResponse response = paymentService.processPayment(paymentRequest);
// 4. 緩存處理結果(可選)
cachePaymentResponse(serialNumber, response);
return Result.success("支付成功", response);
} catch (Exception e) {
// 5. 業務處理失敗,刪除序列號(根據業務需求決定)
// redisTemplate.delete(redisKey);
return Result.fail("支付失敗:" + e.getMessage());
}
}
// 緩存和獲取支付結果的方法(實際實現略)
private void cachePaymentResponse(String serialNumber, PaymentResponse response) {
// 實現緩存邏輯
}
private PaymentResponse getCachedPaymentResponse(String serialNumber) {
// 實現獲取緩存邏輯
return new PaymentResponse();
}
}使用注意事項
- 序列號傳遞方式:建議通過請求頭(
Header)傳遞,避免侵入業務參數 - 過期時間設置:根據業務場景合理設置,一般應大于業務最大處理時間
- 異常處理:業務處理失敗時,根據實際需求決定是否刪除 Redis 中的序列號
- Redis 高可用:確保
Redis服務的高可用,可采用主從復制、哨兵或集群模式 - 監控告警:對
Redis的內存使用、接口重復請求數等指標進行監控 - 序列號生成:確保序列號的唯一性,避免因生成策略問題導致的沖突
方案優化建議
- 序列號壓縮:對序列號進行壓縮處理,減少
Redis存儲占用 - 本地緩存:在服務端增加本地緩存(如
Caffeine),減少對Redis的訪問 - 批量清理:對于過期數據,可采用定時任務批量清理,減輕
Redis負擔 - 分級存儲:根據業務重要性,對不同接口設置不同的存儲策略和過期時間
- 異步處理:對于非核心流程,可采用異步方式處理,提高響應速度






























