一、先搞懂为什么需要跨服务通信
咱们先想个实际的场景:假设你做的是一个电商平台,拆成了三个独立的小服务——一个专门管用户(比如注册、登录、个人信息),一个专门管商品(比如商品上下架、库存、价格),还有一个专门管订单(比如下单、支付、查订单)。
现在用户要下单,得走一个流程:先检查用户有没有登录(找用户服务),再检查商品库存够不够、价格对不对(找商品服务),最后生成订单(自己这个订单服务的事)。你看,这三个服务各管各的,谁也不知道谁内部的代码,那怎么把它们串起来?这就是跨服务通信要解决的核心问题——让不同的小服务,能像同一个大程序里的不同函数一样,互相叫对方干活,还不能出岔子。
比如用户下单时,要是用户服务挂了,那整个下单流程肯定不能走,不然会生成无效订单;要是商品服务返回的库存是错的,那订单就会超卖;要是订单服务叫完商品服务后,自己出问题了,商品服务那边的库存已经扣了,那库存就少了一笔。所以跨服务通信不能只是“能叫到”,还得“可靠”——不能叫错、不能漏、不能乱。
二、跨服务通信的核心要求
在说具体方案之前,咱们得先明确,什么样的通信才叫“可靠”?我总结了四个必须满足的点:
- 能叫到:只要服务是好的,肯定能把请求发过去,不会发丢;
- 不会乱:一个请求只会被处理一次,不会重复处理(比如扣库存不能扣两次);
- 能知道结果:发完请求后,能明确知道对方成功了还是失败了,不能没下文;
- 不拖后腿:不能因为一个服务慢,把整个流程拖死。
三、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 注意事项
- 超时设置:不管用哪种方式,都要设置超时时间,不能一直等,不然会导致服务卡死;
- 重试机制:对于失败的请求,要设置合理的重试次数,不能无限重试,不然会压垮服务;
- 唯一标识:每个请求或事件都要有唯一标识,用来跟踪整个流程,比如订单ID,这样出问题了能快速定位;
- 幂等性:要保证同一个请求或事件只会被处理一次,比如扣库存,不能扣两次;
- 监控:要对跨服务通信的状态进行监控,比如请求成功率、响应时间、事件堆积情况,出问题了能及时发现。
五、文章总结
跨服务通信是微服务架构里最核心的问题之一,没有最好的方案,只有最适合的方案。如果只是简单的调用,用HTTP就行;如果是内部的中等可靠性的调用,用NestJS的微服务TCP;如果是对可靠性要求极高的核心业务,一定要用事件驱动的方式。
在实现的时候,一定要根据自己的业务场景选择合适的方案,不要盲目追求复杂的技术,也不要为了简单而牺牲可靠性。同时,要注意各种细节,比如超时、重试、幂等性、监控,这些细节才是保证通信可靠的关键。
评论
围绕“NestJS微服务模块跨服务通信的可靠实现方案”参与讨论