AI 资讯
Idempotency — Safe Retry
Safe retry: idempotency key để retry một request không biến thành hai lần charge Trong hệ phân tán, retry là mặc định — client, gateway, load balancer, queue consumer đều retry khi timeout hoặc lỗi tạm thời. Vấn đề là nhiều thao tác quan trọng không idempotent tự nhiên: một request POST /charges gửi hai lần thì trừ tiền khách hai lần, một message OrderCreated xử lý hai lần thì ship hai đơn. Idempotency key là cơ chế để server nhận diện "cùng một intent" giữa các lần retry và chỉ thực hiện side effect một lần , trong khi vẫn trả về response giống hệt cho mọi lần gọi lặp — về mặt hiệu ứng thấy được từ bên ngoài, đây là cái người ta hay gọi là "exactly-once effect" (dù ở tầng transport vẫn là at-least-once). Cơ chế hoạt động Client sinh một identifier duy nhất cho mỗi thao tác (thường là UUIDv4) và đính kèm request — quy ước phổ biến là HTTP header Idempotency-Key (Stripe API dùng đúng tên này, và IETF có draft draft-ietf-httpapi-idempotency-key-header chuẩn hoá cùng tên header). Server dùng key làm identity của thao tác trong một cửa sổ TTL: Nhận request với key K . Tra K trong idempotency store. Nếu tồn tại và request cũ ở trạng thái terminal (đã có response), trả lại response đã lưu — không chạy lại business logic. Nếu tồn tại nhưng đang in-flight , trả 409 Conflict (hoặc chờ, tuỳ contract). Nếu chưa tồn tại, INSERT bản ghi với unique constraint trên key, chạy business logic, persist response, commit. Điểm cốt lõi là bước insert + bước business logic + bước lưu response phải nằm trong cùng một transaction boundary — hoặc chí ít, phải có cơ chế đảm bảo không có window mà một retry khác nhìn thấy "chưa có key" trong lúc lần đầu vẫn đang chạy dở. CREATE TABLE idempotency_keys ( key TEXT NOT NULL , user_id BIGINT NOT NULL , request_hash TEXT NOT NULL , -- fingerprint payload status TEXT NOT NULL , -- in_flight | succeeded | failed response_code INT , response_body JSONB , created_at TIMESTAMPTZ NOT NULL DEFAULT now (), locked_until TIMESTAMPTZ , PRIMARY KEY ( user_id ,
AI 资讯
Message Queue — Async Processing
Async processing qua message queue: vì sao đẩy việc nặng ra khỏi request path, và cái giá phải trả bằng eventual consistency Async processing là mô hình tách một request thành hai giai đoạn: request handler nhận việc, xác nhận với client, rồi giao phần xử lý thật cho một worker chạy ngoài request path — thường qua một message queue (RabbitMQ, AWS SQS, Kafka, Redis Streams, hoặc queue trên nền Redis như BullMQ/Sidekiq). Lý do dev gặp nó trong việc thật rất cụ thể: một endpoint gọi payment provider mất 3s, gửi email confirm mất 1s, resize ảnh mất 5s — nếu làm tuần tự trong request, p99 latency của endpoint là tổng các con số đó, và một downstream chậm hoặc chết đủ để làm timeout hết thread pool của app server. Đẩy vào queue thì request trả về trong vài chục ms; nhưng đổi lại, cái "xong" mà client thấy không còn nghĩa là việc đã thực sự hoàn thành. Cơ chế hoạt động Ba thành phần: producer (thường là API server) đóng gói việc thành message rồi publish vào broker; broker (RabbitMQ/SQS/Kafka…) giữ message trong queue có persistence tuỳ cấu hình; consumer/worker poll hoặc được push message, xử lý, rồi ack để broker biết xoá. Nếu worker chết trước khi ack, broker redeliver — đây là gốc của semantic at-least-once : mỗi message được giao ít nhất một lần, có thể nhiều lần. Exactly-once trong hệ phân tán chỉ đạt được ở lớp application bằng cách consumer viết idempotent, không phải bằng cấu hình broker. Ví dụ với RabbitMQ + Node ( amqplib ): // producer — trong HTTP handler const ch = await conn . createConfirmChannel () await ch . assertQueue ( ' image.resize ' , { durable : true }) app . post ( ' /upload ' , async ( req , res ) => { const jobId = crypto . randomUUID () const payload = Buffer . from ( JSON . stringify ({ jobId , s3Key : req . body . key })) await ch . sendToQueue ( ' image.resize ' , payload , { persistent : true , // ghi xuống disk, sống sót broker restart messageId : jobId , // để consumer dedupe contentType : ' application/json ' , }) // đợi broker confirm đ