简单使用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注册,网关使用的时候去拿。

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

相关推荐
一 乐3 小时前
商城推荐系统|基于SprinBoot+vue的商城推荐系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·商城推荐系统
golang学习记3 小时前
VMware 官宣 彻底免费:虚拟化新时代来临!
后端
绝无仅有3 小时前
某短视频大厂的真实面试解析与总结(一)
后端·面试·github
JavaGuide3 小时前
中兴开奖了,拿到了SSP!
后端·面试
绝无仅有3 小时前
腾讯MySQL面试深度解析:索引、事务与高可用实践 (二)
后端·面试·github
IT_陈寒3 小时前
SpringBoot 3.0实战:这套配置让我轻松扛住百万并发,性能提升300%
前端·人工智能·后端
JaguarJack3 小时前
开发者必看的 15 个困惑的 Git 术语(以及它们的真正含义)
后端·php·laravel
Victor3564 小时前
Redis(91)Redis的访问控制列表(ACL)是如何工作的?
后端
努力进修4 小时前
Rust 语言入门基础教程:从环境搭建到 Cargo 工具链
开发语言·后端·rust