SQS standard/FIFO, visibility timeout, DLQ; SNS fan-out; EventBridge; so với Kafka/RabbitMQ

6 câu hỏi~50 phútTừ khóa: SQS, standard, FIFO, visibility timeout, long polling, DLQ, redrive, SNS, fan-out, filter policy, EventBridge, event pattern, Scheduler, Spring Cloud AWS, @SqsListener, idempotency, SAA: Domain 2 Resilient

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 đề

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ĩaTrên shop-app
Standard queueThroughput 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 queueThứ 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 .fifoshop-stock-updates.fifo, group = productId
Visibility timeoutSau 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ác60 s cho email, 5 phút cho PDF
Long pollingWaitTimeSeconds 1–20 s: chờ có message mới trả về; giảm request rỗng, giảm tiềnLuôn 20 s
Message retention1 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ạishop-order-events-dlq, maxReceiveCount 5
Delay queue / message timerTrì 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 sizeTối đa 256 KB (1 MB với extended); lớn hơn → S3 + Extended Client LibraryChỉ gửi orderId, không gửi PDF
SNS topicStandard (fan-out, at-least-once) hoặc FIFO (chỉ sang SQS FIFO)shop-order-topic
SubscriptionSQS, Lambda, HTTP/S, email, SMS, mobile push, Kinesis Firehose2 queue + 1 Lambda
Filter policySubscriber chỉ nhận message có attribute (hoặc body) khớp JSON policyQueue PDF chỉ nhận status = PAID
Event busDefault bus (sự kiện AWS), custom bus (shop-bus), partner bus (SaaS)shop-bus
Rule + event patternLọc theo source, detail-type, nội dung detail (prefix, numeric, exists, anything-but)Đơn > 5 triệu → queue duyệt tay
Target20 target/rule: SQS, SNS, Lambda, Step Functions, API destination (HTTP ngoài), Kinesis, ECS task…SQS + Lambda
Schema registry, archive & replay, Scheduler, PipesTính năng riêng EventBridgeReplay 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 StandardSQS FIFO
Thứ tựBest-effortĐúng trong message group
TrùngAt-least-once (có thể trùng)Exactly-once processing (dedup 5 phút theo MessageDeduplicationId hoặc content hash)
ThroughputGần vô hạn300 msg/s (3.000 batch), high-throughput mode: 70.000 msg/s theo partition
Song songTự doMộ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-appEmail, PDF, WMSCập nhật tồn kho theo productId; sự kiện tài khoản theo userId
SQSSNSEventBridge
Mô hìnhQueue, pull, 1 consumer groupPub/sub, push, N subscriberEvent bus, rule lọc nội dung, N target
Lưu messageTới 14 ngàyKhông (retry giao, rồi DLQ)Không, nhưng có archive & replay
LọcKhôngFilter policy theo attribute/body (đơn giản)Pattern phong phú (numeric, prefix, exists, anything-but, $or)
NguồnAppApp, CloudWatch alarmApp, 200+ dịch vụ AWS, SaaS partner, Scheduler
Target đặc biệtEmail, SMS, mobile push, HTTPStep Functions, API destination (HTTP có auth), ECS task, 20 target/rule
Throughput / độ trễRất cao / msRất cao / msThấp hơn (giới hạn PutEvents theo region) / ~nửa giây
SchemaKhôngKhôngSchema 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/EventBridgeAmazon MQ (RabbitMQ/ActiveMQ)MSK (Kafka)Kinesis Data Streams
Bản chấtServerless, API HTTPSBroker managed, giao thức AMQP/MQTT/JMSKafka managed (cluster hoặc serverless)Streaming serverless của AWS
Khi nàoApp mới trên AWS, cần đơn giản, scale tự độngMigrate 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ủngAnalytics real-time, nối Firehose/Analytics, shard
ReplayEventBridge archive; SQS khôngKhông (queue)Có, theo retentionCó, 24 h – 365 ngày
Vận hànhGần 0Broker instance, Multi-AZCluster, 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

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ỗiHậu quảSửa
DLQ retention ngắn hơn queue chínhMessage 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 = 1Mọi lỗi mạng thoáng qua đều vào DLQ, alarm liên tục3–10 tùy việc; kết hợp backoff
Không có alarmHàng nghìn đơn mất email, phát hiện sau 2 tuầnAlarm > 0 như trên
Consumer tự poll DLQ để retryVòng lặp vô tận, không ai biếtRedrive có chủ đích
Lambda consumer: DLQ gắn vào Lambda thay vì queueVới trigger SQS, Lambda không gửi vào DLQ của function; message quay về queueRedrive policy trên queue; Lambda destination chỉ cho async invoke
FIFO + DLQMột message hỏng chặn cả group tới khi đủ receive countmaxReceiveCount 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íSNSEventBridgeChọn cho shop-app
Fan-out tới SQS/LambdaCó, hàng triệu msg/sCó, giới hạn PutEvents (hàng nghìn/s theo region, nâng được)Flash sale → SNS
Độ trễ~ms~0.5 sEmail/WMS không cần ms; cả hai đều được
LọcFilter policy trên attribute (và body từ 2022), toán tử cơ bảnPattern 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 AWSChỉ 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
SaaSKhôngPartner bus: Stripe, Zendesk, Shopify, Datadog…Webhook thanh toán → EventBridge
Gửi ra HTTP ngoàiHTTP/S subscription (xác nhận, ít kiểm soát)API destination: OAuth/API key, rate limit, retryGửi tới WMS của đối tác → EventBridge
ReplayKhôngArchive + replayFix bug consumer rồi phát lại 2 giờ event → EventBridge
Schedule (cron)KhôngEventBridge Scheduler (one-time, rate, cron, timezone)Nhắc giỏ bỏ quên, báo cáo 8:00 → Scheduler
Gửi cho ngườiEmail, SMS, pushKhông trực tiếp (qua SNS target)Alarm → SNS
Schema / contractKhôngSchema registry, discovery, code binding JavaNhiều team cùng đọc event → EventBridge
GiáRất rẻ theo triệu request; tới SQS/Lambda miễn phí phần giaoTheo 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

Chi phí & thời gian

Chuẩn bị

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

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

Từ khóa trong đềNghĩ tới
decouple, buffer, absorb spikes, retry, no data lossSQS standard
strict ordering, exactly-once, no duplicatesSQS FIFO (group + dedup ID)
messages processed twice, processing takes longTăng visibility timeout
isolate failed messages for analysisDLQ + redrive policy
reduce empty responses / cost of pollingLong polling (WaitTimeSeconds 20)
fan-out one message to many queues; notify users by SMS/emailSNS
subscriber receives only some messagesSNS filter policy
react to AWS service events, SaaS events, content-based routing, cron, replayEventBridge
messages larger than 256 KBS3 + SQS Extended Client
existing app uses JMS, AMQP, MQTT, minimal code changeAmazon MQ
delay processing up to 15 minutesSQS 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