☰ Mục lục series
Tài liệu NestJS tiếng Việt
  1. Nhập môn (Overview)
    1. Giới thiệu
    2. First steps
    3. Controllers
    4. Providers
    5. Modules
    6. Middleware
    7. Exception filters
    8. Pipes
    9. Guards
    10. Interceptors
    11. Custom decorators
  2. Nền tảng (Fundamentals)
    1. Custom providers
    2. Asynchronous providers
    3. Dynamic modules
    4. Injection scopes
    5. Circular dependency
    6. Module reference
    7. Lazy-loading modules
    8. Execution context
    9. Lifecycle events
    10. Discovery service
    11. Platform agnosticism
    12. Testing
  3. Kỹ thuật (Techniques)
    1. Configuration
    2. Database (SQL)
    3. MongoDB
    4. Validation
    5. Caching
    6. Serialization
    7. Versioning
    8. Task scheduling
    9. Queues
    10. Logging
    11. Cookies
    12. Events
    13. Compression
    14. File upload
    15. Streaming files
    16. HTTP module
    17. Session
    18. MVC
    19. Performance (Fastify)
    20. Server-Sent Events
  4. Bảo mật (Security)
    1. Authentication
    2. Authorization
    3. Encryption & Hashing
    4. Helmet
    5. CORS
    6. CSRF
    7. Rate limiting
    8. Vòng đời request
  5. OpenAPI (Swagger)
    1. Introduction
    2. Types and parameters
    3. Operations
    4. Security
    5. Mapped types
    6. Decorators
    7. CLI plugin
    8. Other features
  6. Recipes
    1. REPL
    2. CRUD generator
    3. SWC (fast compiler)
    4. Passport (auth)
    5. Hot reload
    6. MikroORM
    7. Router module
    8. Health checks (Terminus)
    9. CQRS
    10. Prisma
    11. Serve static
    12. Commander
    13. Async local storage
  7. WebSockets
    1. Gateways
    2. Exception filters
    3. Pipes
    4. Guards
    5. Interceptors
    6. Adapters
  8. Microservices
    1. Overview
    2. Redis
    3. MQTT
    4. NATS
    5. RabbitMQ
    6. Kafka
    7. gRPC
    8. Custom transporters
    9. Exception filters
    10. Pipes
    11. Pre-request hooks
    12. Guards
    13. Interceptors
  9. GraphQL
    1. Quick start
    2. Resolvers
    3. Mutations
    4. Subscriptions
    5. Scalars
    6. Directives
    7. Interfaces
    8. Unions and enums
    9. Field middleware
    10. Mapped types
    11. Plugins
    12. Complexity
    13. Extensions
    14. CLI plugin
    15. Generating SDL
    16. Sharing models
    17. Other features
    18. Federation
  10. CLI & triển khai
    1. CLI overview
    2. Workspaces
    3. Libraries
    4. Usage
    5. Scripts
    6. Deployment
    7. Standalone apps
  11. FAQ
    1. Serverless
    2. HTTP adapter
    3. Keep-Alive connections
    4. Global path prefix
    5. Raw body
    6. Hybrid application
    7. HTTPS & multiple servers
    8. Common errors
  12. Phụ lục
    1. Bảng thuật ngữ
Microservices

RabbitMQ

Bản dịch từ docs.nestjs.com/microservices/rabbitmq

RabbitMQ là một message broker mã nguồn mở, nhẹ, hỗ trợ nhiều messaging protocol. Nó có thể được triển khai theo cấu hình distributed và federated để đáp ứng yêu cầu về khả năng mở rộng cao và tính sẵn sàng cao. Ngoài ra, đây là message broker được sử dụng rộng rãi nhất, dùng ở mọi quy mô từ startup nhỏ đến doanh nghiệp lớn trên toàn thế giới.

Installation (cài đặt)

Để bắt đầu xây dựng microservice dựa trên RabbitMQ, trước tiên hãy cài các package cần thiết:

$ npm i --save amqplib amqp-connection-manager

Overview (tổng quan)

Để sử dụng RabbitMQ transporter, truyền object options sau vào method createMicroservice():

const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
  transport: Transport.RMQ,
  options: {
    urls: ['amqp://localhost:5672'],
    queue: 'cats_queue',
    queueOptions: {
      durable: false
    },
  },
});
const app = await NestFactory.createMicroservice(AppModule, {
  transport: Transport.RMQ,
  options: {
    urls: ['amqp://localhost:5672'],
    queue: 'cats_queue',
    queueOptions: {
      durable: false
    },
  },
});

Gợi ý Enum Transport được import từ package @nestjs/microservices.

Options (các option)

Property options phụ thuộc vào transporter được chọn. Transporter RabbitMQ cung cấp các property được mô tả dưới đây.

urls Mảng các connection URL sẽ được thử lần lượt theo thứ tự
queue Tên queue mà server của bạn sẽ lắng nghe
prefetchCount Thiết lập prefetch count cho channel
isGlobalPrefetchCount Bật prefetch theo từng channel
noAck Nếu là false, chế độ acknowledgment thủ công sẽ được bật
consumerTag Tên mà server dùng để phân biệt các message deliveries cho consumer; không được trùng với tag đang dùng trên channel. Thường thì nên bỏ qua option này, khi đó server sẽ tự tạo một tên ngẫu nhiên và trả về trong reply. Consumer Tag Identifier (đọc thêm tại đây)
queueOptions Các option bổ sung cho queue (đọc thêm tại đây)
socketOptions Các option bổ sung cho socket (đọc thêm tại đây)
headers Header sẽ được gửi kèm với mỗi message
replyQueue Reply queue cho producer. Giá trị mặc định là amq.rabbitmq.reply-to
persistent Nếu là truthy, message sẽ tồn tại qua các lần restart của broker, miễn là nó nằm trong một queue cũng tồn tại qua restart
noAssert Khi là false, queue sẽ không được assert trước khi consume
wildcards Chỉ đặt thành true nếu bạn muốn dùng Topic Exchange để định tuyến message tới queue. Bật option này cho phép dùng wildcard (*, #) trong message và event pattern
exchange Tên của exchange. Mặc định là tên queue khi "wildcards" được đặt là true
exchangeType Loại exchange. Mặc định là topic. Các giá trị hợp lệ là direct, fanout, topic, và headers
routingKey Routing key bổ sung cho topic exchange
maxConnectionAttempts Số lần thử kết nối tối đa. Chỉ áp dụng cho cấu hình consumer. -1 nghĩa là vô hạn

Client

Giống như các transporter khác của microservice, bạn có nhiều cách để tạo instance ClientProxy cho RabbitMQ.

Một cách để tạo instance là dùng ClientsModule. Để tạo client instance với ClientsModule, import nó và dùng method register() để truyền một object options với các property giống như đã trình bày ở method createMicroservice() phía trên, cùng với property name dùng làm injection token. Đọc thêm về ClientsModule tại đây.

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'MATH_SERVICE',
        transport: Transport.RMQ,
        options: {
          urls: ['amqp://localhost:5672'],
          queue: 'cats_queue',
          queueOptions: {
            durable: false
          },
        },
      },
    ]),
  ]
  ...
})

Bạn cũng có thể dùng các cách khác để tạo client (ClientProxyFactory hoặc @Client()). Đọc thêm về chúng tại đây.

Context (ngữ cảnh)

Trong các tình huống phức tạp hơn, bạn có thể cần truy cập thêm thông tin về request đến. Khi dùng RabbitMQ transporter, bạn có thể truy cập object RmqContext.

@MessagePattern('notifications')
getNotifications(@Payload() data: number[], @Ctx() context: RmqContext) {
  console.log(`Pattern: ${context.getPattern()}`);
}
@Bind(Payload(), Ctx())
@MessagePattern('notifications')
getNotifications(data, context) {
  console.log(`Pattern: ${context.getPattern()}`);
}

Gợi ý @Payload(), @Ctx() và RmqContext được import từ package @nestjs/microservices.

Để truy cập message gốc của RabbitMQ (gồm properties, fields, và content), dùng method getMessage() của object RmqContext, như sau:

@MessagePattern('notifications')
getNotifications(@Payload() data: number[], @Ctx() context: RmqContext) {
  console.log(context.getMessage());
}
@Bind(Payload(), Ctx())
@MessagePattern('notifications')
getNotifications(data, context) {
  console.log(context.getMessage());
}

Để lấy reference tới channel của RabbitMQ, dùng method getChannelRef của object RmqContext, như sau:

@MessagePattern('notifications')
getNotifications(@Payload() data: number[], @Ctx() context: RmqContext) {
  console.log(context.getChannelRef());
}
@Bind(Payload(), Ctx())
@MessagePattern('notifications')
getNotifications(data, context) {
  console.log(context.getChannelRef());
}

Message acknowledgement (xác nhận message)

Để đảm bảo message không bao giờ bị mất, RabbitMQ hỗ trợ message acknowledgement. Acknowledgement là tín hiệu consumer gửi lại để báo cho RabbitMQ biết một message cụ thể đã được nhận, xử lý xong, và RabbitMQ có thể xóa message đó. Nếu consumer “chết” (channel bị đóng, connection bị đóng, hoặc mất kết nối TCP) mà chưa gửi ack, RabbitMQ sẽ hiểu là message chưa được xử lý xong và sẽ re-queue lại.

Để bật chế độ acknowledgment thủ công, đặt property noAck thành false:

options: {
  urls: ['amqp://localhost:5672'],
  queue: 'cats_queue',
  noAck: false,
  queueOptions: {
    durable: false
  },
},

Khi manual consumer acknowledgement được bật, chúng ta phải tự gửi acknowledgement từ worker để báo là đã xử lý xong một task.

@MessagePattern('notifications')
getNotifications(@Payload() data: number[], @Ctx() context: RmqContext) {
  const channel = context.getChannelRef();
  const originalMsg = context.getMessage();

  channel.ack(originalMsg);
}
@Bind(Payload(), Ctx())
@MessagePattern('notifications')
getNotifications(data, context) {
  const channel = context.getChannelRef();
  const originalMsg = context.getMessage();

  channel.ack(originalMsg);
}

Ghi chú người dịch Cơ chế ack/nack ở đây tương tự khái niệm “commit offset” trong Kafka hoặc visibility timeout trong SQS: nếu bạn không gọi channel.ack(), message vẫn coi như “đang xử lý” và khi consumer chết giữa đường, nó sẽ được đẩy lại vào queue cho consumer khác xử lý. Lỗi hay gặp khi vibe-code: quên gọi ack trong nhánh catch của try/catch, dẫn tới message bị retry vô hạn (hoặc rơi vào dead-letter queue nếu bạn có cấu hình) mà không hiểu vì sao consumer “ăn” CPU liên tục.

Record builders (bộ dựng record)

Để cấu hình các option của message, bạn có thể dùng class RmqRecordBuilder (lưu ý: điều này cũng áp dụng được cho các luồng dựa trên event). Ví dụ, để đặt property headers và priority, dùng method setOptions như sau:

const message = ':cat:';
const record = new RmqRecordBuilder(message)
  .setOptions({
    headers: {
      ['x-version']: '1.0.0',
    },
    priority: 3,
  })
  .build();

this.client.send('replace-emoji', record).subscribe(...);

Gợi ý Class RmqRecordBuilder được export từ package @nestjs/microservices.

Và bạn cũng có thể đọc các giá trị này ở phía server, bằng cách truy cập RmqContext, như sau:

@MessagePattern('replace-emoji')
replaceEmoji(@Payload() data: string, @Ctx() context: RmqContext): string {
  const { properties: { headers } } = context.getMessage();
  return headers['x-version'] === '1.0.0' ? '🐱' : '🐈';
}
@Bind(Payload(), Ctx())
@MessagePattern('replace-emoji')
replaceEmoji(data, context) {
  const { properties: { headers } } = context.getMessage();
  return headers['x-version'] === '1.0.0' ? '🐱' : '🐈';
}

Instance status updates (cập nhật trạng thái instance)

Để nhận cập nhật theo thời gian thực về kết nối và trạng thái của driver instance bên dưới, bạn có thể subscribe vào stream status. Stream này cung cấp các cập nhật trạng thái riêng cho driver đang dùng. Với driver RMQ, stream status phát ra các event connected và disconnected.

this.client.status.subscribe((status: RmqStatus) => {
  console.log(status);
});

Gợi ý Type RmqStatus được import từ package @nestjs/microservices.

Tương tự, bạn có thể subscribe vào stream status của server để nhận thông báo về trạng thái của server.

const server = app.connectMicroservice<MicroserviceOptions>(...);
server.status.subscribe((status: RmqStatus) => {
  console.log(status);
});

Listening to RabbitMQ events (lắng nghe event của RabbitMQ)

Trong một số trường hợp, bạn có thể muốn lắng nghe các event nội bộ được microservice phát ra. Ví dụ, bạn có thể lắng nghe event error để thực hiện thêm các xử lý khác khi có lỗi xảy ra. Để làm điều này, dùng method on(), như sau:

this.client.on('error', (err) => {
  console.error(err);
});

Tương tự, bạn có thể lắng nghe các event nội bộ của server:

server.on<RmqEvents>('error', (err) => {
  console.error(err);
});

Gợi ý Type RmqEvents được import từ package @nestjs/microservices.

Underlying driver access (truy cập driver bên dưới)

Với các use case phức tạp hơn, bạn có thể cần truy cập trực tiếp underlying driver instance. Điều này hữu ích cho các tình huống như tự đóng kết nối bằng tay hoặc dùng các method riêng của driver. Tuy nhiên, hãy nhớ rằng trong hầu hết trường hợp, bạn không cần truy cập trực tiếp driver.

Để làm điều này, bạn dùng method unwrap(), trả về underlying driver instance. Generic type parameter nên chỉ rõ loại driver instance bạn mong muốn.

const managerRef =
  this.client.unwrap<import('amqp-connection-manager').AmqpConnectionManager>();

Tương tự, bạn có thể truy cập underlying driver instance của server:

const managerRef =
  server.unwrap<import('amqp-connection-manager').AmqpConnectionManager>();

Wildcards (ký tự đại diện)

RabbitMQ hỗ trợ dùng wildcard trong routing key để định tuyến message một cách linh hoạt. Wildcard # khớp với không hoặc nhiều từ, còn wildcard * khớp với đúng một từ.

Ví dụ, routing key cats.# khớp với cats, cats.meow, và cats.meow.purr. Routing key cats.* khớp với cats.meow nhưng không khớp với cats.meow.purr.

Để bật hỗ trợ wildcard trong microservice RabbitMQ của bạn, đặt config option wildcards thành true trong object options:

const app = await NestFactory.createMicroservice<MicroserviceOptions>(
  AppModule,
  {
    transport: Transport.RMQ,
    options: {
      urls: ['amqp://localhost:5672'],
      queue: 'cats_queue',
      wildcards: true,
    },
  },
);

Ghi chú người dịch Bật wildcards thực chất chuyển exchange từ dạng “direct” (khớp tên chính xác) sang “topic” — khái niệm này giống với việc dùng route pattern * trong Express hoặc wildcard route trong Laravel, chỉ khác là ở đây RabbitMQ so khớp theo từng đoạn phân tách bởi dấu chấm (.) trong routing key, không phải theo dấu /.

Với cấu hình này, bạn có thể dùng wildcard trong routing key khi subscribe event/message. Ví dụ, để lắng nghe message với routing key cats.#, bạn có thể dùng code sau:

@MessagePattern('cats.#')
getCats(@Payload() data: { message: string }, @Ctx() context: RmqContext) {
  console.log(`Received message with routing key: ${context.getPattern()}`);

  return {
    message: 'Hello from the cats service!',
  }
}

Để gửi message với một routing key cụ thể, bạn có thể dùng method send() của instance ClientProxy:

this.client.send('cats.meow', { message: 'Meow!' }).subscribe((response) => {
  console.log(response);
});

Nội dung gốc: NestJS Documentation © Kamil Myśliwiec, giấy phép MIT. Bản dịch và ghi chú: Lê Văn Dũng.