java中接口冪等性的五種實現(xiàn)方法
在 Java 中保證接口冪等性(即多次調(diào)用同一接口產(chǎn)生與單次調(diào)用相同的結(jié)果,不會引發(fā)副作用),需要結(jié)合業(yè)務場景選擇合適的方案。以下是常見的實現(xiàn)方式及技術(shù)細節(jié):
1. 基于唯一標識的去重機制
核心思想:為每次請求生成唯一標識(如訂單號、請求 ID),服務端通過記錄該標識是否已處理,避免重復執(zhí)行。
實現(xiàn)方式:
數(shù)據(jù)庫唯一約束:將唯一標識作為數(shù)據(jù)庫表的唯一索引,重復請求會觸發(fā)主鍵沖突異常,直接返回成功(或錯誤提示)。
// 示例:訂單表唯一索引(order_no)
@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = "orderNo")})
public class Order {
@Id
private Long id;
private String orderNo; // 唯一訂單號(作為冪等標識)
// 其他字段...
}
// 服務層處理
@Transactional
public Result createOrder(OrderDTO dto) {
try {
// 嘗試插入訂單(依賴數(shù)據(jù)庫唯一約束)
Order order = new Order();
order.setOrderNo(dto.getOrderNo());
orderRepository.save(order);
// 執(zhí)行后續(xù)業(yè)務(如扣減庫存)
return Result.success();
} catch (DataIntegrityViolationException e) {
// 唯一約束沖突,說明已處理過
log.warn("訂單已存在: {}", dto.getOrderNo());
return Result.success(); // 或返回已有結(jié)果
}
}
緩存記錄(Redis) :利用 Redis 的SETNX(不存在則設置)特性,判斷請求是否已處理。
@Autowired
private StringRedisTemplate redisTemplate;
public Result processRequest(String requestId) {
// 嘗試設置唯一標識,過期時間防止內(nèi)存溢出
Boolean isFirst = redisTemplate.opsForValue().setIfAbsent(
"idempotent:" + requestId,
"processed",
1, TimeUnit.HOURS
);
if (Boolean.TRUE.equals(isFirst)) {
// 首次請求,執(zhí)行業(yè)務邏輯
doBusiness();
return Result.success();
} else {
// 重復請求,返回已有結(jié)果
return Result.success("已處理");
}
}
2. 令牌(Token)機制
核心思想:客戶端先向服務端申請令牌,請求接口時攜帶令牌,服務端驗證令牌有效性后處理業(yè)務,并標記令牌為已使用。
實現(xiàn)流程:
- 客戶端請求獲取令牌(服務端生成令牌并存儲到 Redis)。
- 客戶端攜帶令牌調(diào)用業(yè)務接口。
- 服務端校驗令牌:存在則處理業(yè)務并刪除令牌;不存在則拒絕。
// 生成令牌
public String generateToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("token:" + token, "valid", 30, TimeUnit.MINUTES);
return token;
}
// 校驗令牌并處理業(yè)務
public Result doBusiness(String token, BusinessDTO dto) {
// 刪除令牌(原子操作,確保唯一處理)
Boolean isValid = redisTemplate.delete("token:" + token);
if (Boolean.TRUE.equals(isValid)) {
// 令牌有效,執(zhí)行業(yè)務
process(dto);
return Result.success();
} else {
// 令牌無效(已使用或過期)
return Result.fail("重復請求");
}
}
3. 樂觀鎖機制
核心思想:適用于更新操作,通過版本號控制,確保只有版本匹配時才執(zhí)行更新,避免重復更新。
@Entity
public class Product {
@Id
private Long id;
private Integer stock; // 庫存
private Integer version; // 版本號
}
@Transactional
public Result reduceStock(Long productId, Integer quantity) {
// 查詢商品及當前版本
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("商品不存在"));
// 檢查庫存
if (product.getStock() < quantity) {
return Result.fail("庫存不足");
}
// 樂觀鎖更新(where條件包含版本號)
int rows = productRepository.reduceStock(
productId,
quantity,
product.getVersion() // 當前版本
);
if (rows > 0) {
return Result.success();
} else {
// 版本不匹配,說明已被其他請求處理
return Result.fail("操作沖突,請重試");
}
}
// Repository層SQL(JPA示例)
@Modifying
@Query("UPDATE Product p SET p.stock = p.stock - :quantity, p.version = p.version + 1 " +
"WHERE p.id = :id AND p.version = :version")
int reduceStock(@Param("id") Long id,
@Param("quantity") Integer quantity,
@Param("version") Integer version);
4. 狀態(tài)機控制
核心思想:通過狀態(tài)流轉(zhuǎn)約束,確保接口只能在特定狀態(tài)下執(zhí)行,避免重復操作(如訂單狀態(tài)從 “待支付” 到 “已支付” 的單向流轉(zhuǎn))。
public enum OrderStatus {
CREATED(1, "待支付"),
PAID(2, "已支付"),
CANCELLED(3, "已取消");
private int code;
private String desc;
// 構(gòu)造器、getter...
}
@Transactional
public Result payOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("訂單不存在"));
// 僅允許“待支付”狀態(tài)執(zhí)行支付
if (order.getStatus() != OrderStatus.CREATED) {
log.warn("訂單狀態(tài)異常: {}", orderId);
return Result.success("訂單已處理"); // 重復支付請求直接返回成功
}
// 執(zhí)行支付邏輯(如調(diào)用支付網(wǎng)關(guān))
boolean paySuccess = paymentGateway.pay(order);
if (paySuccess) {
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
return Result.success();
} else {
return Result.fail("支付失敗");
}
}
5. 分布式鎖(高并發(fā)場景)
核心思想:在分布式系統(tǒng)中,通過分布式鎖(如 Redis、ZooKeeper)確保同一時間只有一個請求處理業(yè)務。
// 基于Redis的分布式鎖(使用Redisson)
@Autowired
private RedissonClient redissonClient;
public Result processDistributed(String key) {
RLock lock = redissonClient.getLock("lock:" + key);
try {
// 嘗試獲取鎖,最多等待10秒,持有鎖1分鐘
boolean locked = lock.tryLock(10, 60, TimeUnit.SECONDS);
if (locked) {
// 檢查是否已處理(雙重校驗)
if (isProcessed(key)) {
return Result.success("已處理");
}
// 執(zhí)行業(yè)務
doBusiness();
markAsProcessed(key); // 標記為已處理
return Result.success();
} else {
// 獲取鎖失敗,可能是重復請求
return Result.fail("操作繁忙,請重試");
}
} finally {
// 釋放鎖(僅釋放自己持有的鎖)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
選擇建議
- 查詢接口:天然冪等,無需額外處理。
- 新增操作:優(yōu)先使用 “唯一標識 + 數(shù)據(jù)庫 / Redis 去重”。
- 更新操作:優(yōu)先使用 “樂觀鎖” 或 “狀態(tài)機”。
- 分布式系統(tǒng):結(jié)合 “分布式鎖” 與 “唯一標識” 確保一致性。
- 高并發(fā)場景:優(yōu)先使用 Redis(性能優(yōu)于數(shù)據(jù)庫)。
需注意:冪等性設計需結(jié)合業(yè)務場景,避免過度設計;同時要處理異常情況(如網(wǎng)絡超時),確??蛻舳酥卦嚂r的正確性。
到此這篇關(guān)于java中接口冪等性的五種實現(xiàn)方法的文章就介紹到這了,更多相關(guān)java 接口冪等性內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java ThreadLocal類應用實戰(zhàn)案例分析
這篇文章主要介紹了Java ThreadLocal類應用,結(jié)合具體案例形式分析了java ThreadLocal類的功能、原理、用法及相關(guān)操作注意事項,需要的朋友可以參考下2019-09-09
Java?數(shù)據(jù)結(jié)構(gòu)與算法系列精講之棧
棧(stack)又名堆棧,它是一種運算受限的線性表。限定僅在表尾進行插入和刪除操作的線性表。這一端被稱為棧頂,相對地,把另一端稱為棧底,棧是基礎(chǔ)中的基礎(chǔ),如果你還沒掌握透徹就來接著往下看吧2022-02-02
Java解析DICOM圖之如何獲得16進制數(shù)據(jù)詳解
DICOM就是醫(yī)學數(shù)字成像和通信,是醫(yī)學圖像和相關(guān)信息的國際標準(ISO 12052),下面這篇文章主要給大家介紹了關(guān)于Java解析DICOM圖之如何獲得16進制數(shù)據(jù)的相關(guān)資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下。2017-10-10
利用Intellij Idea連接遠程服務器實現(xiàn)遠程上傳部署功能
大家在使用Intellij Idea開發(fā)程序的時候,是不是需要部署到遠程SSH服務器運行呢,當然也可以直接在idea軟件內(nèi)容實現(xiàn)配置部署操作,接下來通過本文給大家分享利用Intellij Idea連接遠程服務器實現(xiàn)遠程上傳部署功能,感興趣的朋友跟隨小編一起看看吧2021-05-05
修改request的parameter的幾種方式總結(jié)
這篇文章主要介紹了修改request的parameter的幾種方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08

