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

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






























