SQS standard/FIFO, visibility timeout, DLQ; SNS fan-out; EventBridge; so với Kafka/RabbitMQ
Tiếp đi: SQS, SNS và EventBridge. Checkout của shop-app hiện làm hết trong một request: lưu đơn, trừ tồn kho, gửi email, tạo hóa đơn PDF, báo cho hệ thống kho. Email server chậm là khách chờ 8 giây, và nếu service kho chết thì đặt hàng thất bại luôn. Ở chương Messaging mình đã dùng RabbitMQ và Kafka; lên AWS thì nên dùng gì và khác chỗ nào?
Bạn đã có nền tảng từ chương Messaging: vì sao cần queue, ack, retry, DLX, partition, consumer group. Bài này không lặp lại các khái niệm đó; nó trả lời câu "lên AWS thì dùng dịch vụ nào, cấu hình gì, code Spring thế nào, và đề thi hỏi gì". Bảng chọn SQS/SNS/MSK ở mức tổng quan đã có trong devops 18.5; ở đây ta đi sâu từng dịch vụ.
1. Bài toán: checkout làm 5 việc đồng bộ
POST /checkout Thời gian Nếu lỗi
1. INSERT order (RDS) 20 ms → phải fail
2. UPDATE stock (RDS, cùng transaction) 10 ms → phải fail
3. SMTP gửi email xác nhận 3–8 s → khách KHÔNG cần chờ
4. Render hóa đơn PDF, lưu S3 1–2 s → làm sau cũng được
5. HTTP POST sang hệ thống kho (WMS) 200 ms → WMS chết là đơn fail?!
─────────────────────────────────────────────────────────
Khách chờ: 5–10 s. Một trong 3 việc sau fail → rollback cả đơn, khách bực.
2. Vấn đề
- Coupling thời gian: checkout nhanh hay chậm phụ thuộc dịch vụ chậm nhất (SMTP).
- Coupling sẵn sàng: WMS down 10 phút thì shop ngừng bán 10 phút, dù đơn hàng chỉ cần WMS biết "trong vài phút tới".
- Không có retry có kiểm soát: lỗi tạm thời (timeout) bị coi như lỗi thật.
- Thêm người nhận = sửa checkout: mai muốn thêm "cộng điểm thưởng", "gửi Zalo", lại mở code checkout ra.
- Chạy RabbitMQ/Kafka tự quản trên EC2 (như chương Messaging) thì phải lo cluster, disk, patch, monitoring; trên AWS không có lý do để tự chạy trừ khi cần tính năng đặc thù.
3. Giải pháp: ba dịch vụ messaging managed, ba vai trò khác nhau
SQS = hàng đợi (queue): 1 message → 1 consumer xử lý, giữ tới 14 ngày, consumer kéo (pull)
SNS = pub/sub (topic): 1 message → N subscriber nhận ngay (push), không lưu
EventBridge = event bus có lọc theo nội dung (rule), nguồn AWS/SaaS/custom, có schedule, archive/replay
| Thuật ngữ | Nghĩa | Trên shop-app |
|---|---|---|
| Standard queue | Throughput gần vô hạn, at-least-once, best-effort ordering (có thể trùng, có thể lộn thứ tự) | shop-order-events |
| FIFO queue | Thứ tự đúng trong message group, exactly-once processing (dedup 5 phút), 300 msg/s (3.000 với batch, cao hơn với high-throughput mode); tên kết thúc .fifo | shop-stock-updates.fifo, group = productId |
| Visibility timeout | Sau khi consumer nhận, message bị ẩn trong N giây (mặc định 30 s, tối đa 12 giờ); không xóa kịp thì hiện lại cho consumer khác | 60 s cho email, 5 phút cho PDF |
| Long polling | WaitTimeSeconds 1–20 s: chờ có message mới trả về; giảm request rỗng, giảm tiền | Luôn 20 s |
| Message retention | 1 phút – 14 ngày (mặc định 4 ngày) | 7 ngày |
| Dead-letter queue (DLQ) | Queue nhận message đã bị nhận maxReceiveCount lần mà không xóa (redrive policy); có redrive to source để đẩy lại | shop-order-events-dlq, maxReceiveCount 5 |
| Delay queue / message timer | Trì hoãn hiển thị 0–15 phút (cả queue hoặc từng message) | Nhắc giỏ bỏ quên sau 15 phút |
| Message size | Tối đa 256 KB (1 MB với extended); lớn hơn → S3 + Extended Client Library | Chỉ gửi orderId, không gửi PDF |
| SNS topic | Standard (fan-out, at-least-once) hoặc FIFO (chỉ sang SQS FIFO) | shop-order-topic |
| Subscription | SQS, Lambda, HTTP/S, email, SMS, mobile push, Kinesis Firehose | 2 queue + 1 Lambda |
| Filter policy | Subscriber chỉ nhận message có attribute (hoặc body) khớp JSON policy | Queue PDF chỉ nhận status = PAID |
| Event bus | Default bus (sự kiện AWS), custom bus (shop-bus), partner bus (SaaS) | shop-bus |
| Rule + event pattern | Lọc theo source, detail-type, nội dung detail (prefix, numeric, exists, anything-but) | Đơn > 5 triệu → queue duyệt tay |
| Target | 20 target/rule: SQS, SNS, Lambda, Step Functions, API destination (HTTP ngoài), Kinesis, ECS task… | SQS + Lambda |
| Schema registry, archive & replay, Scheduler, Pipes | Tính năng riêng EventBridge | Replay 1 giờ event khi fix bug |
Kiến trúc mục tiêu cho checkout:
POST /checkout
1. INSERT order + UPDATE stock (RDS, 1 transaction) ~30 ms
2. Ghi OrderPlaced vào bảng outbox (cùng transaction) (xem distributed 14.7)
3. Trả 201 cho khách ← khách chờ ~50 ms
Outbox relay (hoặc publish trực tiếp nếu chấp nhận rủi ro) ──► SNS shop-order-topic
│ fan-out
┌─────────────────────────────┬───────────────────────┼──────────────────────┐
▼ ▼ ▼ ▼
SQS shop-order-events SQS shop-invoice-queue Lambda shop-order-notifier EventBridge shop-bus
(email + WMS) (PDF → S3) (SMS/push, bài 22.12) (rule: amount > 5tr → duyệt)
consumer: shop-app consumer: shop-app
@SqsListener @SqsListener
│ fail 5 lần
▼
SQS shop-order-events-dlq → CloudWatch alarm → người xem
4. Code / cấu hình từng bước
Bước 1: Tạo DLQ trước, rồi queue chính với redrive policy.
export AWS_REGION=ap-southeast-1
ACCOUNT_ID=$(aws sts get-caller-identity --profile shop-lab --query Account --output text)
DLQ_URL=$(aws sqs create-queue --profile shop-lab --queue-name shop-order-events-dlq \
--attributes MessageRetentionPeriod=1209600 \
--tags Project=shop-app,Env=lab --query QueueUrl --output text)
DLQ_ARN=$(aws sqs get-queue-attributes --profile shop-lab --queue-url $DLQ_URL --attribute-names QueueArn --query Attributes.QueueArn --output text)
Q_URL=$(aws sqs create-queue --profile shop-lab --queue-name shop-order-events \
--attributes "{
\"VisibilityTimeout\": \"60\",
\"ReceiveMessageWaitTimeSeconds\": \"20\",
\"MessageRetentionPeriod\": \"604800\",
\"RedrivePolicy\": \"{\\\"deadLetterTargetArn\\\":\\\"$DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"5\\\"}\",
\"SqsManagedSseEnabled\": \"true\"
}" --tags Project=shop-app,Env=lab --query QueueUrl --output text)
Q_ARN=$(aws sqs get-queue-attributes --profile shop-lab --queue-url $Q_URL --attribute-names QueueArn --query Attributes.QueueArn --output text)
DLQ nên có retention dài hơn queue chính (14 ngày): message vào DLQ mang theo tuổi từ lúc gửi ban đầu, retention ngắn là mất bằng chứng.
Bước 2: SNS topic + subscribe queue + access policy. SNS cần quyền sqs:SendMessage vào queue; không có policy này subscription tạo được nhưng message không tới (lỗi im lặng kinh điển).
TOPIC_ARN=$(aws sns create-topic --profile shop-lab --name shop-order-topic \
--tags Key=Project,Value=shop-app Key=Env,Value=lab --query TopicArn --output text)
cat > q-policy.json <<EOF
{"Version":"2012-10-17","Statement":[{
"Sid":"AllowSnsPublish","Effect":"Allow","Principal":{"Service":"sns.amazonaws.com"},
"Action":"sqs:SendMessage","Resource":"$Q_ARN",
"Condition":{"ArnEquals":{"aws:SourceArn":"$TOPIC_ARN"}}}]}
EOF
aws sqs set-queue-attributes --profile shop-lab --queue-url $Q_URL \
--attributes "{\"Policy\": $(jq -c . q-policy.json | jq -Rs .)}"
aws sns subscribe --profile shop-lab --topic-arn $TOPIC_ARN --protocol sqs --notification-endpoint $Q_ARN \
--attributes RawMessageDelivery=true
RawMessageDelivery=true: body SQS là đúng JSON bạn publish, không bị bọc trong envelope SNS ({"Type":"Notification","Message":"..."}), code consumer đơn giản hơn.
Bước 3: Dependency Spring Cloud AWS 3 (awspring, dùng SDK v2 bên dưới).
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-dependencies</artifactId>
<version>3.2.x</version> <!-- bản tương thích Spring Boot 3.3+ -->
<type>pom</type><scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter-sqs</artifactId>
</dependency>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter-sns</artifactId>
</dependency>
</dependencies>
spring:
cloud:
aws:
region:
static: ${AWS_REGION:ap-southeast-1}
# credentials: KHÔNG cấu hình → default provider chain (role EC2/ECS, hoặc AWS_PROFILE ở local)
sqs:
listener:
max-concurrent-messages: 10 # số message xử lý song song mỗi listener
max-messages-per-poll: 10
poll-timeout: 20s # long polling
shop:
messaging:
order-topic-arn: ${SHOP_ORDER_TOPIC_ARN}
order-queue: shop-order-events
Bước 4: Publisher dùng SnsTemplate kèm message attribute để filter policy lọc được.
public record OrderPlaced(String orderId, String userId, long amount, String status, Instant placedAt) {}
@Service
public class OrderEventPublisher {
private final SnsTemplate sns;
private final String topicArn;
public OrderEventPublisher(SnsTemplate sns, @Value("${shop.messaging.order-topic-arn}") String topicArn) {
this.sns = sns; this.topicArn = topicArn;
}
public void publish(OrderPlaced evt) {
sns.convertAndSend(topicArn, evt, Map.of(
"eventType", "OrderPlaced",
"status", evt.status(),
"amount", evt.amount())); // thành MessageAttributes (String / Number)
}
}
Bước 5: Consumer với @SqsListener. Ack mặc định là ON_SUCCESS: method return bình thường → xóa message; ném exception → không xóa → sau visibility timeout message hiện lại → retry; quá 5 lần → DLQ.
@Component
public class OrderEventsListener {
private static final Logger log = LoggerFactory.getLogger(OrderEventsListener.class);
private final MailService mail;
private final WmsClient wms;
private final ProcessedEventRepository processed; // bảng inbox: eventId PK
@SqsListener(value = "${shop.messaging.order-queue}", id = "order-events")
@Transactional
public void onOrderPlaced(OrderPlaced evt,
@Header(SqsHeaders.SQS_MESSAGE_ID_HEADER) String messageId,
@Header(SqsHeaders.SQS_APPROXIMATE_RECEIVE_COUNT) int receiveCount) {
if (processed.existsById(evt.orderId())) { // idempotency: standard queue có thể trùng
log.info("skip duplicate orderId={} messageId={}", evt.orderId(), messageId);
return; // return bình thường → message bị xóa
}
log.info("order {} receive #{}", evt.orderId(), receiveCount);
mail.sendOrderConfirmation(evt); // lỗi SMTP tạm → ném exception → retry
wms.notifyNewOrder(evt); // WMS down → retry sau 60 s, tối đa 5 lần
processed.save(new ProcessedEvent(evt.orderId(), Instant.now()));
}
}
Vì sao orderId làm key idempotency chứ không phải messageId: SNS fan-out và redrive từ DLQ sinh messageId mới cho cùng một sự kiện nghiệp vụ; chỉ orderId (hoặc eventId bạn tự sinh lúc publish) là ổn định. Pattern inbox/outbox đầy đủ ở distributed 14.7.
Bước 6: Gửi thẳng SQS bằng SqsTemplate (không qua SNS) khi chỉ có một consumer, ví dụ job PDF với delay:
@Service
public class InvoiceJobs {
private final SqsTemplate sqs;
public InvoiceJobs(SqsTemplate sqs) { this.sqs = sqs; }
public void schedulePdf(String orderId) {
sqs.send(to -> to.queue("shop-invoice-queue")
.payload(new InvoiceJob(orderId))
.delaySeconds(30) // chờ payment webhook ổn định
.header("jobType", "INVOICE_PDF"));
}
// FIFO: cần messageGroupId; deduplicationId tự sinh nếu queue bật ContentBasedDeduplication
public void updateStock(StockChanged evt) {
sqs.send(to -> to.queue("shop-stock-updates.fifo")
.payload(evt)
.messageGroupId(evt.productId())
.messageDeduplicationId(evt.eventId()));
}
}
Bước 7: EventBridge rule lọc theo pattern. Đơn giá trị lớn đi vào queue duyệt tay:
aws events create-event-bus --profile shop-lab --name shop-bus --tags Key=Project,Value=shop-app Key=Env,Value=lab
cat > pattern.json <<'EOF'
{
"source": ["shop.orders"],
"detail-type": ["OrderPlaced"],
"detail": {
"amount": [{"numeric": [">=", 5000000]}],
"status": ["PAID"],
"userId": [{"anything-but": {"prefix": "test-"}}]
}
}
EOF
aws events put-rule --profile shop-lab --name shop-high-value-orders --event-bus-name shop-bus \
--event-pattern file://pattern.json --state ENABLED --tags Key=Project,Value=shop-app
REVIEW_Q_ARN=... # queue shop-order-review (tạo như bước 1), policy cho events.amazonaws.com SendMessage
aws events put-targets --profile shop-lab --rule shop-high-value-orders --event-bus-name shop-bus \
--targets "Id=review-queue,Arn=$REVIEW_Q_ARN"
// Publish lên EventBridge từ Java (SDK v2 thuần, Spring Cloud AWS chưa có template riêng cho EventBridge)
eventBridge.putEvents(PutEventsRequest.builder().entries(PutEventsRequestEntry.builder()
.eventBusName("shop-bus").source("shop.orders").detailType("OrderPlaced")
.detail(objectMapper.writeValueAsString(evt))
.build()).build());
5. Cách hoạt động
Vòng đời một message trong SQS standard (shop-order-events, VisibilityTimeout=60, maxReceiveCount=5)
t=0 SNS → SendMessage → message M (ReceiveCount=0), hiển thị
t=1 consumer A: ReceiveMessage (long poll) → nhận M, M bị ẨN 60 s, ReceiveCount=1
t=1.5 A gửi mail OK, gọi WMS → timeout → ném exception → KHÔNG DeleteMessage
t=61 hết visibility → M hiện lại
t=61 consumer B nhận M (ReceiveCount=2) → inbox thấy orderId chưa xử lý xong? (mail đã gửi!)
→ đây là lý do từng bước phải idempotent, hoặc tách mail và WMS thành 2 queue
... lần 5 fail → SQS chuyển M sang shop-order-events-dlq (M giữ nguyên body, thêm tuổi)
DLQ CloudWatch ApproximateNumberOfMessagesVisible > 0 → alarm → người xem → fix WMS → redrive về source
Delete thành công: consumer gọi DeleteMessage(receiptHandle) trước khi hết visibility
Cần thêm giờ: ChangeMessageVisibility(receiptHandle, +300) (Spring Cloud AWS: Visibility.changeTo)
Fan-out SNS Filter policy (subscription của shop-invoice-queue)
publish(OrderPlaced, attrs) {"status": ["PAID"], "amount": [{"numeric": [">", 0]}]}
│
├──► shop-order-events (mọi đơn) → email + WMS
├──► shop-invoice-queue (chỉ PAID) → PDF
└──► Lambda shop-order-notifier (mọi đơn) → push
SNS KHÔNG lưu: subscriber chưa tồn tại lúc publish thì không bao giờ nhận. Vì vậy luôn SNS → SQS,
không SNS → HTTP thẳng vào shop-app (app restart là mất).
EventBridge: rule đánh giá mọi event trên shop-bus, khớp pattern thì gửi tới target (song song, tối đa 20)
Retry tới 24 h / 185 lần nếu target lỗi, rồi vào DLQ của rule (SQS). Archive bus → replay khoảng thời gian
khi consumer có bug (SNS không làm được điều này).
Visibility timeout chọn bao nhiêu? Quy tắc: ≥ thời gian xử lý tối đa của một message (p99 + biên). Quá ngắn → message hiện lại khi consumer vẫn đang xử lý → chạy đôi. Quá dài → message lỗi phải chờ lâu mới retry. Với Lambda consumer (bài 22.12), đặt ≥ 6 × timeout của function (AWS khuyên). Spring Cloud AWS cho phép acknowledgementMode = MANUAL và Visibility để gia hạn khi job dài.
6. So sánh: những cặp đề thi hay hỏi
| SQS Standard | SQS FIFO | |
|---|---|---|
| Thứ tự | Best-effort | Đúng trong message group |
| Trùng | At-least-once (có thể trùng) | Exactly-once processing (dedup 5 phút theo MessageDeduplicationId hoặc content hash) |
| Throughput | Gần vô hạn | 300 msg/s (3.000 batch), high-throughput mode: 70.000 msg/s theo partition |
| Song song | Tự do | Một group chỉ một consumer tại một thời điểm → nhiều group mới scale |
| Đề thi | "maximum throughput", "order not important", "duplicates handled by app" | "strict ordering", "exactly once", "no duplicates", "per-customer order" |
| shop-app | Email, PDF, WMS | Cập nhật tồn kho theo productId; sự kiện tài khoản theo userId |
| SQS | SNS | EventBridge | |
|---|---|---|---|
| Mô hình | Queue, pull, 1 consumer group | Pub/sub, push, N subscriber | Event bus, rule lọc nội dung, N target |
| Lưu message | Tới 14 ngày | Không (retry giao, rồi DLQ) | Không, nhưng có archive & replay |
| Lọc | Không | Filter policy theo attribute/body (đơn giản) | Pattern phong phú (numeric, prefix, exists, anything-but, $or) |
| Nguồn | App | App, CloudWatch alarm | App, 200+ dịch vụ AWS, SaaS partner, Scheduler |
| Target đặc biệt | Email, SMS, mobile push, HTTP | Step Functions, API destination (HTTP có auth), ECS task, 20 target/rule | |
| Throughput / độ trễ | Rất cao / ms | Rất cao / ms | Thấp hơn (giới hạn PutEvents theo region) / ~nửa giây |
| Schema | Không | Không | Schema registry, code binding |
| Đề thi | "decouple", "buffer", "retry", "DLQ" | "fan-out to multiple subscribers", "notify by email/SMS" | "react to AWS service events", "SaaS integration", "content-based routing", "schedule", "replay" |
| SQS/SNS/EventBridge | Amazon MQ (RabbitMQ/ActiveMQ) | MSK (Kafka) | Kinesis Data Streams | |
|---|---|---|---|---|
| Bản chất | Serverless, API HTTPS | Broker managed, giao thức AMQP/MQTT/JMS | Kafka managed (cluster hoặc serverless) | Streaming serverless của AWS |
| Khi nào | App mới trên AWS, cần đơn giản, scale tự động | Migrate app đang dùng RabbitMQ/JMS, không muốn đổi code | Đã dùng Kafka, cần replay dài, consumer group, ordering theo key, throughput khủng | Analytics real-time, nối Firehose/Analytics, shard |
| Replay | EventBridge archive; SQS không | Không (queue) | Có, theo retention | Có, 24 h – 365 ngày |
| Vận hành | Gần 0 | Broker instance, Multi-AZ | Cluster, broker, storage (serverless ít hơn) | Shard capacity hoặc on-demand |
| Đề thi | "serverless", "least operational overhead" | "existing application uses AMQP/MQTT/JMS", "migrate with minimal changes" | "Kafka", "existing Kafka clients" | "real-time analytics", "multiple consumers reading same stream", "replay" |
Chi tiết RabbitMQ vs Kafka về mô hình đã ở Messaging 8.10; trên AWS thêm một lớp: đề SAA hầu như luôn ưu tiên SQS/SNS/EventBridge trừ khi câu hỏi nói rõ "existing application uses JMS/AMQP" (→ Amazon MQ) hoặc "Kafka" (→ MSK).
7. Khi nào dùng / không dùng
- SQS: mọi việc nền có thể làm sau (email, PDF, đồng bộ hệ thống ngoài), buffer giữa service nhanh và chậm, chống spike (checkout nhận 10.000 đơn/phút, WMS xử lý 1.000/phút). Không dùng khi cần nhiều consumer đọc cùng message (dùng SNS → nhiều SQS), cần replay (EventBridge/Kinesis/MSK), hoặc message > 256 KB (S3 + pointer).
- SNS: một sự kiện nhiều nơi quan tâm; gửi alarm cho người (email/SMS). Không dùng SNS → HTTP vào app của bạn làm đường chính (mất message khi app down); luôn qua SQS.
- EventBridge: phản ứng với sự kiện AWS (EC2 state change, S3 object created, GuardDuty finding), tích hợp SaaS, routing theo nội dung, cron (Scheduler), cần replay. Không dùng cho throughput rất cao, độ trễ ms (SQS/SNS/Kinesis), hoặc chỉ một producer một consumer đơn giản (SQS đủ).
- Giữ đồng bộ: những gì khách phải biết kết quả ngay (còn hàng không, thanh toán được không). Đừng ném mọi thứ vào queue.
Mình chạy thử thì có đơn nhận hai email xác nhận giống nhau, cách nhau đúng một phút. Log thấy ApproximateReceiveCount là 2. Chuyển sang FIFO queue để hết trùng được không, hay phải sửa gì khác?
Bạn chẩn đoán gần đúng: cách nhau đúng 60 giây = visibility timeout của bạn. Lần nhận đầu, consumer gửi mail thành công rồi gọi WMS bị timeout, ném exception → message không bị xóa → 60 s sau hiện lại → consumer nhận lần 2 → gửi mail lần nữa. Đây không phải SQS "trùng" (standard cũng có thể trùng, nhưng hiếm hơn nhiều); đây là retry của chính bạn không idempotent. FIFO không chữa được: FIFO chỉ chống trùng lúc gửi (dedup 5 phút), còn message bạn nhận rồi không xóa thì FIFO cũng hiện lại y như vậy.
Ba cách sửa, nên dùng cả ba
1. Tách việc: mỗi queue một việc, để lỗi WMS không làm gửi lại mail
SNS shop-order-topic ─┬─► shop-order-mail-queue → MailListener (visibility 60 s)
├─► shop-order-wms-queue → WmsListener (visibility 120 s, retry 10)
└─► shop-invoice-queue → PdfListener
2. Idempotency theo khóa nghiệp vụ, kiểm tra TRƯỚC mỗi side effect
bảng processed_event(event_id PK, step, done_at) hoặc Redis SET NX với TTL
if (processed.exists(orderId, "MAIL")) skip; else { send; processed.save(orderId, "MAIL"); }
3. Visibility timeout đủ dài + gia hạn khi job chạy lâu
@SqsListener(..., acknowledgementMode = MANUAL)
public void handle(OrderPlaced evt, Acknowledgement ack, Visibility visibility) {
visibility.changeTo(300); // job PDF 3 phút → gia hạn trước
...
ack.acknowledge(); // chỉ xóa khi xong hết
}
Khi nào SQS standard thật sự giao trùng?
Khi một bản sao message trên server SQS chưa kịp đồng bộ trạng thái "đã xóa" (SQS lưu nhiều bản để bền). Xác suất rất thấp nhưng khác 0, nên hợp đồng là at-least-once. Cách phòng vẫn là idempotency; không có cách nào ở phía hạ tầng tắt hẳn. Vì vậy câu "exactly-once" trên hệ phân tán luôn là "at-least-once + idempotent consumer" (Messaging 8.9 cũng kết luận vậy với Kafka).
Vậy FIFO dùng khi nào?
Khi thứ tự quan trọng: cập nhật tồn kho của sản phẩm P phải theo đúng chuỗi "-1, -1, +1", không được xử lý "+1" trước. Message group = productId; trong một group chỉ một consumer xử lý tại một thời điểm, nên nhiều productId mới chạy song song. Và dedup ID = eventId giúp publisher retry an toàn (gửi 2 lần trong 5 phút chỉ tính 1). Giá: throughput thấp hơn, và một message kẹt làm cả group kẹt theo (head-of-line blocking) cho đến khi vào DLQ.
Mình thấy DLQ bắt đầu có message khi WMS bảo trì nửa tiếng. Giờ WMS sống lại, mình đẩy lại những message đó kiểu gì cho đúng? Và có nên để consumer tự đọc DLQ để retry không?
Đúng hướng: DLQ là nơi giữ bằng chứng, không phải nơi tự động retry. Nếu consumer tự đọc DLQ và xử lý lại giống hệt, bạn đã biến DLQ thành queue thường và mất ý nghĩa "dừng lại để người xem". Quy trình chuẩn gồm ba bước: báo động → xem → redrive.
1. Báo động ngay khi có message vào DLQ
aws cloudwatch put-metric-alarm --profile shop-lab --alarm-name shop-order-events-dlq-not-empty \
--namespace AWS/SQS --metric-name ApproximateNumberOfMessagesVisible \
--dimensions Name=QueueName,Value=shop-order-events-dlq \
--statistic Maximum --period 60 --evaluation-periods 1 --threshold 0 \
--comparison-operator GreaterThanThreshold --treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:$AWS_REGION:$ACCOUNT_ID:shop-alerts
Thêm alarm ApproximateAgeOfOldestMessage trên queue chính (ví dụ > 300 s): consumer chết hoặc quá tải, message đang xếp hàng dù chưa vào DLQ. Hai metric này là "cặp đôi" phải có cho mọi queue production.
2. Xem trước khi đẩy lại
# Nhìn mà KHÔNG lấy ra (visibility 0 → không ẩn message khỏi người khác lâu)
aws sqs receive-message --profile shop-lab --queue-url $DLQ_URL --max-number-of-messages 10 \
--visibility-timeout 0 --attribute-names All --message-attribute-names All \
| jq '.Messages[] | {Body, Sent: .Attributes.SentTimestamp, Count: .Attributes.ApproximateReceiveCount}'
Phân loại: (a) lỗi hạ tầng tạm (WMS down) → redrive hết; (b) message độc (body sai format, orderId không tồn tại) → redrive sẽ lại vào DLQ; sửa code hoặc xóa có chủ đích, ghi lại lý do.
3. Redrive về source queue (tính năng có sẵn, không cần code)
aws sqs start-message-move-task --profile shop-lab --source-arn $DLQ_ARN \
--max-number-of-messages-per-second 50 # không ghi --destination-arn = về source queue
aws sqs list-message-move-tasks --profile shop-lab --source-arn $DLQ_ARN \
| jq '.Results[] | {Status, ApproximateNumberOfMessagesMoved, ApproximateNumberOfMessagesToMove}'
Console: SQS → shop-order-events-dlq → Start DLQ redrive → Redrive to source queue (hoặc custom destination) → Velocity control. Message về source với ApproximateReceiveCount reset, lại được 5 lần thử. Có thể đẩy sang queue khác (--destination-arn) nếu bạn muốn một consumer chuyên xử lý "đồ cũ" với tốc độ chậm.
Những lỗi thiết kế DLQ hay gặp
| Lỗi | Hậu quả | Sửa |
|---|---|---|
| DLQ retention ngắn hơn queue chính | Message hết hạn trong DLQ trước khi ai kịp xem (tuổi tính từ lúc gửi ban đầu) | DLQ 14 ngày |
maxReceiveCount = 1 | Mọi lỗi mạng thoáng qua đều vào DLQ, alarm liên tục | 3–10 tùy việc; kết hợp backoff |
| Không có alarm | Hàng nghìn đơn mất email, phát hiện sau 2 tuần | Alarm > 0 như trên |
| Consumer tự poll DLQ để retry | Vòng lặp vô tận, không ai biết | Redrive có chủ đích |
| Lambda consumer: DLQ gắn vào Lambda thay vì queue | Với trigger SQS, Lambda không gửi vào DLQ của function; message quay về queue | Redrive policy trên queue; Lambda destination chỉ cho async invoke |
| FIFO + DLQ | Một message hỏng chặn cả group tới khi đủ receive count | maxReceiveCount thấp hơn cho FIFO; DLQ cũng phải FIFO |
Với Spring Cloud AWS, backoff giữa các lần retry chính là visibility timeout; muốn backoff tăng dần, consumer bắt exception và gọi visibility.changeTo(receiveCount * 60) trước khi ném lại.
SNS và EventBridge với mình nhìn giống nhau: đều nhận một sự kiện rồi phát cho nhiều chỗ, đều có lọc. Với OrderPlaced của shop-app mình nên publish vào cái nào? Có cần cả hai không?
Giống nhau ở "một vào, nhiều ra"; khác nhau ở độ tinh vi của lọc, nguồn sự kiện, replay, và độ trễ/throughput. Với shop-app câu trả lời thực dụng: event nghiệp vụ nội bộ, throughput cao, cần ms → SNS → SQS; event cần routing theo nội dung, tích hợp dịch vụ AWS/SaaS, cần replay, cần lịch → EventBridge. Nhiều team dùng cả hai với vai trò rõ ràng, không phải vì chồng chéo.
| Tiêu chí | SNS | EventBridge | Chọn cho shop-app |
|---|---|---|---|
| Fan-out tới SQS/Lambda | Có, hàng triệu msg/s | Có, giới hạn PutEvents (hàng nghìn/s theo region, nâng được) | Flash sale → SNS |
| Độ trễ | ~ms | ~0.5 s | Email/WMS không cần ms; cả hai đều được |
| Lọc | Filter policy trên attribute (và body từ 2022), toán tử cơ bản | Pattern trên toàn JSON: numeric range, prefix/suffix, anything-but, exists, wildcard, $or | "amount >= 5tr AND status PAID AND không phải test user" → EventBridge |
| Nguồn sự kiện AWS | Chỉ CloudWatch alarm và vài dịch vụ | 200+ dịch vụ: S3 (qua EventBridge notification), EC2, ECS, CodePipeline, GuardDuty, Health… | "Khi ảnh upload lên S3 → resize" (bài 22.12) → EventBridge |
| SaaS | Không | Partner bus: Stripe, Zendesk, Shopify, Datadog… | Webhook thanh toán → EventBridge |
| Gửi ra HTTP ngoài | HTTP/S subscription (xác nhận, ít kiểm soát) | API destination: OAuth/API key, rate limit, retry | Gửi tới WMS của đối tác → EventBridge |
| Replay | Không | Archive + replay | Fix bug consumer rồi phát lại 2 giờ event → EventBridge |
| Schedule (cron) | Không | EventBridge Scheduler (one-time, rate, cron, timezone) | Nhắc giỏ bỏ quên, báo cáo 8:00 → Scheduler |
| Gửi cho người | Email, SMS, push | Không trực tiếp (qua SNS target) | Alarm → SNS |
| Schema / contract | Không | Schema registry, discovery, code binding Java | Nhiều team cùng đọc event → EventBridge |
| Giá | Rất rẻ theo triệu request; tới SQS/Lambda miễn phí phần giao | Theo triệu event, đắt hơn SNS vài lần; event AWS mặc định miễn phí | Volume cực lớn → SNS |
Kiến trúc đề xuất
Checkout ──► SNS shop-order-topic ──┬──► SQS mail / wms / invoice (nội bộ, volume cao, ms)
└──► EventBridge shop-bus (một subscription, chuyển tiếp)
│ rules
├─ amount >= 5tr → SQS review (duyệt tay)
├─ status = PAID && country != VN → API destination (đối tác vận chuyển quốc tế)
├─ archive 30 ngày (replay khi cần)
└─ Scheduler: 15 phút sau OrderPlaced chưa PAID → nhắc thanh toán
Hoặc đơn giản hơn nếu volume vừa: publish thẳng vào EventBridge, bỏ SNS; rule "match all" đẩy sang các SQS. Nhiều kiến trúc mới trên AWS làm vậy. Đề SAA: thấy "AWS service events", "SaaS", "content-based filtering", "schedule", "replay" → EventBridge; thấy "fan-out to many SQS queues", "SMS/email to users", "highest throughput", "lowest latency" → SNS.
Cho mình bài lab để tự tay làm phần này nhé.
Lab 22.11: shop-order-topic fan-out, SQS + DLQ, FIFO, EventBridge rule
Mục tiêu
- Tạo
shop-order-events+shop-order-events-dlq(maxReceiveCount 3 để thấy nhanh),shop-invoice-queue, vàshop-stock-updates.fifo. - SNS
shop-order-topicfan-out sang 2 queue; filter policy để queue invoice chỉ nhậnstatus=PAID. - Spring Boot
@SqsListenernhận message từ local (profileshop-lab); mô phỏng lỗi để message vào DLQ sau 3 lần; redrive về source. - EventBridge
shop-busvới rule lọcamount >= 5000000đẩy vào queue review; so sánh thứ tự giữa standard và FIFO.
Chi phí & thời gian
- SQS Free Tier (always free): 1 triệu request/tháng. SNS: 1 triệu publish, giao tới SQS/Lambda miễn phí. EventBridge: custom event khoảng 1 USD/triệu, lab gửi vài chục event ≈ 0. Tổng lab ≈ 0 USD.
- Long polling 20 s làm app local poll ~3 request/phút/queue; để chạy cả ngày cũng trong Free Tier. Nếu bạn đặt
ReceiveMessageWaitTimeSeconds=0(short polling), app sẽ tạo hàng chục nghìn request rỗng/ngày: đây là cách tốn tiền SQS phổ biến nhất. - Không cần VPC, EC2, RDS. Thời gian: 60–75 phút.
Chuẩn bị
- Profile
shop-lab,AWS_REGION,ACCOUNT_ID; quyềnsqs:*,sns:*,events:*,cloudwatch:PutMetricAlarm. - Project shop-app với dependency Spring Cloud AWS 3 (mục 4 bước 3), Java 21.
jq. - Bảng/tập
processed_eventcho idempotency: trong lab dùngConcurrentHashMaphoặc H2; prod dùng RDS/DynamoDB.
Các bước
Bước 1: Queue + DLQ. Chạy block mục 4 bước 1 nhưng maxReceiveCount = 3 và VisibilityTimeout = 20 để demo nhanh. Thêm queue invoice và FIFO:
INV_URL=$(aws sqs create-queue --profile shop-lab --queue-name shop-invoice-queue \
--attributes ReceiveMessageWaitTimeSeconds=20,VisibilityTimeout=60 --tags Project=shop-app,Env=lab --query QueueUrl --output text)
INV_ARN=$(aws sqs get-queue-attributes --profile shop-lab --queue-url $INV_URL --attribute-names QueueArn --query Attributes.QueueArn --output text)
FIFO_URL=$(aws sqs create-queue --profile shop-lab --queue-name shop-stock-updates.fifo \
--attributes FifoQueue=true,ContentBasedDeduplication=false,ReceiveMessageWaitTimeSeconds=20 \
--tags Project=shop-app,Env=lab --query QueueUrl --output text)
aws sqs list-queues --profile shop-lab --queue-name-prefix shop- --query QueueUrls
Console: SQS → Create queue → Standard/FIFO → Configuration (Visibility, Receive wait, Retention) → Dead-letter queue: Enabled, chọn DLQ, Maximum receives 3 → Tags.
Bước 2: Topic + 2 subscription + filter policy. Chạy block mục 4 bước 2 (policy + subscribe cho shop-order-events), rồi lặp cho invoice với filter:
cat > inv-policy.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"sns.amazonaws.com"},
"Action":"sqs:SendMessage","Resource":"$INV_ARN","Condition":{"ArnEquals":{"aws:SourceArn":"$TOPIC_ARN"}}}]}
EOF
aws sqs set-queue-attributes --profile shop-lab --queue-url $INV_URL --attributes "{\"Policy\": $(jq -c . inv-policy.json | jq -Rs .)}"
INV_SUB=$(aws sns subscribe --profile shop-lab --topic-arn $TOPIC_ARN --protocol sqs --notification-endpoint $INV_ARN \
--attributes RawMessageDelivery=true --return-subscription-arn --query SubscriptionArn --output text)
aws sns set-subscription-attributes --profile shop-lab --subscription-arn $INV_SUB \
--attribute-name FilterPolicy --attribute-value '{"status":["PAID"]}'
aws sns list-subscriptions-by-topic --profile shop-lab --topic-arn $TOPIC_ARN --query 'Subscriptions[].{P:Protocol,E:Endpoint}'
Console: SNS → Topics → Create topic (Standard) → Create subscription → Protocol SQS → Enable raw message delivery → Subscription filter policy.
Bước 3: Publish thử bằng CLI, xem fan-out.
aws sns publish --profile shop-lab --topic-arn $TOPIC_ARN \
--message '{"orderId":"o-1001","userId":"u-1","amount":250000,"status":"PAID","placedAt":"2026-10-20T08:00:00Z"}' \
--message-attributes '{"status":{"DataType":"String","StringValue":"PAID"},"eventType":{"DataType":"String","StringValue":"OrderPlaced"}}'
aws sns publish --profile shop-lab --topic-arn $TOPIC_ARN \
--message '{"orderId":"o-1002","userId":"u-2","amount":99000,"status":"PENDING","placedAt":"2026-10-20T08:01:00Z"}' \
--message-attributes '{"status":{"DataType":"String","StringValue":"PENDING"}}'
sleep 2
for U in $Q_URL $INV_URL; do
echo "== $U"; aws sqs get-queue-attributes --profile shop-lab --queue-url $U \
--attribute-names ApproximateNumberOfMessages --query Attributes.ApproximateNumberOfMessages --output text
done
# shop-order-events: 2 (nhận cả hai)
# shop-invoice-queue: 1 (chỉ PAID)
Console: SNS → Topic → Publish message; SQS → queue → Send and receive messages → Poll for messages.
Bước 4: Consumer Spring Boot ở local. Dùng listener ở mục 4 bước 5, thêm "công tắc lỗi" để demo DLQ:
@SqsListener(value = "shop-order-events", id = "order-events")
public void onOrderPlaced(OrderPlaced evt,
@Header(SqsHeaders.SQS_APPROXIMATE_RECEIVE_COUNT) int receiveCount) {
if (processed.putIfAbsent(evt.orderId(), Boolean.TRUE) != null) {
log.info("duplicate {} skipped", evt.orderId()); return;
}
if (evt.userId().startsWith("fail-")) { // công tắc lỗi
processed.remove(evt.orderId());
throw new IllegalStateException("WMS down (simulated), receive #" + receiveCount);
}
log.info("processed {} amount={} receive #{}", evt.orderId(), evt.amount(), receiveCount);
}
@SqsListener(value = "shop-invoice-queue", id = "invoice")
public void onInvoice(OrderPlaced evt) { log.info("PDF for {}", evt.orderId()); }
AWS_PROFILE=shop-lab AWS_REGION=ap-southeast-1 ./mvnw spring-boot:run
# processed o-1001 amount=250000 receive #1
# PDF for o-1001
# processed o-1002 amount=99000 receive #1 ← invoice queue không có o-1002 (PENDING)
Bước 5: Đẩy message lỗi → DLQ sau 3 lần.
aws sns publish --profile shop-lab --topic-arn $TOPIC_ARN \
--message '{"orderId":"o-1003","userId":"fail-9","amount":120000,"status":"PAID","placedAt":"2026-10-20T08:02:00Z"}' \
--message-attributes '{"status":{"DataType":"String","StringValue":"PAID"}}'
# log app, cách nhau ~20 s (visibility):
# WMS down (simulated), receive #1
# WMS down (simulated), receive #2
# WMS down (simulated), receive #3
# → im lặng: message đã sang DLQ
aws sqs get-queue-attributes --profile shop-lab --queue-url $DLQ_URL --attribute-names ApproximateNumberOfMessages --query Attributes
Bước 6: Redrive. Sửa "công tắc" (đổi fail- thành gì khác hoặc restart app với cờ), rồi:
aws sqs start-message-move-task --profile shop-lab --source-arn $DLQ_ARN
# log app: processed o-1003 ... receive #1 (count đã reset)
Console: SQS → DLQ → Start DLQ redrive.
Bước 7: FIFO vs standard: thứ tự. Gửi 10 message theo thứ tự vào cả hai loại queue, nhận lại và so sánh:
for i in $(seq 1 10); do
aws sqs send-message --profile shop-lab --queue-url $Q_URL --message-body "{\"seq\":$i}" > /dev/null
aws sqs send-message --profile shop-lab --queue-url $FIFO_URL --message-body "{\"seq\":$i}" \
--message-group-id p-42 --message-deduplication-id "p42-$i" > /dev/null
done
# Dừng app Spring để nó không ăn message, rồi:
aws sqs receive-message --profile shop-lab --queue-url $Q_URL --max-number-of-messages 10 | jq -c '[.Messages[].Body]'
aws sqs receive-message --profile shop-lab --queue-url $FIFO_URL --max-number-of-messages 10 | jq -c '[.Messages[].Body]'
# standard: có thể [1,2,4,3,5...] và có thể chưa đủ 10 (phải gọi thêm lần nữa)
# fifo: [1,2,3,...,10] đúng thứ tự
# Dedup: gửi lại "p42-3" trong 5 phút → MD5OfMessageBody trả về nhưng không có message mới
Bước 8: EventBridge rule. Tạo queue shop-order-review (như bước 1, policy cho principal events.amazonaws.com với aws:SourceArn = ARN rule), chạy block mục 4 bước 7 với pattern amount >= 5000000, rồi:
RULE_ARN=$(aws events describe-rule --profile shop-lab --name shop-high-value-orders --event-bus-name shop-bus --query Arn --output text)
# publish 2 event: 1 nhỏ, 1 lớn
aws events put-events --profile shop-lab --entries "[
{\"EventBusName\":\"shop-bus\",\"Source\":\"shop.orders\",\"DetailType\":\"OrderPlaced\",\"Detail\":\"{\\\"orderId\\\":\\\"o-2001\\\",\\\"userId\\\":\\\"u-5\\\",\\\"amount\\\":150000,\\\"status\\\":\\\"PAID\\\"}\"},
{\"EventBusName\":\"shop-bus\",\"Source\":\"shop.orders\",\"DetailType\":\"OrderPlaced\",\"Detail\":\"{\\\"orderId\\\":\\\"o-2002\\\",\\\"userId\\\":\\\"u-6\\\",\\\"amount\\\":7500000,\\\"status\\\":\\\"PAID\\\"}\"}]" \
| jq '.FailedEntryCount' # 0
sleep 2
aws sqs receive-message --profile shop-lab --queue-url $REVIEW_URL | jq -r '.Messages[].Body | fromjson | .detail.orderId' # o-2002 (chỉ đơn lớn)
Body từ EventBridge là envelope đầy đủ (source, detail-type, time, detail), khác với SNS raw delivery; consumer Java map sang record EventEnvelope<T>(String source, String detailType, T detail).
Console: EventBridge → Event buses → Create; Rules → Create rule → Event pattern (Custom pattern JSON) → Target SQS. Tab Sandbox cho test pattern với event mẫu không cần gửi thật.
Kiểm tra kết quả
# 1. Metric SQS: NumberOfMessagesSent/Received/Deleted, ApproximateAgeOfOldestMessage
aws cloudwatch get-metric-statistics --profile shop-lab --namespace AWS/SQS --metric-name NumberOfMessagesReceived \
--dimensions Name=QueueName,Value=shop-order-events --statistics Sum --period 300 \
--start-time $(date -u -d '-1 hour' +%FT%TZ) --end-time $(date -u +%FT%TZ) --query 'Datapoints[].Sum'
# 2. SNS: NumberOfNotificationsFilteredOut (invoice queue bỏ PENDING)
aws cloudwatch get-metric-statistics --profile shop-lab --namespace AWS/SNS --metric-name NumberOfNotificationsFilteredOut \
--dimensions Name=TopicName,Value=shop-order-topic --statistics Sum --period 300 \
--start-time $(date -u -d '-1 hour' +%FT%TZ) --end-time $(date -u +%FT%TZ) --query 'Datapoints[].Sum'
# 3. EventBridge: MatchedEvents / Invocations của rule
aws cloudwatch get-metric-statistics --profile shop-lab --namespace AWS/Events --metric-name MatchedEvents \
--dimensions Name=RuleName,Value=shop-high-value-orders Name=EventBusName,Value=shop-bus --statistics Sum --period 300 \
--start-time $(date -u -d '-1 hour' +%FT%TZ) --end-time $(date -u +%FT%TZ) --query 'Datapoints[].Sum' # [1]
# 4. DLQ alarm (mục trước) chuyển sang ALARM khi có message, OK sau redrive
aws cloudwatch describe-alarms --profile shop-lab --alarm-names shop-order-events-dlq-not-empty --query 'MetricAlarms[0].StateValue'
Thử thêm
- Visibility quá ngắn: đặt
VisibilityTimeout=2, trong listenerThread.sleep(5000)trước khi return: thấy cùng message được xử lý 2–3 lần dù không lỗi. Đây là bug "chạy đôi" trong thực tế. - Manual ack + gia hạn:
acknowledgementMode = MANUAL, nhậnVisibility, gọichangeTo(120)rồi sleep 60 s: không bị nhận lại. - Short polling tốn tiền: đổi
ReceiveMessageWaitTimeSeconds=0, xem metricNumberOfEmptyReceivestăng vọt sau 5 phút. Đổi lại 20. - Archive & replay:
aws events create-archive --archive-name shop-bus-archive --event-source-arn $(aws events describe-event-bus --name shop-bus --query Arn --output text) --retention-days 1, gửi vài event, rồistart-replaykhoảng thời gian đó → event tới lại rule. SNS không có tương đương. - Scheduler:
aws scheduler create-schedule --name shop-cart-reminder --schedule-expression "rate(5 minutes)" --flexible-time-window Mode=OFF --target '{"Arn":"'$Q_ARN'","RoleArn":"arn:aws:iam::'$ACCOUNT_ID':role/shop-scheduler-role","Input":"{\"job\":\"CART_REMINDER\"}"}'(cần role cho Scheduler gửi SQS). Xóa sau khi thấy message tới. - Message lớn: gửi body 300 KB → lỗi
MessageTooLong; cách đúng là S3 + pointer (Amazon SQS Extended Client Library for Java).
Dọn tài nguyên
# 1. Alarm
aws cloudwatch delete-alarms --profile shop-lab --alarm-names shop-order-events-dlq-not-empty 2>/dev/null
# 2. EventBridge: target → rule → archive → bus (phải đúng thứ tự)
aws events remove-targets --profile shop-lab --rule shop-high-value-orders --event-bus-name shop-bus --ids review-queue
aws events delete-rule --profile shop-lab --name shop-high-value-orders --event-bus-name shop-bus
aws events delete-archive --profile shop-lab --archive-name shop-bus-archive 2>/dev/null
aws scheduler delete-schedule --profile shop-lab --name shop-cart-reminder 2>/dev/null
aws events delete-event-bus --profile shop-lab --name shop-bus
# 3. SNS: subscription rồi topic
for S in $(aws sns list-subscriptions-by-topic --profile shop-lab --topic-arn $TOPIC_ARN --query 'Subscriptions[].SubscriptionArn' --output text); do
aws sns unsubscribe --profile shop-lab --subscription-arn $S; done
aws sns delete-topic --profile shop-lab --topic-arn $TOPIC_ARN
# 4. SQS (queue chính trước, DLQ sau; tên queue bị khóa 60 s sau khi xóa)
for U in $Q_URL $INV_URL $FIFO_URL $REVIEW_URL $DLQ_URL; do aws sqs delete-queue --profile shop-lab --queue-url $U; done
aws sqs list-queues --profile shop-lab --queue-name-prefix shop- # trống (có thể trễ 60 s)
Không có gì trong lab này tốn tiền đáng kể nếu quên, trừ khi app của bạn đang short polling liên tục hoặc Scheduler còn chạy mỗi phút; vẫn dọn để tài khoản lab sạch.
Phỏng vấn và đề thi SAA hỏi về SQS, SNS và EventBridge thì sao?
Q1. Visibility timeout là gì, chọn giá trị thế nào, sai thì hậu quả gì?
🇻🇳 Tiếng Việt:
Khi consumer nhận message, SQS ẩn nó trong khoảng visibility timeout để consumer khác không nhận trùng; consumer phải xóa message trước khi hết hạn, nếu không nó hiện lại và được xử lý lại. Em đặt lớn hơn thời gian xử lý tối đa, ví dụ p99 cộng biên, và với job dài thì gia hạn bằng ChangeMessageVisibility. Quá ngắn thì cùng một message bị xử lý hai lần dù không lỗi; quá dài thì message lỗi chờ lâu mới được retry.
🇬🇧 English:
When a consumer receives a message, SQS hides it for the visibility timeout so other consumers do not get it twice; the consumer must delete it before the timeout expires, otherwise it reappears and is processed again. I set it above the maximum processing time, roughly p99 plus a margin, and for long jobs I extend it with ChangeMessageVisibility. Too short and the same message is processed twice even without errors; too long and a failed message waits a long time before retry.
Q2. Standard và FIFO khác nhau thế nào, bạn xử lý message trùng ra sao?
🇻🇳 Tiếng Việt:
Standard cho throughput gần vô hạn, at-least-once, thứ tự best-effort. FIFO giữ thứ tự trong message group và chống trùng khi gửi theo deduplication ID trong 5 phút, nhưng giới hạn 300 message mỗi giây mỗi queue nếu không batch. Dù dùng loại nào, retry sau lỗi vẫn giao lại message, nên consumer phải idempotent: em kiểm tra khóa nghiệp vụ như orderId trong bảng inbox trước mỗi side effect, không dựa vào messageId vì fan-out và redrive sinh ID mới.
🇬🇧 English:
Standard queues give nearly unlimited throughput, at-least-once delivery and best-effort ordering. FIFO queues preserve order within a message group and deduplicate sends by deduplication ID for five minutes, but are limited to 300 messages per second per queue without batching. Either way, a retry after failure redelivers the message, so consumers must be idempotent: I check a business key such as orderId in an inbox table before each side effect, not the messageId, because fan-out and redrive create new IDs.
Q3. Khi nào dùng SNS, khi nào EventBridge?
🇻🇳 Tiếng Việt:
SNS cho fan-out đơn giản với throughput và độ trễ tốt nhất, và để gửi email, SMS, push cho người; filter policy chỉ ở mức attribute. EventBridge khi cần lọc theo nội dung phức tạp, nhận sự kiện từ dịch vụ AWS và SaaS, gọi API ngoài có xác thực, lên lịch, và archive để replay. Trong shop-app em dùng SNS cho sự kiện nội bộ volume cao đẩy vào SQS, và EventBridge cho rule nghiệp vụ như đơn giá trị lớn cần duyệt và cho tích hợp S3, Scheduler.
🇬🇧 English:
SNS for simple fan-out with the best throughput and latency, and to notify people by email, SMS or push; its filter policies work mostly on attributes. EventBridge when we need rich content-based filtering, events from AWS services and SaaS partners, authenticated calls to external APIs, scheduling, and archive with replay. In shop-app I use SNS for high-volume internal events fanned out to SQS, and EventBridge for business rules such as high-value orders needing review, plus S3 and Scheduler integrations.
Q4. DLQ dùng thế nào cho đúng?
🇻🇳 Tiếng Việt:
Gắn redrive policy trên queue với maxReceiveCount từ ba đến mười, DLQ có retention dài hơn queue chính, và alarm trên số message trong DLQ. Khi có message, em xem nguyên nhân: lỗi hạ tầng tạm thì dùng tính năng redrive về source queue, message độc thì sửa code hoặc loại có ghi chú. Không để consumer tự đọc DLQ retry vô tận, vì DLQ là nơi dừng lại để con người xem.
🇬🇧 English:
Attach a redrive policy on the queue with maxReceiveCount between three and ten, give the DLQ a longer retention than the source queue, and alarm on the number of messages in the DLQ. When messages arrive I look at the cause: for transient infrastructure failures I use the built-in redrive to the source queue; for poison messages I fix the code or discard them with a note. I never let the consumer poll the DLQ and retry endlessly, because the DLQ is where processing stops for a human to look.
Góc thi SAA-C03
Câu 1: Một ứng dụng xử lý đơn hàng nhận spike lớn vào giờ cao điểm. Backend xử lý mất vài giây mỗi đơn và đôi khi bị lỗi tạm thời. Công ty cần tách frontend khỏi backend, không mất đơn, và tự động cô lập đơn lỗi liên tục để điều tra. Giải pháp nào LEAST operational overhead?
A. Amazon Kinesis Data Streams với consumer trên EC2.
B. Amazon SQS standard queue với dead-letter queue và redrive policy; backend trong Auto Scaling group poll queue.
C. Amazon SNS topic với HTTP subscription tới backend.
D. Amazon MQ với RabbitMQ Multi-AZ broker.
Đáp án: B. "Decouple", "buffer spike", "no message loss", "isolate failing messages" → SQS + DLQ. A Kinesis dành cho streaming/analytics, consumer phải quản shard và checkpoint. C SNS không lưu message; backend down là mất. D Amazon MQ là broker cần quản instance, chỉ hợp khi app đã dùng AMQP/JMS.
Câu 2: Một hệ thống ngân hàng gửi giao dịch của từng tài khoản vào queue. Giao dịch của cùng một tài khoản phải được xử lý đúng thứ tự và không được xử lý hai lần. Throughput toàn hệ thống hàng nghìn giao dịch mỗi giây. Cấu hình nào đáp ứng?
A. SQS standard queue, consumer sắp xếp lại theo timestamp.
B. SQS FIFO queue, MessageGroupId là accountId, MessageDeduplicationId là transactionId.
C. SQS FIFO queue, MessageGroupId cố định là "transactions".
D. SNS FIFO topic gửi tới Lambda.
Đáp án: B. "Order per account", "exactly once" → FIFO với group theo accountId (nhiều group → song song, đạt hàng nghìn/s với batch hoặc high-throughput mode) và dedup theo transactionId. A standard không bảo đảm thứ tự, sắp lại ở client phức tạp và vẫn trùng. C một group duy nhất → mọi giao dịch nối tiếp, 300 msg/s, không đạt throughput. D SNS FIFO chỉ fan-out sang SQS FIFO, không tới Lambda trực tiếp, và không giải quyết xử lý.
Câu 3: Một công ty muốn tự động chạy một tác vụ mỗi khi có EC2 instance chuyển sang trạng thái stopped trong tài khoản, và đồng thời chuyển các sự kiện từ ứng dụng SaaS giám sát của bên thứ ba vào cùng pipeline xử lý. Dịch vụ nào phù hợp nhất?
A. Amazon SNS với subscription Lambda.
B. Amazon SQS với Lambda trigger.
C. Amazon EventBridge với rule lọc sự kiện EC2 và partner event source cho SaaS.
D. AWS Config rule với remediation.
Đáp án: C. "React to AWS service events" + "SaaS integration" là hai từ khóa đặc trưng của EventBridge (default bus nhận EC2 state change, partner bus nhận SaaS). A, B không nhận sự kiện EC2 hay SaaS trực tiếp. D Config theo dõi tuân thủ cấu hình, không phải pipeline sự kiện tổng quát.
Cách nhớ ngắn gọn
- SQS = queue, lưu tới 14 ngày, pull, 1 consumer group; SNS = push 1→N, không lưu; EventBridge = bus + rule lọc nội dung + nguồn AWS/SaaS + schedule + replay.
- Standard: nhanh, at-least-once, lộn thứ tự. FIFO: thứ tự theo group, dedup 5 phút, 300 msg/s. Cả hai đều cần consumer idempotent.
- Visibility timeout ≥ thời gian xử lý; long polling 20 s; DLQ với maxReceiveCount 3–10, retention dài hơn, alarm > 0, redrive có chủ đích.
- SNS → SQS luôn tốt hơn SNS → HTTP vào app. Message > 256 KB → S3 + pointer.
- Đề nói "existing JMS/AMQP" → Amazon MQ; "Kafka" → MSK; "real-time analytics/replay" → Kinesis; còn lại → SQS/SNS/EventBridge.
| Từ khóa trong đề | Nghĩ tới |
|---|---|
| decouple, buffer, absorb spikes, retry, no data loss | SQS standard |
| strict ordering, exactly-once, no duplicates | SQS FIFO (group + dedup ID) |
| messages processed twice, processing takes long | Tăng visibility timeout |
| isolate failed messages for analysis | DLQ + redrive policy |
| reduce empty responses / cost of polling | Long polling (WaitTimeSeconds 20) |
| fan-out one message to many queues; notify users by SMS/email | SNS |
| subscriber receives only some messages | SNS filter policy |
| react to AWS service events, SaaS events, content-based routing, cron, replay | EventBridge |
| messages larger than 256 KB | S3 + SQS Extended Client |
| existing app uses JMS, AMQP, MQTT, minimal code change | Amazon MQ |
| delay processing up to 15 minutes | SQS delay queue / message timer |
Tự kiểm tra
Vì sao DLQ nên có retention dài hơn queue chính?
Message giữ tuổi từ lúc gửi ban đầu khi chuyển sang DLQ; nếu DLQ retention ngắn, message có thể hết hạn trước khi ai kịp xem và mất bằng chứng.
Chuyển sang FIFO có hết "xử lý hai lần" do consumer lỗi không?
Không. FIFO chỉ chống trùng lúc gửi (dedup 5 phút). Message đã nhận mà không xóa sẽ hiện lại sau visibility timeout ở cả standard và FIFO. Cần consumer idempotent.
SNS subscribe queue xong mà message không tới, kiểm tra gì đầu tiên?
Access policy của queue phải cho sns.amazonaws.com hành động sqs:SendMessage với điều kiện aws:SourceArn = ARN topic. Sau đó kiểm tra filter policy có loại message không.
Khi nào chọn EventBridge thay SNS?
Khi cần lọc theo nội dung phức tạp, nhận sự kiện dịch vụ AWS hoặc SaaS, API destination có xác thực, lịch (Scheduler), archive/replay, schema registry. SNS khi cần throughput/độ trễ tốt nhất và gửi email/SMS cho người.
Trong Spring Cloud AWS 3, message bị xóa khỏi queue khi nào với @SqsListener mặc định?
Ack mode mặc định ON_SUCCESS: method return bình thường thì xóa; ném exception thì không xóa, message hiện lại sau visibility timeout và được retry, quá maxReceiveCount thì vào DLQ.
🧪 Thực hành bài này: Intermediate: Ứng dụng HA: ALB + ASG + RDS + S3 + SQS (chưa triển khai) · lab labs/aws