Trang chủ Bài viết Tin công nghệ Vercel Cron Jobs trong môi trường production: an toàn và chống trùng lặp với Next.js
Tin công nghệ 12/11/2026 1 lượt xem

Vercel Cron Jobs trong môi trường production: an toàn và chống trùng lặp với Next.js

Vercel Cron Jobs trong môi trường production: an toàn và chống trùng lặp với Next.js

Cron job chạy lúc 02:00 UTC mỗi ngày có thể không bao giờ tới ứng dụng, hoặc tới hai lần vào hôm sau và cả hai lần đều cố tạo cùng một báo cáo. Một route xử lý "hôm nay" theo mỗi request sẽ bỏ qua ngày bị lỡ và có thể lặp lại việc của hôm qua. Cách khắc phục là ghi lại ngày nào đến hạn và ngày nào đã hoàn thành trong PostgreSQL.

Bài viết xây dựng một endpoint cron trong Next.js: mỗi ngày báo cáo đã đóng được đưa vào hàng đợi đúng một lần, tự bắt kịp sau các lần gọi bị lỡ và có thể tiếp tục sau khi tiến trình gặp sự cố. Ví dụ ghi số lượng sự kiện theo ngày vào PostgreSQL; phần sau giải thích điều gì thay đổi khi job gửi email hoặc gọi API bên ngoài.

Cấu hình lịch và bảo vệ route

Vercel Cron Jobs gọi một route production bằng HTTP GET theo lịch UTC. Cấu hình đặt ở thư mục gốc của dự án Vercel; trong monorepo đó là app root đã cấu hình, có thể khác gốc repository. Ví dụ đặt lịch 02:00 UTC hằng ngày, nhưng gói Hobby có thể chạy trong phạm vi giờ đã định nên không nên phụ thuộc vào phút chính xác.

Đặt một CRON_SECRET ngẫu nhiên ở môi trường Production; Vercel gửi nó dưới dạng Bearer token. Route phải từ chối khi thiếu biến này và không bao giờ ghi log header. Một request tới route chỉ là tín hiệu đánh thức: nó có thể đến trễ, bị lặp hoặc không đến, và Vercel không thử lại lần gọi cron thất bại. Vì vậy hãy giữ lịch như cơ chế kích hoạt và lưu trạng thái báo cáo thực tế trong cơ sở dữ liệu.

Mô hình hóa theo ngày xử lý

Khóa nghiệp vụ là ngày báo cáo theo UTC. Request ID chỉ dùng để nhận diện lần gọi và phục vụ log, còn hai request cho cùng một ngày phải chia sẻ cùng khóa nghiệp vụ. Cần lưu con trỏ cho ngày kế tiếp cần đưa vào hàng đợi, một job duy nhất cho mỗi ngày và bản thân báo cáo. Trạng thái đã xếp hàng và đã hoàn thành phải tách biệt: xếp hàng thành công không có nghĩa báo cáo đã tồn tại.

Schema gồm bảng sự kiện nguồn, bảng lịch với cột next_day, bảng job theo ngày với queued_at và completed_at, cùng bảng báo cáo. Nếu cần báo cáo theo từng workspace thì dùng khóa job ghép và đưa workspace vào cả truy vấn dữ liệu lẫn khóa báo cáo.

Backfill ngày bị lỡ mà không tranh chấp

Ngày đến hạn được tính bằng đồng hồ cơ sở dữ liệu theo UTC. Lúc 01:59 UTC ngày 11/11, ngày 10/11 vẫn nằm trong khoảng ân hạn hai giờ; đến 02:00 thì thành đến hạn. Quy tắc này xử lý được cả lần gọi trễ lẫn gọi thủ công mà không phải đoán khoảng thời gian từ lúc request tới.

Một câu lệnh duy nhất khóa dòng lịch, đưa tối đa bảy ngày còn thiếu vào hàng đợi và tiến con trỏ. PostgreSQL thực hiện các thay đổi này một cách nguyên tử: nếu thất bại thì cả con trỏ lẫn hàng đợi giữ nguyên; nếu thành công thì job đã bền vững trước khi route xử lý. Lần gọi thứ hai có thể chờ giao dịch ngắn đó kết thúc, thấy con trỏ mới và xếp lô kế tiếp nếu còn. Khóa chính của job bảo vệ chống trùng sau khi sửa tay hoặc chạy lại. Giới hạn bảy ngày giúp mỗi lần gọi có biên; sau sự cố dài cần nhiều lượt quét, nên cảnh báo theo khoảng cách giữa ngày đến hạn mới nhất và con trỏ.

Nhận việc và ghi kết quả trong cùng giao dịch

Với báo cáo chỉ dùng cơ sở dữ liệu, worker sạch nhất là một giao dịch. Chọn một dòng đang chờ bằng FOR UPDATE SKIP LOCKED để các worker cạnh tranh xử lý các ngày khác nhau trong khi dòng được chọn vẫn bị khóa. Câu lệnh ghi số liệu tổng hợp và đánh dấu job hoàn thành cùng lúc; sự cố trước khi commit sẽ nhả khóa và để dòng ở trạng thái chờ.

Khoảng UTC nửa mở gồm nửa đêm đầu ngày và loại nửa đêm ngày kế. Dùng COUNT(e.id) để ngày trống vẫn bằng 0. SKIP LOCKED phù hợp để nhận việc, không phù hợp cho báo cáo cần nhìn toàn bộ dòng đang chờ. Ví dụ cho phép các ngày độc lập hoàn thành không theo thứ tự; nếu thứ Ba phụ thuộc thứ Hai thì phải thể hiện phụ thuộc trong quy tắc nhận việc hoặc tuần tự hóa theo luồng job.

Giữ handler Next.js trong giới hạn

Cài pg và kiểu TypeScript, xuất hai câu SQL trên dưới dạng chuỗi tĩnh. Handler dùng một kết nối PostgreSQL mỗi lần gọi và chạy mỗi câu lệnh trong giao dịch riêng; URL cơ sở dữ liệu và cron secret chỉ tồn tại phía server. Đặt runtime là nodejs, maxDuration 30 giây, giới hạn connection timeout và statement timeout.

Handler so sánh secret bằng timingSafeEqual trên digest SHA-256, trả 503 nếu thiếu cấu hình và 401 nếu sai. Sau khi xếp lịch, vòng lặp xử lý tối đa bảy job trong khoảng 20 giây, ghi log kết quả kèm runId và trả về enqueued, completedDays, nextDay, elapsedMs với Cache-Control no-store. Vì hai câu lệnh commit riêng, phần xếp hàng sống sót qua sự cố trước khi xử lý, và báo cáo đã xong sống sót khi mất phản hồi. Với tải nặng hơn, nên chuyển worker sang queue hoặc workflow bền vững và để route cron chỉ lo đối soát.

Xử lý hiệu ứng bên ngoài riêng

Khóa SQL không thể hoàn tác việc gửi email hay gọi HTTP. Không giữ giao dịch mở trong khi gọi nhà cung cấp chậm; hãy ghi một dòng outbox trong cùng giao dịch với trạng thái nghiệp vụ, rồi để worker khác nhận và gửi. Dùng idempotency key ổn định như tên báo cáo cộng ngày và ID người nhận, ghi lại kết quả nhà cung cấp. Nếu nhà cung cấp đã nhận request nhưng phản hồi bị mất và không thể chống trùng, phải đối soát trước khi thử lại; chỉ riêng timeout không cho biết hiệu ứng đã xảy ra hay chưa.

Kiểm thử trùng lặp, ngày bị lỡ và gián đoạn

Áp schema lên cơ sở dữ liệu PostgreSQL dùng một lần. Chạy truy vấn xếp hàng tại 2026-11-11T01:59:00Z rồi tại 02:00:00Z: với con trỏ ví dụ, lần đầu đưa hai ngày vào hàng đợi, lần sau thêm một ngày. Gọi lại với cùng mốc thời gian và kỳ vọng không có dòng mới. Một mốc tương lai xa hơn con trỏ chỉ nên xếp tối đa bảy ngày.

Nguồn: Planet PostgreSQL

Trích nguồn từ: Planet PostgreSQL

Chia sẻ:

Bài viết liên quan

Zalo