Facebook PixelAsynchronous Request-Reply Pattern: Giải pháp cho những tác vụ nặng | 200Lab Blog

Asynchronous Request-Reply Pattern: Giải pháp cho những tác vụ nặng

07 Jul, 2025

Pattern giúp tách rời phần xử lý backend, diễn ra bất đồng bộ khỏi frontend nhưng vẫn phải đảm bảo frontend nhận được thông báo kết quả cuối cùng

Asynchronous Request-Reply Pattern: Giải pháp cho những tác vụ nặng

Mục Lục

Khi ứng dụng phải xử lý các tác vụ nặng như xuất báo cáo, tổng hợp dữ liệu lớn hoặc thao tác kéo dài, việc giữ kết nối chờ phản hồi trực tiếp từ server thường dẫn đến timeout hoặc trải nghiệm người dùng không tốt. Đây là một trong những điểm nghẽn phổ biến khi xây dựng API cho các tính năng đòi hỏi thời gian dài để hoàn thành.

Asynchronous Request-Reply Pattern là giải pháp giúp bạn tách rời quá trình xử lý với quá trình phản hồi, đảm bảo hệ thống không bị “nghẽn cổ chai” và vẫn cung cấp thông báo kịp thời cho người dùng. Trong bài viết này, chúng ta sẽ phân tích pattern này và cách áp dụng thực tế với TypeScript và Node.js, giúp bạn xử lý các yêu cầu lâu một cách hiệu quả và đảm bảo khả năng mở rộng của ứng dụng.

1. Vấn đề của xử lý đồng bộ

Giả sử bạn có một API đơn giản: POST /api/generate-report. Khi nhận request, server sẽ phải thực hiện những tác vụ nặng như truy vấn hàng triệu bản ghi trong database, tổng hợp dữ liệu, rồi xuất ra file PDF hoặc Excel. Những công việc này có thể kéo dài từ 30 giây đến vài phút, thậm chí lâu hơn.

Nếu xử lý theo kiểu đồng bộ (synchronous), mô hình này sẽ gặp nhiều vấn đề:

  • HTTP Timeout: Đa số web server, proxy hoặc load balancer đều giới hạn thời gian chờ request (thường chỉ 30–60 giây).
  • Trải nghiệm người dùng kém: Quá trình tải báo cáo kéo dài sẽ khiến người dùng mất kiên nhẫn.
  • Lãng phí tài nguyên: Duy trì một kết nối HTTP mở sẽ chiếm dụng thread hoặc process của server, làm giảm hiệu suất phục vụ các request khác.

2. Asynchronous Request-Reply Pattern là gì?

Để giải quyết vấn đề này, cần tách riêng phần nhận request khỏi phần xử lý request.

Theo tài liệu Cloud Design Patterns của Microsoft, Asynchronous Request-Reply Pattern được định nghĩa là:
“Decouple backend processing from a frontend host, where backend processing needs to be asynchronous, but the frontend still needs a clear notification of the outcome.”

Bạn có thể hiểu nôm na nó là: Tách rời phần xử lý backend – diễn ra bất đồng bộ – khỏi frontend, nhưng vẫn phải đảm bảo frontend nhận được thông báo kết quả cuối cùng.

Cách hoạt động của pattern này rất đơn giản:

  1. Backend nhận yêu cầu và trả lời ngay với thông báo đã nhận request (thường kèm một job ID hoặc token).
  2. Công việc được xử lý ở background: Hệ thống thực hiện các tác vụ nặng mà không giữ kết nối chờ client.
  3. Client có thể kiểm tra trạng thái hoặc nhận kết quả dựa trên job ID đã được trả về.

Một ví dụ quen thuộc là quy trình gọi món ở nhà hàng: Bạn đặt món và nhận một thiết bị báo rung, đến khi món sẵn sàng, thiết bị sẽ rung để bạn biết quay lại lấy – bạn không cần đứng chờ tại quầy.

3. Luồng Hoạt động Chi tiết

Kiến trúc này thường bao gồm các thành phần sau: ClientAPI ServerMessage Queue, và Worker.

Bash
+--------+         +-------------+         +----------------+         +---------+
| Client |         | API Server  |         | Message Queue  |         | Worker  |
+--------+         +-------------+         +----------------+         +---------+
    |                     |                         |                      |
    | 1. POST /export     |                         |                      |
    |-------------------->|                         |                      |
    |                     | 2. Create Job           |                      |
    |                     | & Push to Queue         |                      |
    |                     |------------------------>|                      |
    |                     |                         | 3. Worker polls      |
    |                     |                         |    for new jobs      |
    |                     |                         |<---------------------|
    | 4. 202 Accepted     |                         |                      |
    |    { statusUrl }    |                         |                      |
    |<--------------------|                         |                      |
    |                     |                         | 5. Process Job       |
    |                     |                         |    (long task)       |
    |                     |                         |   Update DB status   |
    |                     |                         |--------------------->|
    | 5. GET /status/{id} |                         |                      |
    |-------------------->|                         |                      |
    | 6. "PENDING"        |                         |                      |
    |<--------------------|                         |                      |
    |       ...           |                         |                      |
    | (poll again later)  |                         |                      |
    |       ...           |                         |                      |
    | 7. GET /status/{id} |                         |                      |
    |-------------------->|                         |                      |
    | 8. "COMPLETED"      |                         |                      |
    |    { resultUrl }    |                         |                      |
    |<--------------------|                         |                      |
    |                     |                         |                      |
  1. Client gửi Request: Client gửi một POST request đến endpoint /export.
  2. API Server phản hồi ngay lập tức:
  • Server không xử lý tác vụ ngay, nó tạo mộtjobId duy nhất.
  • Đưa jobId và các tham số cần thiết vào một Message Queue (như RabbitMQ, AWS SQS, Google Pub/Sub).
  • Ngay lập tức trả về một response 202 Accepted cho client, kèm theo một URL để kiểm tra trạng thái (ví dụ: {"statusUrl": "/export/status/abc-123"}).
  1. Worker xử lý trong background:
  • Một hoặc nhiều Worker (các tiến trình riêng biệt) lắng nghe các job mới từ Message Queue.
  • Khi có job mới, một Worker sẽ nhận và bắt đầu thực hiện tác vụ nặng.
  • Trong quá trình xử lý, Worker có thể cập nhật trạng thái của job (PROCESSINGFAILEDCOMPLETED) vào một database (như Redis hoặc PostgreSQL).
  • Khi hoàn thành, Worker lưu file kết quả vào một nơi lưu trữ (như Amazon S3, Google Cloud Storage) và cập nhật trạng thái COMPLETED cùng với link download.
  1. Client kiểm tra trạng thái (Polling):
  • Client sử dụng statusUrl nhận được ở bước 2 để định kỳ gọi đến API Server hỏi về trạng thái của job.
  • Server sẽ đọc trạng thái từ database và trả về (PENDINGPROCESSINGCOMPLETEDFAILED).
  • Khi trạng thái là COMPLETED, response sẽ chứa URL để tải file kết quả.

Ngoài Polling được đề cập ở bước 4 bạn có thể sử dụng thêm các giải pháp khác như: WebSockets / Server-Sent Events (SSE), Webhooks (Server-to-Server), Email Notification

4.  Hướng dẫn Triển khai với TypeScript, Node.js & Express

Chúng ta sẽ cùng bắt tay vào xây dựng một ví dụ thực tế. Để đơn giản hóa, chúng ta sẽ sử dụng các biến tạm trong chương trình (như mảng hoặc object) để giả lập Message Queue và Database, giúp bạn dễ dàng tập trung vào logic chính của Asynchronous Request-Reply Pattern.

  • Cài đặt:
Bash
npm init -y
npm install express typescript ts-node @types/express @types/node uuid @types/uuid
npx tsc --init
  • Cấu trúc file:
Bash
/src
  ├── server.ts
  ├── types.ts
  • src/types.ts: Định nghĩa các kiểu dữ liệu.
Javascript
export type JobStatus = 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';

export interface Job {
  id: string;
  status: JobStatus;
  createdAt: Date;
  updatedAt: Date;
  resultUrl?: string;
  error?: string;
}
  • src/server.ts: Code logic chính của API.
Javascript
import express, { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { Job, JobStatus } from './types';

const app = express();
const PORT = 3000;

// --- Mô phỏng Database và Message Queue ---
// Trong thực tế, bạn sẽ dùng Redis/PostgreSQL cho jobStore
// và RabbitMQ/SQS cho messageQueue.
const jobStore = new Map<string, Job>();

// Hàm mô phỏng worker xử lý job trong background
const processExportJob = async (jobId: string) => {
  console.log(`[Worker] Starting job: ${jobId}`);
  
  // 1. Cập nhật trạng thái thành PROCESSING
  const initialJob = jobStore.get(jobId);
  if (!initialJob) return;

  const processingJob: Job = { ...initialJob, status: 'PROCESSING', updatedAt: new Date() };
  jobStore.set(jobId, processingJob);

  // 2. Mô phỏng tác vụ nặng mất 15 giây
  await new Promise(resolve => setTimeout(resolve, 15000));

  // 3. Hoàn thành và cập nhật kết quả
  // Trong thực tế, đây sẽ là link từ S3 hoặc GCS
  const resultUrl = `https://storage.example.com/exports/${jobId}.zip`;
  const completedJob: Job = {
    ...jobStore.get(jobId)!,
    status: 'COMPLETED',
    updatedAt: new Date(),
    resultUrl: resultUrl,
  };
  jobStore.set(jobId, completedJob);
  console.log(`[Worker] Finished job: ${jobId}`);
};

// --- API Endpoints ---

// POST /export -> Bắt đầu một job mới
app.post('/export', (req: Request, res: Response) => {
  const jobId = uuidv4();
  const newJob: Job = {
    id: jobId,
    status: 'PENDING',
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  // Lưu job vào store (tương đương DB)
  jobStore.set(jobId, newJob);

  // Gửi job vào queue (ở đây ta gọi thẳng worker để mô phỏng)
  processExportJob(jobId);

  // Trả về 202 Accepted ngay lập tức
  res.status(202).json({
    message: 'Export request accepted. Check the status URL for progress.',
    jobId: jobId,
    statusUrl: `http://localhost:${PORT}/export/status/${jobId}`
  });
});

// GET /export/status/:jobId -> Kiểm tra trạng thái của job
app.get('/export/status/:jobId', (req: Request, res: Response) => {
  const { jobId } = req.params;
  const job = jobStore.get(jobId);

  if (!job) {
    return res.status(404).json({ error: 'Job not found' });
  }

  // Nếu job đã xong, trả về 200 OK với kết quả
  if (job.status === 'COMPLETED') {
    return res.status(200).json(job);
  }
  
  // Nếu job thất bại, trả về 200 OK với lỗi
  if (job.status === 'FAILED') {
    return res.status(200).json(job);
  }

  // Nếu job đang chạy, trả về 200 OK với trạng thái hiện tại
  // Client sẽ dựa vào đây để quyết định có poll tiếp hay không
  return res.status(200).json({
      status: job.status,
      message: 'Job is still in progress. Please check back later.'
  });
});

app.listen(PORT, () => {
  console.log(`Server is running at http://localhost:${PORT}`);
});

5. Chạy và kiểm thử

  • Chạy server: npx ts-node src/server.ts
  • Mở một terminal khác và dùng curl để tương tác
Bash
curl -X POST http://localhost:3000/export -v

Bạn sẽ nhận lại ngay lập tức một response 202 Accepted với jobId và statusUrl.

JSON
{
  "message": "Export request accepted. Check the status URL for progress.",
  "jobId": "...",
  "statusUrl": "http://localhost:3000/export/status/..."
}
  • Kiểm tra trạng thái sau vài giây
Bash
curl http://localhost:3000/export/status/{your_job_id}

6. Kết luận

Asynchronous Request-Reply Pattern không chỉ là một kỹ thuật xử lý backend, mà còn là một cách tiếp cận hiện đại trong thiết kế hệ thống. Việc tách riêng quá trình nhận và xử lý request giúp ứng dụng của bạn trở nên dễ mở rộng và cải thiện đáng kể trải nghiệm người dùng.

Bài viết liên quan

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab