一、先搞懂为什么需要跨服务通信

咱们先想个实际的场景:假设你做的是一个电商平台,拆成了三个独立的小服务——一个专门管用户(比如注册、登录、个人信息),一个专门管商品(比如商品上下架、库存、价格),还有一个专门管订单(比如下单、支付、查订单)。

现在用户要下单,得走一个流程:先检查用户有没有登录(找用户服务),再检查商品库存够不够、价格对不对(找商品服务),最后生成订单(自己这个订单服务的事)。你看,这三个服务各管各的,谁也不知道谁内部的代码,那怎么把它们串起来?这就是跨服务通信要解决的核心问题——让不同的小服务,能像同一个大程序里的不同函数一样,互相叫对方干活,还不能出岔子。

比如用户下单时,要是用户服务挂了,那整个下单流程肯定不能走,不然会生成无效订单;要是商品服务返回的库存是错的,那订单就会超卖;要是订单服务叫完商品服务后,自己出问题了,商品服务那边的库存已经扣了,那库存就少了一笔。所以跨服务通信不能只是“能叫到”,还得“可靠”——不能叫错、不能漏、不能乱。

二、跨服务通信的核心要求

在说具体方案之前,咱们得先明确,什么样的通信才叫“可靠”?我总结了四个必须满足的点:

  1. 能叫到:只要服务是好的,肯定能把请求发过去,不会发丢;
  2. 不会乱:一个请求只会被处理一次,不会重复处理(比如扣库存不能扣两次);
  3. 能知道结果:发完请求后,能明确知道对方成功了还是失败了,不能没下文;
  4. 不拖后腿:不能因为一个服务慢,把整个流程拖死。

三、NestJS跨服务通信的具体实现方案

3.1 方案1:用HTTP直接调用(最简单的方式)

先给大家说最容易想到的——HTTP调用,就像我们平时浏览器调接口一样,服务A发个HTTP请求给服务B,服务B返回结果。这个方式的好处是完全通用,不管服务用什么技术栈写的都能调,但缺点是速度慢,而且容易出各种问题,比如超时、重试、重复请求这些都得自己处理。

咱们先看具体的实现,技术栈统一用NestJS + Axios(用来发HTTP请求的工具)。

首先,假设我们有两个服务:用户服务(端口3001)和订单服务(端口3002)。现在订单服务要调用用户服务的“检查登录状态”接口。

第一步,先在订单服务里装Axios:

npm install axios

第二步,在订单服务里写一个专门用来调用用户服务的服务,比如叫UserService:

import { Injectable } from '@nestjs/common';
import axios from 'axios';

@Injectable()
export class UserService {
  // 定义用户服务的基础地址,这里可以改成配置文件里的内容,方便改
  private readonly USER_SERVICE_URL = 'http://localhost:3001';

  // 检查用户登录状态的方法
  async checkUserLogin(userId: number): Promise<boolean> {
    try {
      // 发GET请求到用户服务的/check-login接口,把userId传过去
      const response = await axios.get(`${this.USER_SERVICE_URL}/check-login`, {
        params: { userId },
        // 设置超时时间,比如5秒,超过5秒没响应就报错
        timeout: 5000,
      });
      // 返回用户服务的结果
      return response.data.isLogin;
    } catch (error) {
      // 这里可以做错误处理,比如重试、记日志
      console.error('调用用户服务检查登录失败', error);
      throw new Error('用户服务不可用');
    }
  }
}

第三步,订单服务里调用这个UserService:

import { Injectable } from '@nestjs/common';
import { UserService } from './user.service';

@Injectable()
export class OrderService {
  constructor(private readonly userService: UserService) {}

  async createOrder(userId: number, productId: number) {
    // 第一步:检查用户是否登录
    const isLogin = await this.userService.checkUserLogin(userId);
    if (!isLogin) {
      throw new Error('用户未登录');
    }
    // 后面再调用商品服务检查库存、生成订单...
  }
}

这个方式虽然简单,但问题很多:比如如果订单服务调用用户服务时,网络断了,那请求发不出去;如果用户服务收到请求但处理到一半挂了,订单服务不知道;如果订单服务因为网络波动发了两次请求,用户服务可能会重复处理(比如重复扣积分)。所以这个方式只适合对可靠性要求不高的场景,比如查个商品列表这种,就算失败了重试也没关系。

3.2 方案2:用NestJS的微服务模块(更可靠的方式)

NestJS自己有一套微服务的模块,专门用来解决跨服务通信的问题,比HTTP可靠多了。它的核心原理是用一个中间件(叫传输层)来传递消息,不用我们自己处理HTTP的各种问题。

咱们还是用刚才的电商场景,技术栈用NestJS微服务模块 + TCP传输(TCP是一种更可靠的网络协议,比HTTP稳定)。

首先,得先把两个服务改成NestJS的微服务。

第一步,改造用户服务(接收请求的一方,叫“服务端”):

import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  // 创建微服务实例,用TCP传输,监听3001端口
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.TCP,
      options: {
        host: 'localhost',
        port: 3001,
      },
    },
  );
  await app.listen();
}
bootstrap();

然后,在用户服务的控制器里写要被调用的方法,用@MessagePattern装饰器标记:

import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';

@Controller()
export class UserController {
  // 标记这个方法可以被外部调用,调用的标识是'check_user_login'
  @MessagePattern('check_user_login')
  async checkUserLogin(userId: number): Promise<boolean> {
    // 这里写实际的检查逻辑,比如查数据库看用户是否登录
    console.log('收到检查登录的请求,userId:', userId);
    return userId > 0; // 假设userId大于0就是已登录
  }
}

第二步,改造订单服务(发送请求的一方,叫“客户端”): 首先,在订单服务里装NestJS的微服务依赖:

npm install @nestjs/microservices

然后,在订单服务的根模块(AppModule)里注册和用户服务的连接:

import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { OrderService } from './order.service';

@Module({
  imports: [
    // 注册一个叫USER_SERVICE的客户端,用来连接用户服务
    ClientsModule.register([
      {
        name: 'USER_SERVICE',
        transport: Transport.TCP,
        options: {
          host: 'localhost',
          port: 3001,
        },
      },
    ]),
  ],
  providers: [OrderService],
})
export class AppModule {}

接着,在订单服务的OrderService里注入这个客户端,调用用户服务的方法:

import { Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { Inject } from '@nestjs/common';

@Injectable()
export class OrderService {
  // 注入刚才注册的USER_SERVICE客户端
  constructor(
    @Inject('USER_SERVICE') private readonly userClient: ClientProxy,
  ) {}

  async createOrder(userId: number, productId: number) {
    try {
      // 发送请求到用户服务,调用标识是'check_user_login',传userId
      const isLogin = await this.userClient.send('check_user_login', userId).toPromise();
      console.log('用户登录状态:', isLogin);
      if (!isLogin) {
        throw new Error('用户未登录');
      }
      // 后面再调用商品服务、生成订单...
    } catch (error) {
      console.error('调用用户服务失败', error);
      throw new Error('下单失败');
    }
  }
}

这个方式比HTTP可靠很多,因为NestJS的微服务模块会帮我们处理很多问题:比如超时、重试、连接池(不用每次都重新建立连接,速度更快)。但它还有个问题:如果订单服务调用用户服务时,用户服务处理到一半挂了,订单服务会收到错误,知道失败了,但用户服务那边可能已经做了一半的操作(比如扣了积分),这就会导致数据不一致。

3.3 方案3:用事件驱动(最终的可靠方案)

要解决刚才的问题,就得用事件驱动的方式,核心原理是:把“调用”改成“发事件”,每个服务自己监听事件,处理完后再发新的事件,所有的操作都通过事件流转,就算中间某个服务挂了,事件也会被存起来,等服务恢复后再处理,不会丢。

咱们还是用电商的场景,技术栈用NestJS微服务模块 + RabbitMQ(消息队列,用来存事件的)。

首先,得先装RabbitMQ,然后启动它。

第一步,改造用户服务: 把之前的TCP传输改成RabbitMQ,并且监听“检查用户登录”的事件:

import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.RMQ,
      options: {
        urls: ['amqp://localhost:5672'], // RabbitMQ的地址
        queue: 'user_queue', // 事件存在这个队列里
        queueOptions: {
          durable: true, // 队列持久化,就算RabbitMQ挂了,事件也不会丢
        },
      },
    },
  );
  await app.listen();
}
bootstrap();

然后,在用户服务的控制器里监听事件:

import { Controller } from '@nestjs/common';
import { EventPattern } from '@nestjs/microservices';

@Controller()
export class UserController {
  // 监听'check_user_login'事件
  @EventPattern('check_user_login')
  async checkUserLogin(data: { userId: number; orderId: string }) {
    const { userId, orderId } = data;
    console.log('收到检查登录事件,userId:', userId, 'orderId:', orderId);
    // 检查登录状态
    const isLogin = userId > 0;
    // 处理完后,发一个新的事件:检查登录结果
    await this.sendEvent('check_user_login_result', {
      orderId,
      isLogin,
    });
  }

  // 发事件的方法
  private async sendEvent(pattern: string, data: any) {
    // 这里用RabbitMQ发事件,后面订单服务会监听这个结果
  }
}

第二步,改造订单服务: 订单服务先发起“检查用户登录”的事件,然后监听“检查登录结果”的事件,根据结果决定下一步操作。

订单服务的根模块注册RabbitMQ客户端:

import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { OrderService } from './order.service';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'EVENT_SERVICE',
        transport: Transport.RMQ,
        options: {
          urls: ['amqp://localhost:5672'],
          queue: 'event_queue',
          queueOptions: {
            durable: true,
          },
        },
      },
    ]),
  ],
  providers: [OrderService],
})
export class AppModule {}

订单服务的OrderService:

import { Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { Inject } from '@nestjs/common';

@Injectable()
export class OrderService {
  constructor(
    @Inject('EVENT_SERVICE') private readonly eventClient: ClientProxy,
  ) {}

  // 发起下单流程
  async createOrder(userId: number, productId: number) {
    // 生成一个唯一的订单ID,用来跟踪整个流程
    const orderId = Date.now().toString();
    console.log('开始下单,订单ID:', orderId);
    // 发检查用户登录的事件
    await this.eventClient.emit('check_user_login', {
      orderId,
      userId,
      productId,
    });
  }

  // 监听检查登录结果的事件
  @EventPattern('check_user_login_result')
  async handleLoginResult(data: { orderId: string; isLogin: boolean }) {
    const { orderId, isLogin } = data;
    console.log('收到登录结果,订单ID:', orderId, '是否登录:', isLogin);
    if (!isLogin) {
      console.log('下单失败:用户未登录');
      return;
    }
    // 登录成功,发检查商品库存的事件
    await this.eventClient.emit('check_product_stock', {
      orderId,
      productId: 123, // 这里可以从之前的事件里拿到
    });
  }
}

这个方式的好处是:所有的事件都存在RabbitMQ里,就算某个服务挂了,事件也不会丢,等服务恢复后会继续处理;而且每个事件只会被处理一次(RabbitMQ会保证),不会重复;整个流程是异步的,不会因为一个服务慢把整个流程拖死。

四、应用场景、优缺点和注意事项

4.1 应用场景

  • HTTP调用:适合对可靠性要求不高的场景,比如查询商品列表、获取用户公开信息,就算失败了重试也没关系;
  • NestJS微服务TCP:适合对可靠性要求中等的场景,比如内部的接口调用,比如订单服务调用商品服务查库存(但不能保证数据完全一致);
  • 事件驱动(RabbitMQ):适合对可靠性要求极高的场景,比如下单、支付、扣库存,这些操作不能出错,必须保证数据一致。

4.2 技术优缺点

  • HTTP调用:优点是简单、通用;缺点是速度慢、可靠性差、需要自己处理各种异常;
  • NestJS微服务TCP:优点是速度快、NestJS帮我们处理了很多异常;缺点是不能保证数据一致,要是中间某个服务挂了,会导致数据不一致;
  • 事件驱动(RabbitMQ):优点是可靠性极高、能保证数据一致、异步处理;缺点是实现复杂、需要额外维护RabbitMQ、调试麻烦。

4.3 注意事项

  1. 超时设置:不管用哪种方式,都要设置超时时间,不能一直等,不然会导致服务卡死;
  2. 重试机制:对于失败的请求,要设置合理的重试次数,不能无限重试,不然会压垮服务;
  3. 唯一标识:每个请求或事件都要有唯一标识,用来跟踪整个流程,比如订单ID,这样出问题了能快速定位;
  4. 幂等性:要保证同一个请求或事件只会被处理一次,比如扣库存,不能扣两次;
  5. 监控:要对跨服务通信的状态进行监控,比如请求成功率、响应时间、事件堆积情况,出问题了能及时发现。

五、文章总结

跨服务通信是微服务架构里最核心的问题之一,没有最好的方案,只有最适合的方案。如果只是简单的调用,用HTTP就行;如果是内部的中等可靠性的调用,用NestJS的微服务TCP;如果是对可靠性要求极高的核心业务,一定要用事件驱动的方式。

在实现的时候,一定要根据自己的业务场景选择合适的方案,不要盲目追求复杂的技术,也不要为了简单而牺牲可靠性。同时,要注意各种细节,比如超时、重试、幂等性、监控,这些细节才是保证通信可靠的关键。