深入探討微服務架構中的同步通訊機制

努力的小雨發表於2024-08-09

微服務架構是一種設計方法,將應用程式劃分為一組小型服務,每個服務在獨立的程序中執行,通常根據業務能力進行組織。這些服務透過多種通訊方式互動,以實現整個應用的功能。今天我們著重介紹同步通訊,關於非同步通訊和訊息佇列(MQ)等內容將在後續講解。

這裡所指的通訊,是指我們在客戶端內部進行的服務間通訊,而非透過呼叫外部的Web服務進行訪問。好的,讓我們開始。

負載均衡

服務端負載均衡

在深入探討客戶端負載均衡之前,我們首先應該對服務端負載均衡有一個更加深入的瞭解,例如nginx元件。現在讓我們來仔細看一下:

image

nginx通常用來對我們伺服器內部進行負載轉發請求,而客戶端則是在各個服務內部進行負載分擔。

客戶端負載均衡

在Spring Cloud中,例如使用Ribbon時,客戶端會維護一個伺服器地址列表,在傳送請求之前透過負載均衡演算法選擇一個伺服器進行訪問。這種方式被稱為客戶端負載均衡,因為它在客戶端內部就完成了負載均衡演算法的分配工作。

image

同步通訊

一般情況下,我們在進行HTTP請求時常會藉助各種工具類。我相信大家之前可能經常自行封裝httputils等類,或者使用其他官方提供的工具。比如,今天我們來講解一下RestTemplate工具類。它其實就是一個用來傳送HTTP請求的工具,只不過它在此基礎上多做了一些額外的工作。接下來我們先看一下它的基本用法。

RestTemplate

RestTemplate 是由 Spring 框架提供的一個功能強大的類,專門用於進行同步客戶端的 HTTP 訪問。它的設計旨在簡化使用 HTTP 客戶端進行 REST 呼叫的複雜流程,並提供了豐富的方法和功能來處理各種 HTTP 請求和響應。

建立 RestTemplate 例項

首先,你需要建立一個 RestTemplate 的例項。這可以透過直接例項化或使用Spring的自動裝配來完成。

import org.springframework.web.client.RestTemplate;

RestTemplate restTemplate = new RestTemplate();

傳送請求

使用 RestTemplate 傳送一個GET請求並獲取響應體。

String url = "http://example.com/api/resource";
String result = restTemplate.getForObject(url, String.class);
System.out.println(result);

傳送一個POST請求,通常包含請求體。

String url = "http://example.com/api/resource";
Map<String, Object> requestMap = new HashMap<>();
requestMap.put("key1", "value1");
requestMap.put("key2", "value2");

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestMap, headers);
String response = restTemplate.postForObject(url, entity, String.class);
System.out.println(response);

看到這裡,你可能會想到,這與微服務間的通訊有什麼關係呢?難道不只是簡單的 HTTP 呼叫嗎?讓我們繼續深入探討。

LoadBalanced註解

微服務架構中通常會涉及到註冊中心。今天我們專注討論微服務間的通訊,而不深入講解註冊中心,例如 Nacos 是如何管理所有微服務的註冊以及如何使一個服務節點發現其他服務節點的。假設我們已經獲得了其他服務節點的 IP 地址,你可能會想直接將上述示例中的域名替換為 IP 地址,但是在面對保證高可用的多節點微服務時,直接在程式碼中寫死 IP 地址將會帶來災難性的後果。所有的 IP 地址應當由一個統一元件進行管理和選擇。

因此,Ribbon應運而生。一般情況下,如果在專案的pom檔案中整合了Nacos依賴,通常會預設包含Ribbon元件,因此不需要單獨配置pom檔案來引入Ribbon依賴。

基本用法

如前所述,一種方法是直接例項化物件,另一種則是透過Spring容器進行管理和注入。

@Configuration
public class RestConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

這樣一來,當需要使用時,就可以輕鬆地進行服務呼叫操作。

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping(value = "/findOrderByUserId/{id}")
    public R findOrderByUserId(@PathVariable("id") Integer id) {
        String url = "http://mall-order/order/findOrderByUserId/"+id;
        R result = restTemplate.getForObject(url,R.class);
        return result;
    }
}

注意,mall-order可不是我們說的網站域名,而是我們配置在nacos等註冊中心的服務名。

新增了LoadBalanced註解後,RestTemplate會自動注入所需的依賴項,具體的實現可以透過檢視原始碼來了解。

原始碼分析

關於Spring的自動配置,我之前在講解Spring時提到過很多次,這裡就不再詳細展開了。我們可以看到它實現了SmartInitializingSingleton介面,因此,想要使用負載均衡功能,必須等到所有的bean都載入完畢才能進行。

image

好的,我們可以看到,在這裡他向RestTemplate類新增了一個攔截器。接下來,我們可以探究一下這個攔截器具體做了什麼。我選擇不逐步展示整個過程是因為這並不必要。首先,這樣做記不住,其次,就像處理業務一樣,我們只關注最終走到了哪一個資料表。我們只需記住他一定會經過的那個十字路口即可,這樣可以減輕大腦的負擔。

    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null,
                "Request URI does not contain a valid hostname: " + originalUri);
        return this.loadBalancer.execute(serviceName,
                this.requestFactory.createRequest(request, body, execution));
    }

其實,看到這一點,不用猜也能明白。一旦獲取了serviceName,也就是我們之前定義的微服務名,它會在execute方法中被替換為真正的IP地址,然後最終呼叫HTTP請求完成整個過程。讓我們來仔細看一下原始碼吧。

    public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
            throws IOException {
        ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
        Server server = getServer(loadBalancer, hint);
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        }
        RibbonServer ribbonServer = new RibbonServer(serviceId, server,
                isSecure(server, serviceId),
                serverIntrospector(serviceId).getMetadata(server));

        return execute(serviceId, ribbonServer, request);
    }

所有的負載均衡規則都實現在getServer方法中,我們不再深入追蹤了。在這一步,我們已經找到了要呼叫的微服務。如果你還有疑問,我們可以看一下Server類的具體實現,就能明白了。

image

最終就交給HTTP呼叫即可。然而,僅僅這樣還不夠。難道你會願意在每個服務中編寫這種無關緊要的服務呼叫程式碼嗎?這會非常繁瑣,不僅增加了業務邏輯的複雜性,還讓人感到不便。幸運的是,有了Spring Cloud OpenFeign,這一切變得輕鬆許多。OpenFeign的出現正是為了解決這種服務間通訊的問題,它將這些繁瑣的細節封裝起來。

然而,要注意,這種封裝並不影響通訊的實質。下次我們將詳細討論Spring Cloud OpenFeign與Dubbo呼叫元件的區別與使用方法。

總結

今天我們專注於微服務之間的網路通訊。可以清楚地看到,框架的最終目標是使程式設計師能夠更專注於業務邏輯,而不是被迫寫各種無關緊要的程式碼。總結一下,儘管我們使用了框架和各種抽象,但最終仍然是透過HTTP來進行呼叫。不同的是,在實際呼叫之前,我們引入了一個攔截器來實現微服務的負載均衡。這個攔截器中實現了各種均衡演算法,最終確定真實的IP地址和埠,以便進行訪問並獲取所需的資料。


我是努力的小雨,一名 Java 服務端碼農,潛心研究著 AI 技術的奧秘。我熱愛技術交流與分享,對開源社群充滿熱情。同時也是一位掘金優秀作者、騰訊雲內容共創官、阿里雲專家博主、華為云云享專家。

💡 我將不吝分享我在技術道路上的個人探索與經驗,希望能為你的學習與成長帶來一些啟發與幫助。

🌟 歡迎關注努力的小雨!🌟

🚀 目前,我的探索重點在於 AI Ag

相關文章