深入探讨微服务架构中的同步通信机制

微服务架构是一种设计方法,将应用程序划分为一组小型服务,每个服务在独立的进程中运行,通常根据业务能力进行组织。这些服务通过多种通信方式交互,以实现整个应用的功能。今天我们着重介绍同步通信,关于异步通信和消息队列(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

posted @ 2024-08-09 09:39  努力的小雨  阅读(505)  评论(0编辑  收藏  举报