简单使用Nest+Nacos+Kafka实现微服务

简单实用Nest+Nacos+Kafka+Docker实现微服务架构

PS:并非实战,一个练习。

设计

一个网关,一个用户服务,一个邮件推送服务。

用户服务部署两份,邮件推送服务部署两份。

用户服务和邮件推送服务启动的时候,通过nacos拉取配置,如

新建三个Nest服务

bash 复制代码
nest new gateway
cd gateway
nest g app user-service
nest g app email-service

Nacos

配置中心

新增lib:remote-config
nest g lib remote-config

根目录新增.env

service.ts

ts 复制代码
import {
  Inject,
  Injectable,
  Logger,
  OnModuleDestroy,
  OnModuleInit,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NacosConfigClient } from 'nacos';

@Injectable()
export class RemoteConfigService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(RemoteConfigService.name);
  private client: NacosConfigClient;

  @Inject()
  private configService: ConfigService;

  constructor() {}

  async onModuleInit() {
    try {
      this.client = new NacosConfigClient({
        serverAddr: this.configService.get<string>('NACOS_SERVER'),
        namespace: this.configService.get<string>('NACOS_NAMESPACE'),
        username: this.configService.get<string>('NACOS_SECRET_NAME'),
        password: this.configService.get<string>('NACOS_SECRET_PWD'),
      });

      this.logger.log('Nacos配置客户端初始化成功');
    } catch (error) {
      this.logger.error('Nacos配置客户端初始化失败', error);
    }
  }

  async onModuleDestroy() {
    await this.client.close();
  }
  async getConfig(
    dataId: string,
    group = this.configService.get('NACOS_GROUP_NAME'),
  ) {
    if (this.configService.get(dataId)) {
      return await this.configService.get(dataId);
    }
    this.watchConfig(dataId, group);
    const config = this.parseConfig(
      await this.client.getConfig(dataId, group),
      'json',
    );
    return config;
  }

  /**
   * 监听配置变化
   */
  watchConfig(
    dataId: string,
    group = this.configService.get('NACOS_GROUP_NAME'),
  ) {
    this.client.subscribe(
      {
        dataId,
        group,
      },
      (content, configType = 'json') => {
        const config = this.parseConfig(content, configType);
        this.configService.set(dataId, config);
      },
    );
  }

  /**
   * 解析配置内容
   */
  private parseConfig(content: string, type: string): any {
    try {
      if (type === 'json') {
        return JSON.parse(content);
      } else if (type === 'yaml' || type === 'yml') {
        // 简单的YAML解析,实际项目中可以使用js-yaml等库
        const config = {};
        content.split('\n').forEach((line) => {
          const parts = line.split(':').map((part) => part.trim());
          if (parts.length >= 2) {
            config[parts[0]] = parts.slice(1).join(':');
          }
        });
        return config;
      } else if (type === 'properties') {
        const config = {};
        content.split('\n').forEach((line) => {
          const parts = line.split('=').map((part) => part.trim());
          if (parts.length >= 2) {
            config[parts[0]] = parts.slice(1).join('=');
          }
        });
        return config;
      }
      return content;
    } catch (error) {
      this.logger.error('配置解析失败', error);
      return content;
    }
  }
}

module.ts

ts 复制代码
import { Module } from '@nestjs/common';
import { RemoteConfigService } from './remote-config.service';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: ['.env'],
    }),
  ],
  providers: [RemoteConfigService],
  exports: [RemoteConfigService],
})
export class RemoteConfigModule {}
尝试

在nacos web管理页面 新增配置

服务注册

nest g lib nacos

新增一个ip.service.ts

ts 复制代码
// src/utils/ip.service.ts
import { Injectable } from '@nestjs/common';
import * as os from 'os';

@Injectable()
export class IpService {
  /**
   * 获取本机非内部 IPv4 地址(优先返回第一个有效地址)
   * @returns 本机 IP 地址(如 192.168.1.100),无有效地址时返回 127.0.0.1
   */
  getLocalIp(): string {
    const interfaces = os.networkInterfaces(); // 获取所有网络接口
    let localIp = '127.0.0.1'; // 默认回环地址

    // 遍历所有网络接口
    for (const devName in interfaces) {
      const iface = interfaces[devName];
      if (!iface) continue;

      // 遍历接口下的所有地址
      for (const alias of iface) {
        // 筛选条件:IPv4、非内部地址(非 127.0.0.1 等回环地址)、已启动
        if (
          alias.family === 'IPv4' && // 只取 IPv4
          !alias.internal && // 排除内部地址(如 127.0.0.1)
          alias.address !== '127.0.0.1' && // 进一步排除回环地址
          !alias.address.startsWith('169.254.') // 排除 APIPA 地址(本地链路地址)
        ) {
          localIp = alias.address;
          return localIp; // 找到第一个有效地址后返回
        }
      }
    }

    return localIp; // 未找到有效地址时返回回环地址
  }
}

nacos.module.ts

动态模块,传入参数

ts 复制代码
import { DynamicModule, Module } from '@nestjs/common';
import { NacosOptions, NacosService } from './nacos.service';
import { IpService } from './ip.service';

@Module({})
export class NacosModule {
  static forRoot(options: NacosOptions): DynamicModule {
    return {
      module: NacosModule,
      providers: [
        IpService,
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,
        },
        NacosService,
      ],
      exports: [NacosService],
    };
  }
}

nacos.service.ts

ts 复制代码
import {
  Inject,
  Injectable,
  Logger,
  OnModuleDestroy,
  OnModuleInit,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NacosNamingClient } from 'nacos';
import { IpService } from './ip.service';

interface Instance {
  instanceId: string;
  healthy: boolean;
  enabled: boolean;
  serviceName?: string;
  weight?: number;
  ephemeral?: boolean;
  clusterName?: string;
}

export interface NacosOptions {
  noRegister?: boolean;
  serviceName: string; //Service name
  instance?: Partial<Instance>; //Instance
  groupName?: string;
  subList?: { groupName?: string; dataId: string }[];
}

@Injectable()
export class NacosService implements OnModuleInit, OnModuleDestroy {
  @Inject()
  private configService: ConfigService;

  @Inject(IpService)
  private ipService: IpService;

  @Inject('CONFIG_OPTIONS')
  private options: NacosOptions;

  private readonly logger = new Logger(NacosService.name);
  private client: NacosNamingClient;
  async onModuleInit() {
    this.client = new NacosNamingClient({
      logger: {
        ...console,
        log: () => {},
        debug: () => {},
      },
      serverList: this.configService.get<string>('NACOS_SERVER'), // replace to real nacos serverList
      namespace: this.configService.get<string>('NACOS_NAMESPACE'),
      username: this.configService.get<string>('NACOS_SECRET_NAME'),
      password: this.configService.get<string>('NACOS_SECRET_PWD'),
    });
    await this.client.ready();
    if (!this.options.noRegister) {
      await this.destroy();
      await this.register();
    }
    this.logger.log('Nacos客户端准备就绪');
  }

  async getSub(serviceName: string) {
    const service = {
      dataId: serviceName,
      groupName: this.configService.get('NACOS_GROUP_NAME'),
    };
    const res = await this.client.getAllInstances(
      service.dataId,
      service.groupName,
    );
    this.configService.set(`nacos_${serviceName}`, res);
    return res.filter((item) => item.healthy);
  }

  async register() {
    await this.client.registerInstance(
      this.options.serviceName,
      // @ts-ignore
      {
        ...this.options.instance,
        ip: this.ipService.getLocalIp(),
        port: this.configService.get<number>('PORT'),
      },
      this.options.groupName,
    );
  }
  async destroy() {
    await this.client.deregisterInstance(
      this.options.serviceName,
      // @ts-ignore
      {
        ...this.options.instance,
        ip: this.ipService.getLocalIp(),
        port: this.configService.get<number>('PORT'),
      },
      this.options.groupName,
    );
  }

  async onModuleDestroy() {}
}
尝试

在 user-service服务

启动服务

这个时候就可以在其他服务获取到user-service的请求地址,可以使用http请求来处理。

改造user-service (TCP)

将user-service改造成微服务,使用TCP

ts 复制代码
import { NestFactory } from '@nestjs/core';
import { UserServiceModule } from './user-service.module';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';

async function bootstrap() {
  const port = Number(process.env.PORT) || 3001;
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    UserServiceModule,
    {
      transport: Transport.TCP,
      options: {
        port,
      },
    },
  );
  await app.listen();
}
bootstrap();

修改user-service.controller.ts

ts 复制代码
import { Controller, Get } from '@nestjs/common';
import { UserServiceService } from './user-service.service';
import { MessagePattern } from '@nestjs/microservices';

@Controller()
export class UserServiceController {
  constructor(private readonly userServiceService: UserServiceService) {}

  @MessagePattern('user')
  handleFindById() {
    return this.userServiceService.register();
  }
}

启动服务

这里因为是本地,需要修改下获取IP的逻辑

改造gateway

引入nacos

app.service.ts中 注册微服务

ts 复制代码
import { NacosService } from '@app/nacos';
import { Inject, Injectable, OnApplicationBootstrap } from '@nestjs/common';
import {
  ClientProxyFactory,
  Transport,
  ClientProxy,
} from '@nestjs/microservices';

@Injectable()
export class AppService implements OnApplicationBootstrap {
  private client: ClientProxy;
  @Inject(NacosService)
  private readonly nacosService: NacosService;

  async onApplicationBootstrap() {
    const res = await this.nacosService.getSub('user-service');
    const instance = res[0];

    this.client = ClientProxyFactory.create({
      transport: Transport.TCP,
      options: {
        host: instance.ip,
        port: instance.port,
      },
    });
    await this.client.connect();
  }

  getHello(): string {
    return 'Hello World!';
  }
  async getInstance() {
    const res = await this.client.send('user', '');
    return !res;
  }
}

启动服务

改造email-service(Kafka)

service.ts

这里打印接收的数据

ts 复制代码
import { Controller, Get, Logger } from '@nestjs/common';
import { EmailServiceService } from './email-service.service';
import { MessagePattern, Payload } from '@nestjs/microservices';

@Controller()
export class EmailServiceController {
  constructor(private readonly emailServiceService: EmailServiceService) {}

  @MessagePattern('email')
  getHello(@Payload() data) {
    Logger.log('email-service', data);
  }
}

对于 main.ts

需要先创建一个临时configApp,用来获取kafka-config

ts 复制代码
import { NestFactory } from '@nestjs/core';
import { EmailServiceModule } from './email-service.module';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { RemoteConfigModule, RemoteConfigService } from '@app/remote-config';

async function bootstrap() {
  const configApp = await NestFactory.create(RemoteConfigModule);
  await configApp.init();
  const configService = configApp.get<RemoteConfigService>(RemoteConfigService);
  const res = await configService.getConfig('kafka-config');

  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    EmailServiceModule,
    {
      transport: Transport.KAFKA,
      options: {
        client: {
          clientId: 'email-service',
          brokers: res.brokers,
        },
        consumer: {
          groupId: 'email-service',
        },
      },
    },
  );
  await app.listen();
  await configApp.close();
}
bootstrap();

在gateway使用
app.servite.ts

ts 复制代码
import { NacosService } from '@app/nacos';
import { RemoteConfigService } from '@app/remote-config';
import {
  Inject,
  Injectable,
  Logger,
  OnApplicationBootstrap,
  OnModuleDestroy,
} from '@nestjs/common';
import {
  ClientProxyFactory,
  Transport,
  ClientProxy,
  ClientKafka,
} from '@nestjs/microservices';

@Injectable()
export class AppService implements OnApplicationBootstrap, OnModuleDestroy {
  private client: ClientProxy;
  private kafkaClient: ClientKafka;
  @Inject(NacosService)
  private readonly nacosService: NacosService;
  @Inject(RemoteConfigService)
  private readonly configService: RemoteConfigService;

  async onApplicationBootstrap() {
    const res = await this.nacosService.getSub('user-service');
    const instance = res[0];

    this.client = ClientProxyFactory.create({
      transport: Transport.TCP,
      options: {
        host: instance.ip,
        port: instance.port,
      },
    });
    await this.client.connect();
    Logger.log(`user-service 连接成功`);
    const config = await this.configService.getConfig('kafka-config');
    this.kafkaClient = new ClientKafka({
      client: {
        brokers: config.brokers,
      },
      producer: {
        allowAutoTopicCreation: true,
      },
    });

    await this.kafkaClient.connect();
    Logger.log(`email-service 连接成功`);
  }
  async onModuleDestroy() {
    await this.kafkaClient.close(); // 自动断开连接(避免资源泄漏)
  }

  getHello(): string {
    return 'Hello World!';
  }
  async getInstance() {
    const res = await this.client.send('user', '');
    return !res;
  }
  sendEmail() {
    this.kafkaClient.emit('email', {
      email: '<EMAIL>',
      content: '测试发送邮件',
    });
  }
}

测试一下

如果需要返回邮件的发送状态。如返回true

如果直接这样修改。

会报错。

send的话,接收reply,需要先订阅下。

修改app.service.ts

在kafkaClient连接之前。

本地测试email-service开启多个实例:

当关掉一个后,接下来的请求会发送到另外一个上面

同样的也可以给email-service加上Nacos

因为本地启动多个相同服务的原因,临时给nacos加了个随机数,

可以判断是否有健康的实例来做进一步处理。

其他

这是一个练习。

其他的,比如,服务下线后,nacos是能检测到健康状态的,那么就要通过配置或者服务变化(nacos有订阅),来通知使用者如gateway。

部署

如通过docker打包,注入环境变量port等等,应用会通过nacos注册,网关使用的时候去拿。

这样就实现了服务的注册和发现?

相关推荐
serendipity_hky7 分钟前
【SpringCloud | 第2篇】OpenFeign远程调用
java·后端·spring·spring cloud·openfeign
嘟嘟MD11 分钟前
程序员副业 | 2025年11月复盘
后端·创业
SadSunset12 分钟前
(15)抽象工厂模式(了解)
java·笔记·后端·spring·抽象工厂模式
汝生淮南吾在北14 分钟前
SpringBoot+Vue养老院管理系统
vue.js·spring boot·后端·毕业设计·毕设
李慕婉学姐41 分钟前
【开题答辩过程】以《基于springboot的地铁综合服务管理系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·spring boot·后端
期待のcode1 小时前
Springboot配置属性绑定
java·spring boot·后端
海上彼尚1 小时前
Go之路 - 6.go的指针
开发语言·后端·golang
hour_go1 小时前
微服务架构的故障演练数字化:方法解析与实践优势
微服务·云原生·架构
LYFlied2 小时前
在AI时代,前端开发者如何构建全栈开发视野与核心竞争力
前端·人工智能·后端·ai·全栈
用户47949283569152 小时前
我只是给Typescript提个 typo PR,为什么还要签协议?
前端·后端·开源