前言
在程序開發的過程中是否遇到如下的問題:
- 同一件商品手速很快多點擊了幾次,在後台生成了兩筆訂單。
- 同一筆訂單點了由於網絡卡頓,點了兩次支付,結果發現重複支付了。
- 微服務架構下應用間通過RPC調用失敗,進入重試機制,導致一個請求提交多次。
- 黑客利用充值抓包到的數據,進行多次調用充值、評論、訪問,造成數據的異常。
這些問題均可以通過接口冪等性設計來解決。冪等性意味着同一個請求無論被重複執行多少次,都能產生相同的結果,不會導致重複的操作或不一致的數據狀態。
在現代分佈式系統中,接口的冪等性設計和實現至關重要。本文將深入探討接口冪等的重要性、實現方法以及可能面臨的挑戰,並提供測試接口冪等性的有效策略。
什麼是接口冪等性
接口冪等性指的是一個接口或操作在相同的請求參數下,無論被執行多少次,其結果都是一致的且不會產生副作用。換句話説,如果一個請求已經成功執行,再次執行相同的請求應該不會對系統狀態產生任何額外的影響。例如,一個獲取用户信息的接口就是冪等的,因為多次獲取同一個用户的信息不會改變系統的狀態。
相反,非冪等接口可能會導致重複的操作和潛在的問題。以支付操作為例,如果沒有實現冪等性,重複支付可能會給用户和商家帶來不必要的麻煩和損失。
為什麼需要接口冪等性
- 防止重複操作:冪等性可以確保系統不會因為重複的請求而產生重複的操作,從而避免數據錯誤和不一致。
- 提高系統可靠性:在網絡不穩定或其他異常情況下,重複的請求是很常見的。冪等性可以幫助系統處理這些重複請求,而不會導致系統出錯或不穩定。
- 增強用户體驗:用户不需要擔心因為不小心重複操作而導致的問題,從而提高了用户的使用體驗和滿意度。
- 簡化錯誤處理:由於冪等接口可以安全地處理重複請求,因此在處理錯誤和恢復時更加容易,減少了複雜的錯誤恢復邏輯。
如何設計接口冪等性
- 使用唯一標識:為每個請求分配一個唯一的標識,例如請求 ID 或流水號。通過在請求中傳遞這個唯一標識,系統可以判斷是否已經處理過該請求。
- 設計冪等的操作:確保操作本身是冪等的。例如,更新數據時可以採用"更新或插入"的策略,而不是直接修改已有記錄。
- 使用事務:在涉及多個數據庫操作的情況下,使用事務來確保整個操作的原子性和冪等性。
- 利用緩存:將請求的結果緩存起來,當接收到相同的請求時,直接返回緩存中的結果,避免重複執行操作。
如何實現接口冪等性
以下實現方式是基於demo完成,用於説明冪等性的設計和實現。
- 唯一標識:可以通過生成全局唯一的 ID(如 UUID)來標識每個請求。在請求的參數中包含這個 ID,服務器在處理請求時可以根據 ID 來判斷是否已經處理過該請求。
服務端生成 requestId 之後將 requestId 放到redis中,當然需要給 ID 設置一個失效時間,超時的 ID 也會被刪除。
public class RequestIdGenerator {
public static String generateRequestId() {
Stirng uuid = UUID.randomUUID().toString();
putCacheIfAbsent(uuid);
return uuid;
}
}
在接口中,將生成的請求 ID 與請求參數一起傳遞給服務器。
// 生成請求 ID
String requestId = RequestIdGenerator.generateRequestId();
// 構建請求參數
Map<String, String> requestParams = new HashMap<>();
requestParams.put("requestId", requestId);
requestParams.put("otherParam", "value");
// 發送請求
httpClient.sendRequest(requestParams);
服務器在接收到請求後,可以根據請求 requestId 來判斷是否已經處理過該請求,並進行相應的處理。
當後端接收到訂單提交的請求的時候,會先判斷requestId在緩存中是否存在,第一次請求的時候,requestId一定存在,也會正常返回結果,但是第二次攜帶同一個requestId的時候被拒絕了。
- 冪等的操作:以訂單狀態更新為例,如果訂單已經處於最終狀態(如已支付或已發貨),再次更新訂單狀態不會改變其實際狀態,因此是冪等的。
public class OrderService {
public void updateOrderStatus(String orderId, OrderStatus status) {
// 根據 orderId 獲取訂單
Order order = orderIdToOrderMapper orderIdToOrder(orderId);
// 判斷訂單是否處於最終狀態
if (order.isFinalStatus()) {
// 訂單已處於最終狀態,不需要進行實際的更新操作
return;
}
// 更新訂單狀態
order.setStatus(status);
orderRepository.save(order);
}
}
- 事務:在數據庫操作中,可以使用事務來保證操作的原子性和冪等性。如果某個操作失敗,事務可以回滾到之前的狀態,避免不一致的數據。
@Transactional
public void performTransactionalOperation() {
// 開啓事務
Transaction transaction = transactionManager.beginTransaction();
transaction.setIsolationLevel(IsolationLevel.READ_COMMITTED);
transaction.setPropagationBehavior(Propagation.REQUIRED);
// 數據庫操作 1
//...
// 數據庫操作 2
//...
// 提交事務
transactionManager.commit();
}
開啓事務是一種悲觀鎖實現的方式,一開始更新數據就把數據加鎖了,具有強烈的獨佔和排他特性。
- 緩存:通過將請求的結果緩存起來,可以避免重複執行相同的操作。當接收到相同的請求時,直接從緩存中獲取結果返回。
public class CacheService {
private Map<String, Object> cache = new ConcurrentHashMap<>();
public Object getCachedResult(String key) {
// 從緩存中獲取結果
if (cache.containsKey(key)) {
return cache.get(key);
}
// 執行實際的操作並獲取結果
Object result = performExpensiveOperation(key);
// 將結果緩存起來
cache.put(key, result);
// 返回結果
return result;
}
}
使用冪等性接口帶來了什麼結果
- 併發請求處理:在高併發環境下,可能會同時接收到多個相同的請求。為了處理這種情況,可以使用分佈式鎖或其他併發控制機制來確保只有一個請求執行實際的操作。目前的分佈式鎖一般基於zookeeper或者redis實現。
- 失敗請求的處理:如果請求在執行過程中失敗,需要確保冪等性仍然得到維護。可以通過記錄請求的狀態或使用重試機制來處理失敗的請求。
- 與現有系統的集成:在將冪等性引入現有系統時,可能需要對現有系統進行一些修改和適配。這可能涉及到與其他組件或服務的協調和集成。
- 測試的複雜性:由於冪等性的測試需要模擬重複請求和各種邊界情況,測試的複雜性可能會增加。需要設計全面的測試用例來覆蓋各種可能的情況。
怎麼驗證接口是否具有冪等性
- 模擬重複請求:使用測試工具或手動模擬發送相同的請求多次,檢查結果是否一致。
- 驗證數據一致性:檢查相關的數據是否在重複請求後保持一致,沒有出現重複操作或數據不一致的情況。
- 壓力測試:在高併發情況下測試接口的冪等性,確保在大量請求同時到達時系統仍然能正確處理。
- 異常情況測試:模擬各種異常情況,如網絡中斷、服務器故障等,檢查接口在這些情況下是否仍然保持冪等性。
冪等性接口的總結
實現接口的冪等性對於構建可靠和高效的系統至關重要。通過使用唯一標識、冪等操作、事務和緩存等技術,可以有效地設計和實現冪等接口。
同時,要注意處理可能面臨的挑戰,並通過全面的測試來確保接口的正確性和穩定性。在實際項目中,積極應用這些方法將有助於提高系統的可靠性、安全性和用户體驗。
參考
- 前任開發在代碼裏下毒了,支付下單居然沒加冪等 https://juejin.cn/post/7324186292297482290
關於作者
來自一線全棧程序員nine的八年探索與實踐,持續迭代中。歡迎關注“雨林尋北”或添加個人衞星codetrend(備註技術)。