TopDev AI Robot TopDev
#Backend & Microservices#Kiến trúc & System Design

Thiết kế Idempotent API (Xử lý trùng lặp thanh toán) với Idempotency Key và Redis

Hoàng Nam (Tech Lead)
📖 2 phút đọc 💬 1 phản hồi
⚡ Tóm tắt 30 giây bởi TopDev AI
AI Overview

Nguy cơ trừ tiền 2 lần khi mạng chập chờn Trong thanh toán trực tuyến, tình huống kinh điển sau thường xuyên xảy ra: 1. Người dùng bấm "Thanh toán". 2. Backend xử lý trừ tiền thành công từ tài khoản …

☰ Mục lục bài viết (4 phần) Mở / Đóng mục lục

Nguy cơ trừ tiền 2 lần khi mạng chập chờn#

Trong thanh toán trực tuyến, tình huống kinh điển sau thường xuyên xảy ra:

  1. Người dùng bấm "Thanh toán".
  2. Backend xử lý trừ tiền thành công từ tài khoản ngân hàng.
  3. Khi backend gửi phản hồi về cho mobile app, mạng 4G của người dùng bị rớt gói tin (packet loss).
  4. Mobile app tưởng thanh toán chưa thành công nên tự động gửi lại request (Retry) hoặc người dùng sốt ruột bấm nút thêm một lần nữa.

Nếu API của bạn không có tính Idempotent (Tính lũy thừa / Tính bất biến qua nhiều lần gọi), người dùng sẽ bị trừ tiền 2 lần!

Cơ chế Idempotency Key chuẩn quốc tế (như Stripe)#

Mỗi khi gửi request thanh toán, client bắt buộc phải sinh một chuỗi UUID duy nhất đặt trong Header: Idempotency-Key: c9b8f2d3-....

CODE
Client (Gửi Request kèm Idempotency-Key)
       │
       ▼
[ Backend Middleware ]
       │──> Kiểm tra Key trong Redis
       │
       ├─► [Đang xử lý]: Trả về HTTP 409 Conflict hoặc chờ
       ├─► [Đã hoàn thành]: Trả về đúng Response đã lưu trong Cache!
       └─► [Chưa từng có]: Đánh dấu "Processing" và tiếp tục chạy logic

Code mẫu Middleware xử lý Idempotency bằng Node.js & Redis#

TYPESCRIPT
import type { Request, Response, NextFunction } from 'express';
import Redis from 'ioredis';

const redis = new Redis();

export async function idempotencyMiddleware(req: Request, res: Response, next: NextFunction) {
  const idempotencyKey = req.header('Idempotency-Key');
  if (!idempotencyKey) return next();

  const cacheKey = `idempotency:${idempotencyKey}`;
  
  // 1. Thử thiết lập khóa với cờ NX (Chỉ set nếu chưa tồn tại)
  const acquired = await redis.set(cacheKey, JSON.stringify({ status: 'PROCESSING' }), 'EX', 120, 'NX');
  
  if (!acquired) {
    const cachedData = await redis.get(cacheKey);
    const parsed = JSON.parse(cachedData || '{}');
    
    if (parsed.status === 'PROCESSING') {
      return res.status(409).json({ message: 'Giao dịch đang được xử lý, vui lòng không gửi lại.' });
    }
    
    // Trả về kết quả trước đó mà không thực thi lại logic trừ tiền!
    return res.status(parsed.statusCode).json(parsed.body);
  }

  // 2. Bọc hàm res.json để lưu kết quả sau khi xử lý xong
  const originalJson = res.json.bind(res);
  res.json = (body: any) => {
    redis.set(cacheKey, JSON.stringify({ status: 'COMPLETED', statusCode: res.statusCode, body }), 'EX', 86400);
    return originalJson(body);
  };

  next();
}

Góc thảo luận#

Hệ thống thanh toán của công ty bạn đã tích hợp cơ chế Idempotency Key chưa? Thời gian tồn tại (TTL) của một key anh em thường để bao lâu?


Xem thêm nội dung liên quan:

Chia sẻ thảo luận này:

Thảo luận

1 phản hồi

Ý kiến từ cộng đồng kỹ sư TopDev
💬

Tham gia thảo luận

Cộng đồng mở · Đăng nhập 1-chạm qua Google
  1. Thu Trang (Backend Engineer)
    #1

    Một lưu ý rất quan trọng: Idempotency Key nên được hash kèm theo UserId hoặc Token của request để tránh trường hợp hacker đoán mò hoặc tái sử dụng key của người khác để lấy trộm thông tin response cũ.

Bài liên quan trong #Backend & Microservices

1
TopDev AI Companion
AI
✓ Đã sao chép link