0
0
Lập trình
Thaycacac
Thaycacac thaycacac

⚡ Khám Phá Sâu: Cách `Promise.all` Hoạt Động Với API & DB Trong Node.js

Đăng vào 8 tháng trước

• 6 phút đọc

Khám Phá Sâu: Cách Promise.all Hoạt Động Với API & DB Trong Node.js

Mục Lục

1. So Sánh JavaScript và Node.js

JavaScript (V8)

  • Single-threaded: JavaScript chạy trong một luồng duy nhất.
  • Thực thi mã từ trên xuống dưới: Mọi đoạn mã sẽ được thực thi tuần tự.
  • Sử dụng hàng đợi microtask: Để giải quyết các promises.

Node.js Runtime

  • Xây dựng trên nền V8 + libuv: Kết hợp hai thành phần này để hoạt động hiệu quả.
  • libuv: Cung cấp vòng lặp sự kiện và pool luồng.
  • Tích hợp với stack mạng của hệ điều hành: Để thực hiện I/O không đồng bộ.

👉 Node.js đạt được đồng thời không phải bằng cách thêm luồng cho JavaScript, mà bằng cách phân công công việc cho hệ điều hành, các engine cơ sở dữ liệu và các worker libuv.


2. Cách Hoạt Động của Promise.all

javascript Copy
const p = Promise.all([p1, p2, p3]);
  • Tạo một promise tổng hợp p.
  • Đăng ký lắng nghe từng promise đầu vào (p1, p2, p3).
  • Theo dõi kết quả và số lượng pending.
  • Nếu tất cả được giải quyết → p sẽ giải quyết với một mảng kết quả.
  • Nếu bất kỳ promise nào từ chối → p sẽ từ chối ngay lập tức.
  • Việc giải quyết diễn ra trong hàng đợi microtask, trước các timer và I/O.

3. Gọi API với Promise.all

javascript Copy
const fetch = require("node-fetch");

async function run() {
  const [r1, r2, r3] = await Promise.all([
    fetch("https://httpbin.org/delay/1"),
    fetch("https://httpbin.org/delay/1"),
    fetch("https://httpbin.org/delay/1")
  ]);
  console.log(await Promise.all([r1.json(), r2.json(), r3.json()]));
}
run();

Diễn biến bên trong:

  1. fetch tạo ra các socket TCP.
  2. Hệ điều hành xử lý DNS, bắt tay TCP, TLS.
  3. libuv đăng ký các socket để kiểm tra trạng thái sẵn sàng.
  4. Khi dữ liệu đến → libuv đẩy các callback I/O.
  5. Các promises được giải quyết qua microtasks.

✅ Tính đồng thời đến từ stack mạng của hệ điều hành.


4. Truy Vấn Cơ Sở Dữ Liệu với Promise.all

javascript Copy
const { Pool } = require("pg");
const pool = new Pool();

async function run() {
  const [users, orders] = await Promise.all([
    pool.query("SELECT * FROM users LIMIT 5"),
    pool.query("SELECT * FROM orders LIMIT 5")
  ]);
  console.log(users.rows, orders.rows);
}
run();

Diễn biến bên trong:

  1. Các truy vấn được ghi vào các socket DB.
  2. PostgreSQL thực hiện chúng song song trong các tiến trình worker của DB.
  3. Kết quả được truyền trở lại.
  4. libuv thông báo cho vòng lặp sự kiện.
  5. Các promises được giải quyết, được tổng hợp bởi Promise.all.

✅ Tính đồng thời ở đây được cung cấp bởi engine cơ sở dữ liệu.


5. Vòng Lặp Sự Kiện & Microtasks

Các giai đoạn của vòng lặp sự kiện Node.js:

  1. TimersetTimeout, setInterval.
  2. Pending callbacks.
  3. Idle/prepare.
  4. Poll → sự kiện I/O mới (ví dụ: socket sẵn sàng).
  5. ChecksetImmediate.
  6. Close callbacks.

🔑 Giữa mỗi giai đoạn → hàng đợi microtask được chạy trước. Do đó, việc giải quyết promise (Promise.all) xảy ra trước timer hoặc callback I/O.


6. libuv: Mạng So Với Pool Luồng

Mạng I/O (fetch, socket DB)

  • Sử dụng các cơ chế của hệ điều hành như epoll/kqueue/IOCP.
  • Không có luồng nào được tham gia → rất dễ mở rộng.

Pool Luồng

  • Được sử dụng khi hệ điều hành không cung cấp API không đồng bộ.
  • Ví dụ: fs.readFile, crypto.pbkdf2.
  • Kích thước mặc định = 4 (UV_THREADPOOL_SIZE).

👉 Các cuộc gọi API và socket DB hiếm khi sử dụng luồng - chúng phụ thuộc vào hệ điều hành và các máy chủ bên ngoài để đạt được tính đồng thời.


7. Thử Nghiệm: Truy Vấn Tuần Tự So Với Song Song

🔹 Truy Vấn DB Tuần Tự

javascript Copy
const { Pool } = require("pg");
const pool = new Pool();

async function sequential() {
  console.time("sequential");
  await pool.query("SELECT pg_sleep(1)");
  await pool.query("SELECT pg_sleep(1)");
  await pool.query("SELECT pg_sleep(1)");
  await pool.query("SELECT pg_sleep(1)");
  await pool.query("SELECT pg_sleep(1)");
  console.timeEnd("sequential");
}
sequential().then(() => pool.end());
  • Mỗi truy vấn tạm dừng 1 giây.
  • Chạy tuần tự → ~5 giây tổng cộng.

🔹 Truy Vấn DB Song Song

javascript Copy
const { Pool } = require("pg");
const pool = new Pool();

async function parallel() {
  console.time("parallel");
  await Promise.all([
    pool.query("SELECT pg_sleep(1)"),
    pool.query("SELECT pg_sleep(1)"),
    pool.query("SELECT pg_sleep(1)"),
    pool.query("SELECT pg_sleep(1)"),
    pool.query("SELECT pg_sleep(1)")
  ]);
  console.timeEnd("parallel");
}
parallel().then(() => pool.end());
  • Tất cả các truy vấn được thực hiện cùng lúc.
  • Mỗi truy vấn tạm dừng 1 giây → ~1 giây tổng cộng.

🔹 Kết Quả So Sánh

Chiến Lược Truy Vấn Thời Gian
Tuần Tự 5 × 1s ~5s
Song Song 5 × 1s ~1s

🚀 Promise.all mang lại tốc độ gấp 5 lần bằng cách chồng chéo công việc I/O.


8. Cạm Bẫy Thực Tiễn

⚠️ Giới Hạn Tốc Độ API

  • Quá nhiều yêu cầu đồng thời → HTTP 429 hoặc bị hạn chế. ✅ Sử dụng kiểm soát độ đồng thời (p-limit, Bottleneck).

⚠️ Cạn Kiệt Pool DB

javascript Copy
await Promise.all(bigArray.map(() => pool.query("...")));
  • Vượt quá kết nối pool → truy vấn bị xếp hàng → độ trễ tăng vọt. ✅ Điều chỉnh kích thước pool + chạy truy vấn theo .

⚠️ Xử Lý Lỗi

  • Promise.all sẽ thất bại ngay lập tức khi gặp từ chối đầu tiên. ✅ Sử dụng Promise.allSettled để tăng cường khả năng chịu lỗi.

⚠️ Sử Dụng Bộ Nhớ

  • Promise.all lớn → lưu trữ tất cả kết quả trung gian trong bộ nhớ. ✅ Đối với khối lượng công việc lớn → stream kết quả thay vì gộp tất cả lại.

9. Các Giải Pháp Thay Thế Promise.all

  • Promise.allSettled → nhận tất cả kết quả, ngay cả khi một số bị lỗi.
  • Promise.any → giải quyết khi có thành công đầu tiên (hữu ích cho các dịch vụ dư thừa).
  • Promise.race → giải quyết/từ chối khi có kết quả đầu tiên (tuyệt vời cho timeout).

🔑 Kết Luận Cuối

  • Promise.all điều phối các tác vụ không đồng bộ, nó không làm cho JavaScript chạy song song.
  • Tính đồng thời API đến từ mạng của hệ điều hành.
  • Tính đồng thời cơ sở dữ liệu đến từ các engine cơ sở dữ liệu.
  • Vòng lặp sự kiện Node.js + libuv kết nối tất cả lại với nhau.
  • Sử dụng cẩn thận: giới hạn pool, giới hạn tốc độ API, sử dụng bộ nhớ là rất quan trọng.
  • Các thử nghiệm cho thấy lợi ích về độ trễ thực tế, nhưng cũng cho thấy rủi ro về khả năng mở rộng.

💡 Ý Nghĩa Kết Thúc

Promise.all không phải là phép thuật — nó là sự điều phối.
Tính “song song” bạn thấy đến từ kernel hệ điều hành, libuv và các hệ thống bên ngoài.

Công việc của một kỹ sư không chỉ là biết nó “chạy đồng thời”, mà còn phải hiểu mức độ đồng thời mà hệ thống có thể xử lý trước khi gặp sự cố. Đó là điều phân biệt giữa các demo đơn giản và các hệ thống sản xuất chất lượng cao.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào