快手二面:你有沒有呼叫過第三方介面?碰到過哪些坑?

码农Academy發表於2024-05-14

在我們的業務開發中,呼叫第三方介面已經成為常態,比如對接一些ERP系統、WMS系統、一些資料服務系統等,它極大地擴充套件了我們應用的功能和服務範圍。然而,實際對接過程中,我們往往會在這一環節遇到各種意想不到的問題,本文將深入探討幾種常見的第三方介面呼叫難題及其應對策略。

呼叫第三方系統介面遇到的大坑.png

介面訪問不到

在執行第三方介面呼叫任務時,如果遇到程式響應遲滯直至超時,或者直接丟擲諸如Connection refusedHost is unreachableSocketTimeoutException之類的網路異常情況,這明確指示了無法成功建立起與目標伺服器的通訊連線。產生此問題的根源可能源自於多種因素,其中包括但不限於網路狀況不佳、伺服器尚未啟動、域名解析錯誤或介面地址有誤等。

為應對這類問題,首要步驟是自查本地網路環境是否正常。一旦確定自身網路並無故障,可行的操作之一是運用ping命令對目標域名進行探測,以驗證域名能否被正確解析並得到響應。若域名無法解析,則可能表明對方伺服器DNS配置存在問題;即使域名可以解析,但如果ping測試結果顯示響應異常或超時,說明目標服務端存在潛在故障。在這種情況下,及時與對方的技術團隊取得聯絡,共享診斷資訊,共同協作進行問題排查是一種有效的解決策略。

介面突然沒有返回資料/資料異常

原本正常的介面突然開始返回空資料,或者是返回的資料結構與預期不符,比如缺少必要的欄位、資料格式錯誤、資料內容無效等,導致客戶端無法正常解析和使用。

面對這類介面突然無響應或無法返回資料的問題,首先,我們需要從源頭著手,全面核查請求引數和認證憑證的有效性。這包括仔細審查傳送至介面的請求資料是否完整準確,以及確保使用的Token、Key等身份認證資訊處於有效狀態。同時,必須密切關注介面供應商是否有未提前公告的變更,如API版本升級、介面廢棄等情況。

在程式碼實現層面上,為了能快速響應這類異常,我們應當對關鍵資料欄位設定嚴格的監控與預警機制。例如,可以植入手動埋點並透過企業通訊工具(如釘釘訊息、電子郵件提醒)實現即時告警。一旦監測到核心資料未能如期返回,系統應能立即發出警報,使開發人員能夠在第一時間獲知並處理此類問題,以防止其對整體業務流程造成干擾或經濟損失。

以一個實際應用場景為例,當我們在上游系統中使用訂單號向下遊WMS系統查詢出入庫訂單詳情時,若發現特定訂單號未能返回預期的訂單資訊,那麼透過預先設定的監控和告警系統,我們將在第一時間接收到警告資訊。在此基礎上,應迅速與第三方系統的技術支援團隊取得聯絡,查明原因並解決問題。同時,對於這類無法匹配的資料,應在業務流程中設立防護機制,及時攔截處理,以免對核心業務造成負面影響。

介面超時/異常,不穩定

由於網路抖動,或者第三方系統不穩定,部署,伺服器負載不均、併發訪問量過大等等問題,可能會導致呼叫介面時花費的時間超出預期設定的超時時間,從而引發TimeoutException;或者接收到HTTP狀態碼錶明出現異常,如500 Internal Server Error404 Not Found等。這種坑使我們平常最容易遇見的也是最頭疼的所在,因此需要我們給予足夠的重視。

對於這類異常,首先我們在呼叫介面時設定合理的超時時間,我們以使用Retrofit2呼叫http介面為例,設定其請求超時時間以及讀取超時時間:

import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import java.util.concurrent.TimeUnit;

// 建立 OkHttpClient 例項並設定超時時間
OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS) // 連線超時時間為30秒
    .readTimeout(30, TimeUnit.SECONDS)      // 讀取超時也為30秒
    .build();

// 建立 Retrofit 例項,使用自定義的 OkHttpClient
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://your-api-url.com/")
    .client(okHttpClient) // 使用上面設定超時時間的 OkHttpClient
    .addConverterFactory(GsonConverterFactory.create()) // 使用Gson轉換器
    .build();

// 建立你的API介面例項
YourApiInterface apiService = retrofit.create(YourApiInterface.class);

有關Retrofit2的說明以及使用介紹,請參考:求求你別再用OkHttp呼叫API介面了,快來試試這款HTTP客戶端庫吧

同時,這對此類異常,我們還用做好介面重試機制。我們可以從以下幾種方案中考慮重試:

固定間隔重試

設定一個固定的等待時間間隔,在每次失敗後等待該間隔再進行下一次嘗試。比如我們可以使用定時任務框架如QuartzSpring Task SchedulerElasticJobxxl-job來定期執行重試任務。

這種方案實現簡單,但是可能不適用於所有場景,特別是當失敗是由於瞬時問題(如網路抖動)時,固定間隔可能過長或過短。

關於SringBoot自帶的定時任務的使用講解,請參考:玩轉SpringBoot:SpringBoot的幾種_定時任務_實現方式

指數退避重試

每次失敗後,等待時間間隔按指數級增長(例如,第一次失敗等待1秒,第二次等待2秒,第三次等待4秒,以此類推)。比如我們可以使用Spring RetryGuavaRetryerResilience4j等去實現指數退避重試。

我們以Spring Retry為例:

import org.springframework.retry.annotation.Backoff;  
import org.springframework.retry.annotation.Retryable;  
import org.springframework.stereotype.Service;  
  
@Service  
public class MyService {  
  
    @Retryable(value = {MyCustomException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000))  
    public void myMethod() {  
        // 這裡是可能會失敗的操作  
        // 如果丟擲 MyCustomException 異常,方法會被重試,最多重試3次  
        // 每次重試之間會有1秒的延遲(使用指數退避策略的話,延遲會逐漸增加)  
          
        // 假設某些條件下會丟擲異常  
        if (someCondition()) {  
            throw new MyCustomException("Operation failed");  
        }  
          
        // 如果操作成功,則正常返回  
    }  

	@Recover  
    public void recoverMyMethod(MyCustomException e) {  
        // 當 myMethod 的重試次數耗盡後,會呼叫這個方法  
        // 你可以在這裡記錄日誌、傳送通知或執行其他恢復操作  
        System.err.println("Operation failed after retries. Cause: " + e.getMessage());  
    } 
     
}

這種方案能夠自適應地調整重試間隔,減少連續失敗的可能性。但是缺點也很明顯,在長時間執行的系統中,如果問題持續存在,重試間隔可能會變得非常長,可能一不小心,會一直執行下去。

介面變更,版本迭代相容性

第三方系統對API進行版本升級或服務調整屬於常見現象,這種情況下,原有的介面可能面臨無法繼續使用的問題,或者返回的資料結構、格式可能發生變動,部分介面隨著版本升級可能存在不向下相容的情況,呼叫舊版介面在新版環境下可能失效。針對此類狀況,最佳實踐是始終保持對服務提供商通告的關注,一旦得知有關更新資訊,應迅速作出響應,及時調整並更新呼叫介面的方式。在程式碼層面,有必要預先設計並實現一套介面版本管理和相容性處理機制,以確保無論介面如何演變,系統都能夠平滑地適應和處理。

介面變更時,採用介面引數動態化是一種有效的應對策略,其核心理念是讓客戶端呼叫介面時具備更強的靈活性和適應性,特別是在介面新增、刪除或修改引數的情況下,比如採取Map,JSON接受引數(當然不是很推薦。。。。)。

並且,對介面進行嚴密的異常監測同樣至關重要,透過實時監控介面呼叫的異常狀況,能夠在問題發生的第一時間發現並上報。及時與第三方系統的技術支援團隊溝通協調,並採取相應的補救措施,能夠最大限度地減少介面變動對業務連續性的影響,確保系統穩定高效執行。

API限制

在一定時間段內頻繁呼叫介面,然後突然所有請求都開始失敗,返回的錯誤提示可能是呼叫頻率過高、超出配額等。這是由於大多數第三方API為了防止濫用,會對呼叫次數、頻次或流量進行限制。我們應密切關注介面文件中的呼叫限制說明,並在程式碼中採取限流措施,如設定合適的請求間隔、使用令牌桶演算法或漏桶演算法控制請求速度。當然也要做好介面監控告警策略。

針對此類問題,我們可以採取以下一些技術方案實現:

設定請求間隔(固定延遲)

在每次請求後,新增固定的延遲時間,比如每次請求後等待1秒(Thread.sleep(1000)),這種方式實現簡單,但可能不夠靈活,特別是當API的呼叫限制在不同時間段內變化時。

令牌桶演算法(Token Bucket)

令牌桶演算法是一種計算機網路流量整形和速率限制演算法。它允許突發流量,但長期平均輸出流量不會超過設定的速率。適用於允許短時間內的高流量,但長期需要控制平均流量的場景。我們可以使用Google的Guava庫中的RateLimiter來實現令牌桶演算法。

import com.google.common.util.concurrent.RateLimiter;  
  
@Service  
public class ApiService {  
  
    private final RateLimiter rateLimiter = RateLimiter.create(1.0); // 每秒生成一個令牌  
  
    @Autowired  
    private RestTemplate restTemplate;  
  
    public String callApi() {  
        if (!rateLimiter.tryAcquire()) { // 嘗試獲取令牌,如果沒有則返回false  
            throw new RuntimeException("Rate limit exceeded");  
        }  
        return restTemplate.getForObject("http://example.com/api", String.class);  
    }  
}

漏桶演算法(Leaky Bucket)

漏桶演算法是另一種流量整形和速率限制演算法。它將流量視為水倒入一個固定容量的桶中,如果桶滿了,水就會溢位(即請求被拒絕)。桶底有一個漏洞,水以一定的速度從桶中漏出,從而控制平均流量。適用於需要嚴格控制流量,不允許突發流量的場景。漏桶演算法通常需要自己實現,但也可以使用現有的庫,比如Bucket4j。

import io.github.bucket4j.Bandwidth;  
import io.github.bucket4j.Bucket;  
import io.github.bucket4j.Refill;  
  
@Service  
public class ApiService {  
  
    private final Bucket bucket = Bucket.builder()  
            .addLimit(Bandwidth.classic(10, Refill.greedy(10, TimeUnit.SECONDS))) // 每10秒新增10個令牌  
            .build();  
  
    @Autowired  
    private RestTemplate restTemplate;  
  
    public String callApi() {  
        try {  
            bucket.asScheduler().consume(1); // 消耗一個令牌  
        } catch (InterruptedException | InsufficientTokensException e) {  
            throw new RuntimeException("Rate limit exceeded", e);  
        }  
        return restTemplate.getForObject("http://example.com/api", String.class);  
    }  
}

滑動視窗演算法:

滑動視窗演算法用於跟蹤在特定時間視窗內的請求數量。當視窗內的請求數達到限制時,新的請求將被拒絕或延遲。視窗可以隨著時間的推移而滑動,以適應不同的時間間隔。

import java.util.LinkedList;  
import java.util.Queue;  
import java.util.concurrent.TimeUnit;  
  
@Service  
public class ApiService {  
  
    private final long windowSizeInMilliseconds;  
    private final int maxRequestsPerWindow;  
    private final Queue<Long> window = new LinkedList<>();  
  
    public ApiService(long windowSizeInMilliseconds, int maxRequestsPerWindow) {  
        this.windowSizeInMilliseconds = windowSizeInMilliseconds;  
        this.maxRequestsPerWindow = maxRequestsPerWindow;  
    }  
  
    public synchronized boolean tryAcquire() {  
        long currentTime = System.currentTimeMillis();  
        // 移除視窗外的時間戳  
        while (!window.isEmpty() && currentTime - window.peek() > windowSizeInMilliseconds) {  
            window.poll();  
        }  
        // 如果視窗內的請求數已達到上限,則不允許新的請求  
        if (window.size() >= maxRequestsPerWindow) {  
            return false;  
        }  
        // 在視窗內新增當前請求的時間戳  
        window.offer(currentTime);  
        return true;  
    }   
}

分散式限流

如果應用部署在多個例項或節點上,需要實現分散式限流以確保全域性的呼叫頻率不超過限制。可以使用Redis等分散式快取系統來共享令牌或記錄請求計數。

錯誤碼定義混亂,欄位結構不一致

我們常常會遇到介面文件與實際錯誤碼定義、欄位結構不一致的問題,例如文件中標明錯誤碼400代表引數錯誤,但實際上可能收到的是404錯誤響應;又或者返回的資料結構與文件描述不相吻合,這使得我們難以精準識別並恰當處理結果。針對此類問題,應當採取以下策略:

首先,構建自定義錯誤處理機制,建立專門的錯誤處理類,對所有可能出現的錯誤碼進行統一且明確的處理。這樣,無論介面返回何種錯誤碼,都能確保有一套標準的邏輯進行響應和記錄。

其次,針對那些與文件描述不符或者含義模糊不清的錯誤碼和欄位,應及時與第三方系統的技術團隊展開溝通交流,明確其真實含義和用途。這樣的互動有助於確保介面對接的精確性,避免因對錯誤碼或欄位理解不準確而引發的系統內部錯誤。

對於介面文件與實際不符的情況,一方面要透過定製化的錯誤處理機制增強系統的容錯性與一致性,另一方面要強化與第三方系統的溝通協作,確保對接介面的清晰性和準確性,從而有效避免潛在問題對自身系統產生的不良影響。

返回的資料格式不統一

對於同一個系統,介面返回的資料格式在不同場景下可能有所差異,例如有的時候返回JSON物件,有的時候卻是字串或其他格式,例如xml等。

針對這類問題,我們需要編寫包容性較強的解析邏輯,確保在任何情況下都能準確解構並處理返回資料。建立多個資料模型類對應不同格式的資料,根據介面返回的內容決定使用哪個模型類進行反序列化。針對不同的資料格式編寫介面卡,確保資料能統一轉換為應用程式可處理的格式。

作為介面服務提供者,我們應當怎麼做?

作為第三方系統介面的開發者,在設計和開發對外介面時,應當遵循一系列最佳實踐,以避免給呼叫方帶來上述提及的問題,我們應當注意以下幾個方面:

  1. 詳盡清晰的介面文件

    • 完整撰寫並持續更新介面文件,包括介面路徑、請求方法、請求引數、響應格式、錯誤碼含義、版本變更記錄等。
    • 錯誤碼定義應規範有序,避免混淆,確保每個錯誤碼都有明確的解釋和處理建議。
    • 欄位定義應清晰明確,註明必填項、可選項、資料型別和欄位意義,避免欄位命名混亂或含義不明。
  2. 版本控制與相容性

    • 設計介面版本管理機制,當介面有重大變更時推出新版本,並確保老版本介面在一定期限內仍可訪問,以便呼叫方平穩過渡。
    • 釋出新版本前,主動告知呼叫方介面變更內容和遷移計劃,給予充足的準備時間。
  3. 穩定性與效能

    • 高效穩定的伺服器架構,設定合理的超時和限流策略,避免介面超時、無響應或資料異常。
    • 保證服務的高可用性,採用負載均衡、叢集部署等方式確保介面穩定執行。
  4. 錯誤處理與反饋

    • 在介面設計時,對各種可能的錯誤場景都要有明確的錯誤碼和錯誤訊息返回,幫助呼叫方快速定位問題。
    • 提供健全的異常處理機制,確保在介面內部出現問題時,也能返回有意義的錯誤資訊。
  5. 介面測試與驗證

    • 提供詳盡的介面測試案例,確保介面的實際行為與文件描述一致。
    • 對於重大變更,可以提供沙箱環境或預釋出環境,讓呼叫方提前進行聯調和驗證。
  6. 變更通知與溝通

    • 在介面有任何變更(包括功能調整、引數修改、下線等)時,透過郵件、公告、API文件更新等方式提前通知呼叫方。
    • 開放技術支援渠道,及時解答呼叫方在對接介面過程中遇到的問題,提供必要的協助和支援。

作為第三方系統介面的開發者,可以最大程度地保證介面質量,降低呼叫方對接難度,同時也提升了自身服務的使用者體驗和市場競爭力。不然,別人在對接時,真的會在心裡時不時的來一句”MMP“。。。

本文已收錄於我的個人部落格:碼農Academy的部落格,專注分享Java技術乾貨,包括Java基礎、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中介軟體、架構設計、面試題、程式設計師攻略等

相關文章