Thông báo: không mất, không gửi trùng
7/8
Bài 7 / 8~12 phútKhông mất, không gửi trùngMiễn phí… lượt xem

Đối chiếu thực tế: các hệ thống thông báo thật chống trùng thế nào?

Đặt thiết kế của bạn cạnh APNs, FCM, Resend, Twilio Verify và LinkedIn: mỗi hệ chống trùng ở chặng nào, vì ràng buộc gì, và vì sao chọn khác bạn.

TL;DR: Bài này đặt thiết kế của bạn cạnh năm hệ thống công khai: APNs và FCM (dịch vụ push của Apple và Google), Resend (API gửi email), Twilio Verify (API gửi OTP) và hệ thống thông báo của LinkedIn. Mỗi hệ đặt việc chống trùng ở một chặng khác nhau trên đường gửi, và chặng đó quyết định trùng nào bị chặn, trùng nào còn lọt. Push gộp tin cùng khoá thay vì chặn, email nhận khoá gửi kèm trong 24 giờ, OTP giữ nguyên mã để tin trùng thành vô hại. Họ chọn khác bạn vì ràng buộc của họ khác bảng của bạn.

Bạn đã có một thiết kế: OTP đỉnh 200 tin/giây qua SMS giá 700đ một tin, 5 triệu tin khuyến mãi dồn vào 15 phút, nhà cung cấp SMS không nhận khoá chống trùng, còn push có khoá gộp phía thiết bị. Đừng hỏi "người ta làm thế nào"; hỏi "họ có ràng buộc gì mà bảng của tôi không có".

Header apns-id của APNs nghe như khoá chống trùng, nhưng Apple chỉ dùng nó để báo lỗi. Mỗi hệ dưới đây kèm tên tài liệu, thời điểm đọc và link nguồn.

1. Cách đọc một case: tìm chặng và ràng buộc

Nghĩ tới việc gửi thư bảo đảm: điện thoại rớt trước khi bạn nghe câu "đã nhận", nên bạn mang thêm một bản. Bưu điện nào ghi số hiệu bưu gửi thì thấy trùng số và trả lại bản thứ hai. Hộp thư nhà người nhận chỉ chứa một giấy báo, giấy mới đè giấy cũ. Thư người ta đã mở ra đọc thì không ai lấy lại được.

Gửi thư bảo đảmHệ thống thông báo
Không nghe được câu "đã nhận"Lời gọi nhà cung cấp (NCC) bị timeout, vùng mập mờ
Số hiệu bưu gửi, bưu điện tự lọcKhoá gửi kèm mà NCC nhận và tự chống trùng
Hộp thư chỉ chứa giấy báo mới nhấtKhoá gộp (collapse) của push: tin mới thay tin cũ cùng khoá
Thư đã mởSMS, email đã tới máy, không chặng nào thu hồi được
Hai bản thư giống hệt nhauTin trùng cùng nội dung, phiền nhưng vô hại
💡 Cách nhớ

Hỏi mỗi case ba câu: họ chặn trùng ở chặng nào, chặng đó chặn được loại trùng nào, ràng buộc nào buộc họ chọn chặng đó.

Nhớ lại — Gửi rồi đánh dấu

Vùng mập mờ là thứ bạn đã đếm ở vòng gửi rồi đánh dấu: lời gọi timeout thì phía bạn không biết NCC đã gửi hay chưa, thử lại thì có thể trùng, không thử thì có thể mất.

Năm chặng một tin đi qua, mỗi chặng kèm cơ chế chống trùng đặt ở đó; chặng API nhà cung cấp được nhấn vì chặn được thử lại sau timeout, chặng máy người dùng tô cảnh báo vì SMS và email đã tới thì không thu hồi được

2. Khoá gộp của APNs và FCM có chống trùng không?

APNs (Apple Push Notification service) và FCM (Firebase Cloud Messaging của Google) là hai cổng bắt buộc khi push tới iPhone và Android: bạn gọi API của họ, họ giữ kết nối tới từng máy và giao tin.

Thử đoán trước

Bạn vận hành cổng push cho hàng tỉ thiết bị, nhiều máy tắt mạng cả tuần. Bạn giữ bao nhiêu tin chờ cho mỗi máy, và vượt mức thì làm gì?

APNs: một tin chờ cho mỗi app

Tài liệu "Sending notification requests to APNs" của Apple (đọc tháng 10/2026) cho thấy apns-id chỉ là UUID để APNs báo lỗi về, không phải khoá chống trùng. Khoá gộp thật là apns-collapse-id (tối đa 64 byte): gửi cùng một thông báo nhiều lần thì dùng cùng giá trị để gộp thành một thông báo cho người dùng.

Chi tiết đáng đặt cạnh bảng của bạn nằm ở kho chờ. APNs giữ tin chưa giao được theo thời hạn apns-expiration bạn đặt, nhưng với mỗi app chỉ giữ một thông báo, thường là tin mới nhất, và Apple ghi rõ điều đó không luôn được bảo đảm. Apple không nêu lý do; suy từ chính tài liệu (best-effort, hành vi tuỳ trạng thái pin của máy) thì ràng buộc của họ là số máy cực lớn và nhiều máy offline lâu, nên kho chờ nhỏ và chấp nhận mất. Dù bạn chọn gửi ít nhất một lần, "ít nhất một lần" chỉ kéo tới chặng API của Apple.

FCM: 100 tin chờ, quá thì bỏ hết

Tài liệu "Collapsible and non-collapsible messages" của Firebase (đọc tháng 10/2026) định nghĩa tin collapsible là tin có thể bị thay bằng tin mới nếu chưa được giao tới thiết bị. Bạn đánh dấu bằng collapse_key; FCM giữ đồng thời tối đa bốn tin như vậy cho mỗi thiết bị, mỗi tin một khoá. Tin không gộp được thì giữ tối đa 100 tin cho mỗi máy Android. Chạm mức đó, FCM bỏ toàn bộ tin đang giữ, rồi báo app lúc máy kết nối lại để app tự đồng bộ với server của bạn.

Đó là lựa chọn thứ ba ngoài cặp "tối đa một lần / ít nhất một lần": chấp nhận mất, nhưng phát tín hiệu mất. Chữ "chưa được giao" thì hay bị đọc lướt: tin đầu đã hiện trên máy rồi lần thử lại mới tới, khoá gộp không còn gì để thay và người dùng thấy hai thông báo. Khoá gộp chỉ thu hẹp vùng trùng khi máy offline.

Ở bài 03 bạn so tỉ lệ mất nhân giá mất với tỉ lệ trùng nhân giá trùng cho từng loại. Với tin khuyến mãi, giá một tin mất gần bằng 0, còn tin trùng khiến 1% người nhận tắt thông báo. APNs và FCM tự chọn phía mất cho tin chờ lâu: khớp với kết luận của bạn ở khuyến mãi, lệch hẳn ở tin giao dịch.

3. Email: nhà cung cấp nhận khoá gửi kèm

Thử đoán trước

Bạn là NCC gửi email. Khách gọi bạn, timeout phía họ, họ gọi lại đúng nội dung đó. Bạn cần khách gửi kèm gì để không gửi hai lần, và nhớ nó bao lâu?

Tài liệu "Idempotency Keys" của Resend (đọc tháng 10/2026) trả lời đúng câu đó. Bạn gửi header Idempotency-Key dài tới 256 ký tự, Resend nhớ khoá 24 giờ. Gọi lại cùng khoá, cùng nội dung thì API trả phản hồi cũ mà không gửi lần nữa. Cùng khoá khác nội dung thì nhận 409 mã invalid_idempotent_request; hai yêu cầu cùng khoá chạy song song thì yêu cầu sau nhận 409 mã concurrent_idempotent_requests, thử lại sau là an toàn.

Đây là chặng duy nhất đóng được vùng mập mờ do timeout, vì chỉ NCC biết lần gọi trước đã thật sự gửi hay chưa.

Cơ chế của Resend đúng là idempotency key bạn đã học ở khoá Phân tán: bên nhận nhớ khoá đã xử lý, gặp lại thì trả kết quả cũ. Khác ở chỗ bên nhận là công ty khác, nên thời hạn nhớ khoá do họ đặt.

Ràng buộc khác bạn: bảng của bạn giả định NCC SMS không nhận khoá, nên bài 04 kết luận phần trùng do timeout chỉ thu hẹp được. Resend cho thấy phần còn lọt ấy là điều khoản hợp đồng, không phải định luật. Khi chọn NCC SMS, câu "có nhận khoá gửi kèm không, nhớ bao lâu" đáng nằm cạnh giá 700đ một tin. Thời hạn 24 giờ cũng là ràng buộc: job kẹt trạng thái đang gửi qua đêm rồi mới được thu hồi sẽ mang một khoá mà Resend đã quên.

Thử ngẫmtin "đơn đã giao" đi qua push, máy người dùng offline hai ngày và trong lúc đó có thêm ba tin khuyến mãi. Với kho một tin mỗi app của APNs, mở máy ra người dùng thấy tin nào?

Nhớ lại — Khoá chống trùng

Khoá chống trùng bạn chọn cho job ở bài 04, ví dụ bộ người dùng, loại thông báo, mã nghiệp vụ, dùng lại được làm khoá gửi kèm NCC, miễn là nó sinh một lần lúc tạo job.

4. OTP: chặn tin trùng hay làm nó vô hại?

Thử đoán trước

Bảng của bạn ghi giá một tin OTP trùng là 700đ cộng rủi ro người dùng nhập mã cũ đã bị vô hiệu. Nếu lần gửi lại mang đúng mã cũ thì phía trùng còn khoản nào? Mức giao nhận bạn chọn cho OTP ở bài 03 có đổi không?

Twilio Verify là API gửi mã OTP tới số điện thoại rồi kiểm mã người dùng nhập. Trang hướng dẫn cho lập trình viên của Twilio Verify (đọc tháng 10/2026) ghi rằng mã có hiệu lực 10 phút, và người dùng yêu cầu gửi lại trong 10 phút đó thì nhận cùng một mã. Twilio cũng khuyên giới hạn mỗi số điện thoại một yêu cầu mỗi 30 giây, kèm backoff luỹ thừa.

Twilio để tin trùng lọt rồi hạ giá của nó xuống. Hai tin cùng mã thì nhập mã nào cũng đúng, chỉ còn tiền SMS. Giả sử 1 trong 100 lượt gửi OTP thành trùng: 200 tin/giây sinh 2 tin trùng mỗi giây, tức 1.400đ mỗi giây, khoảng 5 triệu đồng mỗi giờ đỉnh. Đó là tiền có trần và đo được, không còn là người dùng bị chặn đăng nhập.

Ràng buộc khác bạn: Twilio tự sinh và tự giữ mã, còn bảng của bạn ngầm giả định mỗi lần gửi sinh mã mới. Lưu mã vào job lúc tạo để mọi lần thử lại đọc đúng mã đó, phía trùng của OTP rẻ đi rõ rệt, và phép so ở bài 03 có thể nghiêng sang gửi ít nhất một lần.

Với OTP SMS ở Việt Nam, không ngân hàng hay sàn nào công bố kiến trúc gửi OTP, nên bài này không suy đoán. Tự kiểm trên hai ứng dụng bạn dùng: bấm "Gửi lại mã" khi mã đầu còn hạn, rồi nhập mã đầu tiên. Còn dùng được thì ứng dụng đó đang làm tin trùng thành vô hại; bị từ chối thì họ chịu đúng khoản giá mà bảng của bạn ghi.

Thử ngẫmbạn chuyển sang gửi lại đúng mã cũ cho OTP. Khoản giá nào của tin mất vẫn còn nguyên, và vì sao nút "Gửi lại mã" vẫn cần giới hạn tần suất?

5. Tin thừa: LinkedIn coi gửi quá nhiều là cái giá chính

Bài báo "Near real-time optimization of activity-based notifications" của nhóm LinkedIn tại KDD 2018 nhìn từ phía khác. Phần tóm tắt viết rằng gửi thông báo cho mọi sự kiện sẽ làm phiền thành viên, nên họ đưa bài toán về tối ưu có ràng buộc giữa mức tương tác và lượng gửi, quyết định gửi gần thời gian thực trên hệ xử lý stream.

Ràng buộc khác bạn: thứ bài báo tối ưu là thông báo hoạt động (activity-based, kiểu có người xem hồ sơ, có người thích bài), loại mà bỏ một tin gần như không mất gì. Họ vì thế cố ý không gửi một phần, tức chọn mất ngay trước hàng job. Tin khuyến mãi của bạn cùng loại đó, và người đã tắt thông báo vì bị làm phiền thì cả push giao dịch cũng không nhận nữa.

Nhớ lại — Cách ly luồng gửi

Ở bài 05, OTP kẹt sau 500 nghìn job khuyến mãi khi NCC push chậm. Tầng lọc kiểu LinkedIn giảm số job chen trước OTP, nhưng không bảo đảm hạn 15 giây; cách ly mới bảo đảm.

6. Bảng tổng hợp: ràng buộc nào đổi lựa chọn nào

Ba câu hỏi khi phải thử lại sau timeout: nhà cung cấp nhận khoá gửi kèm, tin mới thay được tin cũ, bạn giữ được mã cũ; cả ba đều không thì phần trùng còn lọt phải đo và nhân giá

HệNguồn, nămRàng buộc khác bạnLựa chọnChặng
APNsTài liệu Apple, đọc 10/2026Số máy cực lớn, offline lâu (suy luận)Giữ một tin mỗi app, khoá gộp 64 byteKho chờ NCC
FCMTài liệu Firebase, đọc 10/2026Như APNs4 khoá gộp, 100 tin chờ, quá thì bỏ hết và báo appKho chờ NCC
ResendTài liệu Resend, đọc 10/2026Khách thử lại sau timeoutIdempotency-Key nhớ 24 giờ, 409 khi lệch nội dungAPI NCC
Twilio VerifyTài liệu Twilio, đọc 10/2026Tự sinh và giữ mã OTPCùng mã trong 10 phút, một yêu cầu mỗi 30 giâyNội dung tin
LinkedInKDD 2018Tin hoạt động, giá mất gần 0Tối ưu lượng gửi, cố ý không gửiTrước hàng job
OTP SMS ở Việt NamTự quan sát, không có nguồn công bốKhông rõKhông suy đoánKhông rõ

Đọc dọc cột chặng: mọi hệ đều lo phần sau hàng job, nơi bạn không có quyền. Trong ba câu hỏi ở hình trên, chỉ câu cuối là bạn tự quyết được.

7. Pitfall tổng hợp

❌ Nhầm 1: dùng apns-id làm khoá chống trùng cho push.

Plain Text
apns-id: 7c9e6679-7425-40de-944b-e07fc1f90ae7

✅ apns-id là mã tra cứu để APNs báo lỗi; tài liệu không hứa lọc yêu cầu cùng apns-id. Muốn người dùng thấy một thông báo thì dùng apns-collapse-id, và vẫn giữ khoá chống trùng phía bạn cho job lặp, vì các kênh khác của cùng tin không có khoá gộp nào.

Plain Text
apns-collapse-id: giao_dich:812:55001

❌ Nhầm 2: sinh khoá gửi kèm mới ở mỗi lần gọi NCC.

SQL
-- Moi lan thu lai sinh khoa moi: NCC thay hai yeu cau khac nhau
SELECT id, payload, gen_random_uuid() AS idem_key
FROM job_gui
WHERE id = $1;

✅ Khoá sinh một lần lúc tạo job, nằm trên hàng job, mọi lần thử lại đọc lại đúng giá trị đó.

SQL
-- Khoa sinh mot lan luc INSERT, lan thu lai doc lai dung khoa cu
ALTER TABLE job_gui
  ADD COLUMN idem_key uuid NOT NULL DEFAULT gen_random_uuid();

SELECT id, payload, idem_key FROM job_gui WHERE id = $1;

Sinh khoá lúc gọi thì lần thử lại mang khoá khác, Resend coi là yêu cầu mới và gửi email thứ hai.

8. Đào sâu

📚 Đào sâu: tài liệu gốc của từng hệ

Tài liệu chính chủ (đã đọc tháng 10/2026):

Ghi chú: đọc tài liệu NCC nào cũng tìm ba thứ: họ nhận khoá nào, nhớ bao lâu, kho chờ đầy thì làm gì.

9. Liên hệ các bài khác

10. Tóm tắt

  • Khi đọc thiết kế của một hệ thông báo thật, tìm chặng họ đặt chống trùng trước, rồi mới tìm ràng buộc buộc họ chọn chặng đó.
  • Khi NCC nhận khoá gửi kèm, dùng lại khoá chống trùng của job làm khoá đó; khi NCC không nhận, đưa câu hỏi này vào tiêu chí chọn NCC.
  • Khi dùng push, coi khoá gộp là cách giảm số thông báo hiện ra; nó không bảo đảm hết trùng; cổng push tự chọn mất khi máy offline lâu.
  • Khi không chặn được trùng, hỏi xem có làm cho tin trùng vô hại được không, như gửi lại đúng mã OTP cũ.

11. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao khoá gửi kèm của Resend chặn được trùng do timeout, còn khoá chống trùng phía bạn ở bài 04 thì không?
  2. Q2
    APNs chỉ giữ một thông báo cho mỗi app khi máy offline, FCM giữ tới 100 tin rồi bỏ hết. Vì sao họ chọn mất, và điều đó giới hạn lựa chọn gửi ít nhất một lần của bạn thế nào?
  3. Q3
    Đồng nghiệp đề xuất gắn apns-collapse-id cho tin giao dịch và bỏ khoá chống trùng phía mình cho gọn. Bạn phản biện thế nào?
  4. Q4
    Bạn đổi luồng OTP sang lưu mã vào job lúc tạo và gửi lại đúng mã đó khi thử lại. Phép so ở bài 03 cho OTP đổi thế nào, và cái giá mới là gì?
  5. Q5
    LinkedIn cố ý không gửi một phần thông báo, còn bạn đang cố không làm mất tin nào. Khi nào lựa chọn của LinkedIn đúng cho hệ của bạn, và khi nào sai?

Bài tiếp theo: Tổng kết module

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Tổng kết case thông báo: thiết kế hoàn chỉnh và các cái giá