DEV Community

Cover image for Webhook vs Polling: Phương Thức Tích Hợp API Nào Tốt Hơn?
Sebastian Petrus
Sebastian Petrus

Posted on • Originally published at apidog.com

Webhook vs Polling: Phương Thức Tích Hợp API Nào Tốt Hơn?

Tóm tắt: Polling kiểm tra cập nhật định kỳ (đơn giản nhưng không hiệu quả). Webhooks đẩy cập nhật theo thời gian thực (hiệu quả nhưng phức tạp). Sử dụng polling cho các lần kiểm tra không thường xuyên, webhooks cho cập nhật thời gian thực. Modern PetstoreAPI hỗ trợ cả hai mô hình với việc gửi webhook đáng tin cậy.

Dùng thử Apidog ngay hôm nay

Hiểu về sự khác biệt

Polling: Client hỏi “Có cập nhật nào không?” lặp đi lặp lại. Webhooks: Server nói “Đây là một bản cập nhật!” khi có điều gì đó xảy ra.

  • Polling = Kiểm tra hộp thư của bạn mỗi giờ
  • Webhooks = Người đưa thư rung chuông khi có thư đến

Polling: Cách hoạt động

Client thực hiện các yêu cầu định kỳ để kiểm tra các thay đổi.

// Poll mỗi 30 giây
setInterval(async () => {
  const response = await fetch('https://petstoreapi.com/api/v1/orders/123');
  const order = await response.json();

  if (order.status === 'completed') {
    console.log('Order completed!', order);
    clearInterval(pollInterval);
  }
}, 30000);
Enter fullscreen mode Exit fullscreen mode

Các kiểu polling:

Polling đơn giản:

GET /api/v1/orders/123
# Returns current order state
Enter fullscreen mode Exit fullscreen mode

Polling có điều kiện (ETag):

GET /api/v1/orders/123
If-None-Match: "abc123"

# Returns 304 Not Modified if unchanged
# Returns 200 with new data if changed
Enter fullscreen mode Exit fullscreen mode

Polling dựa trên 'since':

GET /api/v1/orders/123/events?since=1710331200
# Returns events since timestamp
Enter fullscreen mode Exit fullscreen mode

Webhooks: Cách hoạt động

Server gửi HTTP POST đến endpoint của bạn khi sự kiện xảy ra.

Quy trình thiết lập:

// 1. Đăng ký webhook endpoint
POST /api/v1/webhooks
{
  "url": "https://myapp.com/webhooks/petstore",
  "events": ["order.created", "order.completed"],
  "secret": "whsec_abc123"
}

// 2. Server gửi webhook khi sự kiện xảy ra
POST https://myapp.com/webhooks/petstore
{
  "id": "evt_123",
  "type": "order.completed",
  "created": 1710331200,
  "data": {
    "orderId": "123",
    "status": "completed",
    "completedAt": "2024-01-01T12:00:00Z"
  }
}

// 3. Xác minh và xử lý webhook
// Respond with 200 OK
Enter fullscreen mode Exit fullscreen mode

Khi nào nên sử dụng Polling

Thích hợp cho:

  • Kiểm tra không thường xuyên (một lần mỗi giờ)
  • Số lượng tài nguyên nhỏ
  • Các triển khai đơn giản
  • Khi bạn kiểm soát client
  • Kiểm thử và gỡ lỗi

Ví dụ:

  • Kiểm tra trạng thái báo cáo hàng ngày
  • Đồng bộ hóa danh bạ vài phút một lần
  • Giám sát tình trạng máy chủ
  • Kiểm tra trạng thái thanh toán (không thường xuyên)

Polling tốt khi:

  • Cập nhật hiếm khi xảy ra
  • Chấp nhận được sự chậm trễ nhỏ
  • Bạn muốn triển khai đơn giản
  • Tài nguyên nhỏ

Khi nào nên sử dụng Webhooks

Thích hợp cho:

  • Cập nhật thời gian thực
  • Nhiều tài nguyên cần giám sát
  • Các sự kiện nhạy cảm về thời gian
  • Tích hợp bên thứ ba
  • Cập nhật thường xuyên

Ví dụ:

  • Xác nhận thanh toán
  • Tin nhắn trò chuyện
  • Cảnh báo giá cổ phiếu
  • Thay đổi trạng thái đơn hàng
  • Thông báo CI/CD

Webhooks tốt hơn khi:

  • Cập nhật cần phải tức thì
  • Polling sẽ không hiệu quả
  • Nhiều client giám sát cùng một tài nguyên
  • Bạn muốn giảm tải máy chủ

Bảng so sánh

<!--kg-card-begin: html-->



















































Yếu tố Polling Webhooks
Độ trễ Lên đến khoảng thời gian poll Thời gian thực
Tải máy chủ Cao (nhiều yêu cầu rỗng) Thấp (chỉ các sự kiện thực)
Độ phức tạp Đơn giản Phức tạp
Độ tin cậy Cao (client kiểm soát việc thử lại) Trung bình (cần logic thử lại)
Thiết lập Không cần Đăng ký endpoint
Vấn đề tường lửa Không (chỉ đi ra) Có thể cần đưa vào danh sách trắng
Chi phí Cao hơn (nhiều yêu cầu hơn) Thấp hơn (ít yêu cầu hơn)
Tốt nhất cho Kiểm tra không thường xuyên Cập nhật thời gian thực
<!--kg-card-end: html-->

Triển khai Polling

Polling cơ bản

async function pollOrderStatus(orderId, callback) {
  let lastStatus = null;

  const poll = async () => {
    try {
      const response = await fetch(`https://petstoreapi.com/api/v1/orders/${orderId}`);
      const order = await response.json();

      // Chỉ gọi callback nếu trạng thái đã thay đổi
      if (order.status !== lastStatus) {
        lastStatus = order.status;
        callback(order);
      }

      // Dừng polling nếu đạt trạng thái cuối
      if (['completed', 'cancelled'].includes(order.status)) {
        return;
      }

      // Tiếp tục polling
      setTimeout(poll, 5000);
    } catch (error) {
      console.error('Lỗi polling:', error);
      setTimeout(poll, 30000); // Tạm dừng khi có lỗi
    }
  };

  poll();
}

// Cách sử dụng
pollOrderStatus('order-123', (order) => {
  console.log(`Trạng thái đơn hàng: ${order.status}`);
});
Enter fullscreen mode Exit fullscreen mode

Smart Polling (Tạm dừng lũy thừa)

async function smartPoll(url, callback, options = {}) {
  const {
    maxRetries = 10,
    initialInterval = 1000,
    maxInterval = 60000,
    stopCondition = () => false
  } = options;

  let retries = 0;
  let interval = initialInterval;
  let lastData = null;

  const poll = async () => {
    try {
      const response = await fetch(url);
      const data = await response.json();

      // Gọi callback nếu dữ liệu thay đổi
      if (JSON.stringify(data) !== JSON.stringify(lastData)) {
        lastData = data;
        callback(data);
      }

      // Dừng nếu điều kiện được đáp ứng
      if (stopCondition(data)) {
        return;
      }

      // Đặt lại khoảng thời gian nếu yêu cầu thành công
      interval = initialInterval;

    } catch (error) {
      retries++;
      if (retries >= maxRetries) {
        throw new Error('Đã vượt quá số lần thử lại tối đa');
      }
    }

    // Lên lịch poll tiếp theo với tạm dừng lũy thừa
    setTimeout(poll, interval);
    interval = Math.min(interval * 2, maxInterval);
  };

  poll();
}

// Cách sử dụng: Poll đơn hàng cho đến khi hoàn thành
smartPoll('https://petstoreapi.com/api/v1/orders/123',
  (order) => console.log('Đơn hàng:', order),
  {
    stopCondition: (order) => ['completed', 'cancelled'].includes(order.status),
    initialInterval: 2000,
    maxInterval: 30000
  }
);
Enter fullscreen mode Exit fullscreen mode

Polling với ETag

async function pollWithEtag(url, callback) {
  let etag = null;

  const poll = async () => {
    const headers = {};
    if (etag) {
      headers['If-None-Match'] = etag;
    }

    const response = await fetch(url, { headers });

    if (response.status === 304) {
      // Chưa được sửa đổi, tiếp tục polling
      setTimeout(poll, 30000);
      return;
    }

    const data = await response.json();
    etag = response.headers.get('etag');

    callback(data);
    setTimeout(poll, 30000);
  };

  poll();
}
Enter fullscreen mode Exit fullscreen mode

Triển khai Webhooks

Đăng ký Webhooks

// Đăng ký webhook endpoint
async function registerWebhook(url, events) {
  const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`
    },
    body: JSON.stringify({
      url,
      events,
      secret: generateSecret()
    })
  });

  return response.json();
}

function generateSecret() {
  return 'whsec_' + crypto.randomBytes(32).toString('hex');
}
Enter fullscreen mode Exit fullscreen mode

Nhận Webhooks

const express = require('express');
const crypto = require('crypto');
const app = express();

// Bộ phân tích cú pháp thân thô để xác minh chữ ký
app.use('/webhooks', express.raw({ type: 'application/json' }));

app.post('/webhooks/petstore', async (req, res) => {
  const signature = req.headers['x-petstore-signature'];
  const body = req.body;

  // Xác minh chữ ký
  const isValid = verifySignature(body, signature, process.env.WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).json({ error: 'Chữ ký không hợp lệ' });
  }

  const event = JSON.parse(body.toString());

  // Xử lý sự kiện
  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    case 'order.completed':
      await handleOrderCompleted(event.data);
      break;
    case 'order.cancelled':
      await handleOrderCancelled(event.data);
      break;
  }

  // Xác nhận đã nhận
  res.status(200).json({ received: true });
});

function verifySignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}
Enter fullscreen mode Exit fullscreen mode

Kiểm thử Webhooks tại cục bộ

# Sử dụng ngrok để công khai endpoint cục bộ
ngrok http 3000

# Đăng ký URL ngrok làm webhook endpoint
curl -X POST https://petstoreapi.com/api/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/petstore",
    "events": ["order.created", "order.completed"]
  }'
Enter fullscreen mode Exit fullscreen mode

Phân phối Webhook đáng tin cậy

Webhooks có thể thất bại. Triển khai logic thử lại.

Bên gửi (Máy chủ)

// Hàng đợi webhooks để phân phối
const webhookQueue = [];

async function sendWebhook(event) {
  const webhooks = await db.webhooks.findMany({
    where: { events: { contains: event.type } }
  });

  for (const webhook of webhooks) {
    webhookQueue.push({
      webhook,
      event,
      attempts: 0,
      nextAttempt: Date.now()
    });
  }

  processQueue();
}

async function processQueue() {
  const now = Date.now();

  for (const item of webhookQueue) {
    if (item.nextAttempt > now) continue;

    try {
      await deliverWebhook(item);
      // Xóa khỏi hàng đợi khi thành công
      webhookQueue.splice(webhookQueue.indexOf(item), 1);
    } catch (error) {
      // Lên lịch thử lại với tạm dừng lũy thừa
      item.attempts++;
      item.nextAttempt = now + getBackoff(item.attempts);

      if (item.attempts >= 5) {
        // Đánh dấu là thất bại sau 5 lần thử
        await markWebhookFailed(item);
        webhookQueue.splice(webhookQueue.indexOf(item), 1);
      }
    }
  }

  setTimeout(processQueue, 5000);
}

function getBackoff(attempt) {
  // 1phút, 5phút, 15phút, 1giờ, 4giờ
  const delays = [60000, 300000, 900000, 3600000, 14400000];
  return delays[attempt - 1] || delays[delays.length - 1];
}

async function deliverWebhook({ webhook, event }) {
  const signature = generateSignature(event, webhook.secret);

  const response = await fetch(webhook.url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Petstore-Signature': signature,
      'X-Petstore-Event': event.type
    },
    body: JSON.stringify(event),
    timeout: 10000
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Bên nhận (Client)

// Xử lý webhook bất biến
const processedEvents = new Set();

app.post('/webhooks/petstore', async (req, res) => {
  const event = JSON.parse(req.body.toString());

  // Bỏ qua nếu đã được xử lý (tính bất biến)
  if (processedEvents.has(event.id)) {
    return res.status(200).json({ received: true });
  }

  try {
    await processEvent(event);
    processedEvents.add(event.id);

    // Xóa các ID sự kiện cũ (giữ lại 1000 ID cuối)
    if (processedEvents.size > 1000) {
      const arr = Array.from(processedEvents);
      arr.slice(0, arr.length - 1000).forEach(id => processedEvents.delete(id));
    }

    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Lỗi xử lý webhook:', error);
    // Trả về 5xx để kích hoạt thử lại
    res.status(500).json({ error: 'Xử lý thất bại' });
  }
});

async function processEvent(event) {
  // Xử lý sự kiện
  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    // ... xử lý các sự kiện khác
  }
}
Enter fullscreen mode Exit fullscreen mode

Phương pháp tiếp cận lai

Sử dụng cả polling và webhooks cho các cập nhật quan trọng.

class OrderMonitor {
  constructor(orderId, callback) {
    this.orderId = orderId;
    this.callback = callback;
    this.pollInterval = null;
  }

  async start() {
    // Bắt đầu với polling để có phản hồi tức thì
    this.startPolling();

    // Đăng ký webhook để cập nhật thời gian thực
    await this.registerWebhook();
  }

  startPolling() {
    this.pollInterval = setInterval(async () => {
      const order = await this.fetchOrder();
      this.callback(order);

      if (['completed', 'cancelled'].includes(order.status)) {
        this.stop();
      }
    }, 10000);
  }

  async registerWebhook() {
    const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${TOKEN}` },
      body: JSON.stringify({
        url: 'https://myapp.com/webhooks/petstore',
        events: [`order.${this.orderId}`],
        oneTime: true // Tự động xóa sau lần gửi đầu tiên
      })
    });

    this.webhookId = (await response.json()).id;
  }

  stop() {
    if (this.pollInterval) {
      clearInterval(this.pollInterval);
    }
    if (this.webhookId) {
      fetch(`https://petstoreapi.com/api/v1/webhooks/${this.webhookId}`, {
        method: 'DELETE'
      });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Câu hỏi thường gặp

Hỏi: Tôi nên poll thường xuyên đến mức nào? Tùy thuộc vào mức độ khẩn cấp. 30 giây cho gần thời gian thực. 5 phút cho không khẩn cấp. Cân bằng độ mới với tải máy chủ.

Hỏi: Điều gì xảy ra nếu endpoint webhook của tôi bị sập? Các nhà cung cấp webhook tốt sẽ thử lại với tạm dừng lũy thừa. Triển khai tính bất biến để xử lý các lần gửi trùng lặp.

Hỏi: Làm cách nào để bảo mật webhooks? Xác minh chữ ký bằng cách sử dụng các khóa bí mật được chia sẻ. Chỉ sử dụng HTTPS. Xác thực dữ liệu sự kiện.

Hỏi: Tôi có thể sử dụng webhooks cho dữ liệu lịch sử không? Không. Webhooks chỉ dành cho các sự kiện mới. Sử dụng polling hoặc API theo lô cho dữ liệu lịch sử.

Hỏi: Tôi nên sử dụng polling hay webhooks cho ứng dụng di động? Polling đơn giản hơn cho di động. Webhooks yêu cầu thông báo đẩy làm trung gian.

Hỏi: Làm cách nào để gỡ lỗi các vấn đề về webhook? Sử dụng các công cụ như webhook.site để kiểm thử. Ghi nhật ký tất cả các lần gửi webhook. Cung cấp lịch sử sự kiện webhook trong API của bạn.

Modern PetstoreAPI hỗ trợ cả polling và webhooks. Xem hướng dẫn về webhooks để biết chi tiết triển khai. Kiểm thử tích hợp webhook với Apidog.

Top comments (0)