Khi ứng dụng của chúng ta phát triển, việc duy trì nó có thể trở nên khó khăn hơn. Một trong những cách tiếp cận mà chúng ta có thể thực hiện là cấu trúc ứng dụng của mình bằng microservices .

Theo dõi source code tại đâybranch dev nha

Source microservices theo dõi tại đây

Link postman để test cho dễ.

Tổng quan về microservice

Bằng cách triển khai kiến ​​trúc microservice, ta chia API của mình thành các thành phần độc lập, nhỏ hơn. Việc có một source Code riêng và triển khai một microservice riêng có thể giúp ứng dụng của chúng ta có khả năng mở rộng hơn. Vì chúng ta triển khai riêng từng microservice nên việc xử lý các bản releases tính năng và sửa lỗi có thể dễ dàng hơn.

Ngoài ra, sử dụng microservices có thể là một cách tiếp cận tốt khi chọn công cụ phù hợp cho công việc. Ví dụ: công ty của bạn có thể quản lý một số microservices trong Java và một số trong Node.js.

Việc xây dựng ứng dụng của chúng ta bằng microservices cũng mang lại sự linh hoạt hơn về mặt triển khai và quản lý tài nguyên giữa các dịch vụ. Việc quản lý di chuyển lược đồ cơ sở dữ liệu cũng có thể dễ dàng hơn nếu chúng ta có nhiều cơ sở dữ liệu nhỏ hơn thay vì một cơ sở dữ liệu nguyên khối.

Tất cả những lợi ích trên cũng đi kèm với một số hạn chế. Mặc dù mỗi microservice đơn giản hơn một ứng dụng nguyên khối, nhưng việc quản lý nhiều ứng dụng nhỏ hơn là một thách thức. Khi cập nhật một microservice, chúng ta cần đảm bảo rằng chúng ta sẽ không làm hỏng các services khác phụ thuộc vào nó.

Chúng ta cũng cần điều chỉnh microservices khi phát triển. Giao tiếp giữa các services không miễn phí và gây ra một chút độ trễ. Nó có thể tăng lên khi số lượng phụ thuộc giữa các microservices tăng lên. Ngoài ra, việc thử nghiệm microservices có thể không dễ dàng.

Triển khai microservice với NestJS

May mắn thay, NestJS đã chuẩn bị sẵn một bộ công cụ để giúp làm việc với microservice dễ dàng hơn. Trong bài viết này, chúng ta tạo ra một microservices đơn giản để quản lý đăng ký email . Sử dụng kiến ​​thức từ loạt bài này, chúng ta tạo một cách để lưu trữ danh sách những người đăng ký email.

Chúng ta phân biệt 2 source bằng cách gọi là project chính và microservice nha, để biết ta đang develop ở đâu.

Tạo microservice

Đầu tiên, chúng ta tạo một project hoàn toàn mới cho microservice của mình. Để bắt đầu, chúng ta sử dụng NestJS CLI .

npm install -g @nestjs/cli
nest new nestjs-email-subscriptions

Chúng ta tạo ra module subcriber để khỏi bị rối khi phát triển thêm trên này, hoặc có thể viết trực tiếp cùng cấp với app module cũng không vấn đề gì cả. Bật cmd / Terminal và nhớ cd vào project nha:

nest g module cats

Và cài một số package cần thiết như mongoose và microservice. Do mình đang dùng mongodb 5 nên mình cài v5 nha, khác version hay bị lỗi mất công.

npm install @nestjs/microservices @nestjs/mongoose@^7.2.4 mongoose@^5.13.14

Phần setup database mình đã hướng dẫn ở những bài trước rồi, nên mình sẽ chỉ code chứ không nói rõ từng bước nữa.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';
import { SubscriberModule } from './subscriber/subscriber.module';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(),
    MongooseModule.forRoot(process.env.MONGODB_URL, {
      useNewUrlParser: true,
      useFindAndModify: false,
      useCreateIndex: true,
    }),
    SubscriberModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
MONGODB_URL=mongodb://localhost:27017/nestjs_email_subscription
PORT=4000

Ta tạo model để lưu trữ email đăng ký:

Lưu ý: những phần liên quan tới subcriber chúng ta viết bên trong module subscriber nha, vì mình đang viết như vậy, các bạn viết ở ngoài cũng được nhưng phải import và config cho đúng.

import { Schema, Document } from 'mongoose'; 

const SubscriberSchema = new Schema(
  {
    name: String,
    email: String,
  },
  { timestamps: true },
);

export { SubscriberSchema };

export interface Subscriber extends Document {
  name: string;
  email: string;
}

Tiếp theo tạo repositor cho subcriber này để chúng ta làm việc với database. Vì chức năng chúng ta xây dựng cũng khá đơn giản, chỉ có việc ghi và đọc dữ liệu thôi, chứ không có logic gì cả.

import { InjectModel } from '@nestjs/mongoose';
import { Subscriber } from './subscriber.model';
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { ObjectID } from 'mongodb';

@Injectable()
export class SubscriberRepository {
  constructor(
    @InjectModel('Subscriber')
    private readonly subscriber: Model<Subscriber>,
  ) {}

  async create(doc): Promise<any> {
    doc._id = new ObjectID();
    return await new this.subscriber(doc).save();
  }

  async getAll() {
    return this.subscriber.find();
  }
}

Để gọi repository ta tạo 1 service nho nhỏ, các bạn lười thì có thể viết trực tiếp vào bên trong controller luôn, nhưng chúng ta cứ viết theo chuẩn để quên cái style này, không thừa đâu:

import { Injectable } from '@nestjs/common';
import { CreateSubscriberDto } from './subscriber.dto';
import { SubscriberRepository } from './subscriber.repository';

@Injectable()
export class SubscriberService {
  constructor(private readonly subscriberRepository: SubscriberRepository) {}

  async addSubscriber(createSubscriberDto: CreateSubscriberDto) {
    return await this.subscriberRepository.create(createSubscriberDto);
  }

  async getAllSubscriber() {
    return this.subscriberRepository.getAll();
  }
}

Khi chúng ta đã sẵn sàng logic cốt lõi, chúng ta có thể bắt đầu suy nghĩ về cách triển khai microservice của mình. Đối với điều đó, chúng ta tạo một controller, nhưng không phải để lắng nghe request và để lắng nghe sự kiện.

Sử dụng lớp TCP

Mặc dù chúng tôi có thể tạo API bằng HTTP, nhưng NestJS đề xuất một cách tiếp cận khác. Thay vì sử dụng HTTP, NestJS có abstraction của riêng nó đối với lớp vận chuyển TCP cho microservices.

Có nhiều cách được đề xuất để kết nối với các microservices NestJS, chẳng hạn như gRPC, … nhưng đó là một chủ đề dành cho các bài viết sau.

Khi khởi động nhiều ứng dụng NestJS vào microservices, NestJS sẽ thiết lập kết nối trước lệnh gọi đầu tiên cho một microservices cụ thể. Sau đó, nó sẽ sử dụng lại nó trong mỗi lần gọi tiếp theo, điều không phải lúc nào cũng đạt được với HTTP.

Ngoài ra, sử dụng TCP theo cách khác cho phép chúng ta đạt được giao tiếp dựa trên sự kiện nếu chúng ta muốn. Bằng cách này, client không phải chờ phản hồi từ microservice.

Cách tiếp cận đầu tiên được đề xuất bởi NestJS là kiểu thông báo  request-response . Nó phù hợp khi chúng ta muốn trao đổi messages giữa các services. Để tạo một xử lý dựa trên message, chúng ta cần decoratorí @MessagePattern().

import { Controller } from '@nestjs/common';
import { SubscriberService } from './subscriber.service';
import { EventPattern, MessagePattern, Payload } from '@nestjs/microservices';
import { CreateSubscriberDto } from './subscriber.dto';

@Controller('subscriber')
export class SubscriberController {
  constructor(private readonly subscriberService: SubscriberService) {}

  @MessagePattern({ cmd: 'add-subscriber' })
  async addSubscriber(@Payload() createSubscriberDto: CreateSubscriberDto) {
    return await this.subscriberService.addSubscriber(createSubscriberDto);
  }

  @MessagePattern({ cmd: 'get-all-subscriber' })
  async getAllSubscriber() {
    return await this.subscriberService.getAllSubscriber();
  }
}

Ta tạo DTO cho phần đang ký email

export class CreateSubscriberDto {
  name: string;
  email: string;
}

Ở trên, trình xử lý của chúng ta lắng nghe các thông báo khớp với mẫu được cung cấp. Ví dụ, một mẫu là một giá trị đơn giản có thể là một đối tượng hoặc một chuỗi. NestJS gửi chúng cùng với dữ liệu:

  • nếu { cmd : ‘add-subscriber’ } là pattern, thì phương thức addSubscriber được gọi
  • nếu { cmd : ‘get-all-subscribers’ } là pattern, thì phương thức getAllSubscribers được gọi

Và khai báo toàn bộ vào SubscriberModule

import { Module } from '@nestjs/common';
import { SubscriberController } from './subscriber.controller';
import { SubscriberService } from './subscriber.service';
import { MongooseModule } from '@nestjs/mongoose';
import { SubscriberSchema } from './subscriber.model';
import { SubscriberRepository } from './subscriber.repository';

@Module({
  imports: [
    MongooseModule.forFeature([
      {
        name: 'Subscriber',
        schema: SubscriberSchema,
      },
    ]),
  ],
  controllers: [SubscriberController],
  providers: [SubscriberService, SubscriberRepository],
})
export class SubscriberModule {}

Điều cuối cùng cần làm là đăng ký microservice của chúng ta. Để làm điều đó, hãy sửa đổi chức năng bootstrap mặc định trong file main.ts


import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { ConfigService } from '@nestjs/config';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService);
 
  await app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.TCP,
    options: {
      port: configService.get('PORT'),
    },
  });
 
  app.startAllMicroservices();
}
bootstrap();

Thêm microservice vào project chính

Có thể có trường hợp chúng ta không nghĩ đến việc thiết kế ứng dụng của mình với microservice ngay từ đầu. Trong trường hợp này, chúng ta muốn kết nối project của mình với một microservice mới.

Để làm điều đó, hãy tạo một SubscribersModule bên trong ứng dụng NestJS nguyên khối của chúng ta.

Chúng ta cũng cần phải cài package để có thể kết nối được:

npm install @nestjs/microservices

Và đồng thời cũng tạo module subscriber, và đăng ký SUBSCRIBERS_SERVICE

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ClientProxyFactory, Transport } from '@nestjs/microservices';
import { SubscriberController } from './subscriber.controller';

@Module({
  imports: [ConfigModule],
  controllers: [SubscriberController],
  providers: [
    {
      provide: 'SUBSCRIBER_SERVICE',
      useFactory: (configService: ConfigService) =>
        ClientProxyFactory.create({
          transport: Transport.TCP,
          options: {
            host: configService.get('SUBSCRIBER_SERVICE_HOST'),
            port: configService.get('SUBSCRIBER_SERVICE_PORT'),
          },
        }),
      inject: [ConfigService],
    },
  ],
})
export class SubscriberModule {}

Thêm config vào file .env


SUBSCRIBER_SERVICE_HOST=localhost
SUBSCRIBER_SERVICE_PORT=4000

Sau cùng ta tạo controller để giao tiếp với microservice

import { Controller, Get, Inject, Post, Req, UseGuards } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { AuthGuard } from '@nestjs/passport';

@Controller('subscriber')
export class SubscriberController {
  constructor(
    @Inject('SUBSCRIBER_SERVICE')
    private readonly subscriberService: ClientProxy,
  ) {}

  @Get()
  @UseGuards(AuthGuard('jwt'))
  async getSubscribers() {
    return this.subscriberService.send(
      {
        cmd: 'get-all-subscriber',
      },
      {},
    );
  }

  @Post()
  @UseGuards(AuthGuard('jwt'))
  async createSubscriberTCP(@Req() req: any) {
    return this.subscriberService.send(
      {
        cmd: 'add-subscriber',
      },
      req.user,
    );
  }
}

Chúng ta cũng có thể tạo một microservice chuyên dụng thay vì sử dụng ClientProxy trực tiếp trong controller. Điều này sẽ cho chúng tôi khả năng export nó từ SubscribersModule và sử dụng ở những nơi khác trong ứng dụng của chúng ta.

Phải authen thì mới có thể call api được vì mình đang dùng AuthGuard, tham khảo tại đây

Ở trên, chúng ta đã tạo quy trình sau:

  1. Người dùng gọi  api /subscriber trong project chính,
  2. Ứng dụng của chúng ta gọi microservice để lấy dữ liệu cần thiết,
  3. Microservice lấy dữ liệu từ cơ sở dữ liệu của chính nó,
  4. Project chính của chúng ta trả dữ liệu cho client.

Trong SubscriberController của chúng ta , chỉ người dùng đã đăng nhập mới có thể liệt kê đăng ký email. Chúng ta đã không triển khai bất kỳ cơ chế xác thực nào bên trong microservice của chúng ta để xử lý đăng ký email. Điều này là do chúng ta dự định nó là một  private API . Chúng ta chỉ muốn ứng dụng chính của mình có thể giao tiếp với nó.

Sử dụng giao tiếp event-based

Ngoài việc sử dụng @MessagePattern() , chúng ta cũng có thể triển khai giao tiếp event-based. Điều này phù hợp với những trường hợp chúng ta không muốn đợi phản hồi. Chúng tôi có thể làm như vậy trong trường hợp register email

Bên trong controller của microservice, ta khai báo nơi lắng nghe event:

@EventPattern({ cmd: 'add-subscriber' })
async eventAddSubscriber(createSubscriberDto: CreateSubscriberDto) {
    return this.subscriberService.addSubscriber(createSubscriberDto);
}

Sau đó bổ sung api bắn sự kiện bên trong project chính

@Post('event')
  @UseGuards(AuthGuard('jwt'))
  async createSubscriberEvent(@Req() req: any) {
    this.subscriberService.emit(
      {
        cmd: 'add-subscriber',
      },
      req.user,
    );
    return true;
  }

Ngay cả khi điều này có thể hiệu quả hơn, tuy nhiên, việc triển khai những điều trên có thể không phải là điều chúng ta muốn trong trường hợp của mình. Vì chúng ta không chờ phản hồi nên chúng tôi sẽ không biết liệu việc thêm người đăng ký có thành công hay không. Ngay cả khi có, chúng ta sẽ không nhận được thông tin chi tiết như id.

Kết luận

Trong bài viết này, mình đã giới thiệu ý tưởng về microservice. Điều này bao gồm việc tạo một microservice đơn giản và kết nối nó với một ứng dụng hiện có. Mặc dù đây là một trường hợp sử dụng phổ biến, nhưng chúng ta cũng có thể thiết kế kiến ​​trúc với các microservices ngay từ đầu. Mình sẽ giới thiệu đến các bạn những cách giao tiếp khác giữa các services trong các bài viết sắp tới.

Rất mong được sự ủng hộ của mọi người để mình có động lực ra những bài viết tiếp theo.
{\__/}
( ~.~ )
/ > ♥️ I LOVE YOU 3000

JUST DO IT!