Danh sách bài viết

Bài 52: Checklist & Anti-patterns Coordination — Incident Double Charge

Bài tổng kết Module 4 Distributed Coordination. Điểm khởi đầu là một incident thực tế: lock expire giữa chừng khi payment gateway chậm, hai worker cùng charge $500 cho một đơn hàng. Phân tích root cause, chuỗi sửa theo layer, sau đó hệ thống hóa top 10 anti-patterns hay gặp trong coordination, decision tree chọn primitive phù hợp cho từng bài toán, và checklist production-ready trước khi đưa coordination lên production.

28/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

  • Phân tích incident double charge do lock expire giữa chừng và chỉ ra chính xác điểm sai.
  • Nắm top 10 anti-pattern hay gặp trong coordination với Redis, liên kết với từng bài đã học.
  • Dùng được decision tree để chọn primitive phù hợp (INCR, lock, idempotency key, semaphore, leader election, singleton worker).
  • Có checklist production-ready kiểm tra trước khi deploy coordination lên production.
  • Hiểu giới hạn cốt lõi: lock TTL không đảm bảo mutual exclusion tuyệt đối khi có process pause.
2

Incident: Lock Expire Giữa Chừng → Double Charge

Bối cảnh

Payment service xử lý đơn hàng. Mỗi đơn được bảo vệ bằng Redis lock để chống double charge:

def process_payment(order_id: str, amount: int):
    lock_key = f"lock:order:{order_id}"
    token = str(uuid.uuid4())

    # Acquire lock — TTL 30 giây
    acquired = redis.set(lock_key, token, nx=True, ex=30)
    if not acquired:
        return {"status": "already_processing"}

    try:
        # Gọi payment gateway external
        result = payment_gateway.charge(order_id, amount)
        # Ghi kết quả vào DB
        db.save_charge(order_id, result)
        return {"status": "success", "charge_id": result.charge_id}
    finally:
        # Release lock
        release_lock(redis, lock_key, token)

TTL = 30s. Trong điều kiện bình thường, payment gateway trả về trong 5s. Mọi thứ hoạt động ổn hàng tháng.

Kịch bản sự cố

Một đơn hàng $500. Gateway external gặp vấn đề nội bộ và bắt đầu retry timeout — tổng thời gian xử lý 40s.

T=0s   : Worker A acquire lock:order:X (TTL=30s), gọi gateway.charge()
T=30s  : Lock EXPIRE — Worker A vẫn đang chờ gateway trả lời.
T=31s  : Client timeout (frontend retry) → request mới vào hàng đợi.
          Worker B acquire lock:order:X (thấy trống) → gọi gateway.charge() lần 2.
T=40s  : Gateway A trả về success → charge $500. Worker A ghi DB.
T=42s  : Gateway B trả về success → charge $500 LẦN 2. Worker B ghi DB.

Kết quả: khách bị charge 2 × $500 = $1,000 cho 1 đơn $500.

Hậu quả: refund thủ công, complaint trên mạng xã hội, phí tranh chấp thẻ tín dụng (chargeback fee). Không phải lỗi tính năng — là lỗi correctness trong distributed coordination.

3

Root Cause Phân Tích

Sự cố có nhiều lớp nguyên nhân:

Lớp 1 — TTL < thời gian xử lý thực tế

TTL 30s được chọn dựa trên p99 của gateway là 5s và buffer × 6. Nhưng khi gateway gặp vấn đề retry nội bộ, thời gian thực tế là 40s. TTL không bao giờ đảm bảo job hoàn thành trước khi expire — đây là thiết kế có lỗ hổng với external dependency có thể biến động.

Lớp 2 — Chỉ dựa lock, không có idempotency ở gateway

Nếu payment gateway có idempotency key, hai lần gọi với cùng order_id sẽ trả về cùng kết quả mà không charge lần 2. Nhiều gateway hiện đại (Stripe, Adyen) hỗ trợ điều này. Code trên không truyền idempotency key.

Lớp 3 — Không có fencing token ở DB layer

Khi Worker B gọi db.save_charge(order_id, result), DB không kiểm tra xem đã có charge record cho order_id chưa. Không có unique constraint trên (order_id) trong bảng charges.

Lớp 4 — Correctness lock được implement như efficiency lock

Bài 44 đã phân biệt hai loại lock:

  • Efficiency lock: nếu 2 worker cùng chạy thỉnh thoảng, kết quả vẫn đúng (chỉ tốn tài nguyên thêm). Ví dụ: cache recomputation.
  • Correctness lock: nếu 2 worker cùng chạy, kết quả SAI. Ví dụ: payment charge.

Payment là correctness lock nhưng code trên implement như efficiency lock — không có lớp bảo vệ thứ hai khi lock fail. Bài 46 đã nêu: correctness lock phải có fencing token + resource check, không thể chỉ dựa TTL.

4

Fix Theo Layer

Không có single fix. Mỗi layer bổ sung một lớp bảo vệ độc lập.

Layer 1 — Idempotency key gửi tới gateway

Gateway hiện đại nhận idempotency_key; nếu nhận cùng key lần 2 trong một khoảng thời gian, trả lại kết quả lần 1 mà không charge thêm. Stripe gọi là Idempotency-Key header; Adyen dùng reference.

result = payment_gateway.charge(
    order_id=order_id,
    amount=amount,
    idempotency_key=order_id  # gateway dedup theo key này
)

Điều kiện: gateway phải support idempotency và lưu state đủ lâu (ít nhất bằng retry window của client). Kiểm tra tài liệu gateway cụ thể.

Layer 2 — DB unique constraint trên order_id

Đây là arbiter cuối — ngay cả khi cả lock lẫn idempotency key đều thất bại, DB không cho phép ghi 2 charge record cho cùng 1 order:

-- Migration
ALTER TABLE charges ADD CONSTRAINT uq_charges_order_id UNIQUE (order_id);

-- Hoặc check trước khi INSERT
INSERT INTO charges (order_id, amount, charge_id, created_at)
VALUES (%s, %s, %s, NOW())
ON CONFLICT (order_id) DO NOTHING
RETURNING charge_id;

Nếu Worker B cố ghi, INSERT không có effect. Worker B đọc lại charge record hiện có và trả về kết quả đó.

Layer 3 — Fencing token + conditional update

Tạo fencing token khi acquire lock (bài 46). DB chỉ chấp nhận ghi nếu token mang số cao hơn token đã ghi:

# Acquire lock + lấy fencing token (monotonic counter)
token = redis.incr("fencing:order:counter")
acquired = redis.set(lock_key, token, nx=True, ex=60)

# Ghi DB với điều kiện fencing
db.save_charge_if_token_newer(order_id, token, result)

Worker B có token cao hơn Worker A (vì acquire sau) — nếu Worker A đã ghi với token thấp, Worker B vẫn ghi được. Để ngăn điều này, DB constraint ở Layer 2 mới là arbiter thực sự.

Layer 4 — TTL phù hợp + watchdog renewal

Nếu không thể dùng idempotency key (gateway cũ không hỗ trợ), ít nhất phải đảm bảo TTL lớn hơn worst-case job time:

MAX_GATEWAY_TIMEOUT_S = 120  # timeout cứng cho gateway
TTL = MAX_GATEWAY_TIMEOUT_S + 30  # buffer thêm 30s

acquired = redis.set(lock_key, token, nx=True, ex=TTL)

Watchdog renewal (bài 44 — 45) giữ lock sống nếu job chạy lâu hơn dự kiến. Tuy nhiên, watchdog chỉ giải quyết TTL < job time; không giải quyết correctness khi Redis bị partition.

Thứ tự ưu tiên

Bắt buộc  : DB unique constraint (layer 2) — không bao giờ bỏ qua cho correctness.
Strongly recommended : Idempotency key (layer 1) — nếu gateway support.
Nên có    : TTL phù hợp (layer 4).
Optional  : Fencing token (layer 3) nếu cần audit trail hoặc conflict detection.
5

Bài Học Từ Incident

  • Lock là defense layer 1, không phải layer duy nhất. Với correctness-critical operation, mỗi layer bảo vệ phải hoạt động độc lập với các layer còn lại.
  • TTL không bao giờ đảm bảo job hoàn thành trước khi expire khi có external dependency biến động. Đây là giới hạn thiết kế của lock TTL, không phải lỗi cài đặt.
  • Phân biệt efficiency vs correctness lock ngay từ đầu. Payment, ledger, inventory deduction, seat allocation là correctness — cần nhiều lớp hơn.
  • Test với external dependency chậm. Chaos test: inject delay 50s vào gateway call và kiểm tra hệ thống có charge 2 lần không.
  • TTL được chọn từ p99, không phải p100. Nếu có tail latency ở external dependency, p99 là điểm mù. Dùng p99.9 hoặc timeout cứng cộng buffer lớn hơn.
6

Top 10 Coordination Anti-patterns

  1. SETNX + EXPIRE riêng hai bước (bài 43)

    Hai lệnh không atomic. Client crash sau SETNX nhưng trước EXPIRE → lock vĩnh viễn không có TTL → deadlock.

    Fix: SET key value NX EX seconds — acquire và set TTL trong một lệnh atomic.

  2. Release dùng DEL không check owner (bài 45)

    Worker A acquire lock, bị pause dài hơn TTL → lock expire → Worker B acquire. Worker A tỉnh dậy, gọi DEL → xóa lock của Worker B → Worker C acquire → 3 worker cùng chạy.

    Fix: Lua script check value == token trước khi DEL.

  3. Correctness lock chỉ dựa TTL (bài 44, 46 — và incident này)

    GC pause, VM live migration, mạng chậm có thể làm process "đóng băng" lâu hơn TTL. Hai worker cùng hold lock trong khoảng pause.

    Fix: Fencing token + resource-layer check, hoặc idempotency key + DB constraint.

  4. Dùng lock cho thao tác đã atomic

    Tăng counter, append item vào list, set conditional — tất cả đều có lệnh Redis atomic tương ứng. Wrap chúng trong lock là over-engineer không cần thiết.

    Fix: INCR, LPUSH, SET NX, ZADD NX, hoặc Lua script thay lock.

  5. Lock không có TTL

    Client crash sau acquire → lock còn mãi → mọi worker sau đó bị block vĩnh viễn.

    Fix: Luôn set TTL khi acquire. Không có exception.

  6. TTL nhỏ hơn worst-case job time (incident này)

    Chọn TTL từ p99 latency bình thường, bỏ qua tail latency hoặc external dependency chậm → lock expire giữa chừng.

    Fix: TTL = worst-case timeout (không phải p99) + buffer. Dùng watchdog renewal nếu job time variable.

  7. Redlock cho correctness mà không có fencing (bài 47)

    Redlock giảm xác suất sai với single Redis node, nhưng vẫn không đảm bảo mutual exclusion tuyệt đối khi có clock drift hoặc process pause. Martin Kleppmann (2016) đã phân tích chi tiết.

    Fix: Redlock chỉ phù hợp efficiency lock. Correctness lock cần fencing + resource check hoặc consensus system (Zookeeper, etcd).

  8. Leader election task không idempotent (bài 49)

    Leader chạy task (gửi email, charge). Vì split brain (2 leader do pause > TTL), task chạy 2 lần. Task không idempotent → duplicate side-effect.

    Fix: Task của leader phải idempotent (kiểm tra đã làm chưa trước khi làm). Kết hợp với lock check và watchdog.

  9. Distributed semaphore dùng INCR/DECR (bài 50)

    Worker increment counter khi vào, decrement khi ra. Worker crash trước khi decrement → counter không bao giờ giảm → semaphore bị "leak" → dần dần không worker nào vào được.

    Fix: Sorted Set với timestamp làm score, cleanup expired member định kỳ.

  10. Cron job không dedup trên multi-instance (bài 51)

    3 instance cùng có cron schedule → job chạy 3 lần mỗi fire. Không có cơ chế singleton.

    Fix: Singleton worker pattern — acquire lock per cron fire với TTL = fire interval. Chỉ 1 instance acquire được mỗi lần.

7

Decision Tree — Chọn Coordination Primitive

Câu hỏi đầu tiên: bài toán thuộc loại nào?

Cần coordination gì?
│
├── Atomic operation đơn (counter, conditional set, append)?
│   └── → INCR / SET NX / Lua atomic script
│       (KHÔNG cần lock — Redis đã atomic single-command)
│
├── Request retry-safe (payment, webhook dedup, form submit)?
│   └── → Idempotency key (bài 48)
│       Store: Redis SET NX EX hoặc DB unique constraint
│
├── Mutual exclusion — chỉ 1 worker tại 1 thời điểm?
│   ├── Efficiency lock (nếu 2 worker thỉnh thoảng chạy cùng = OK)?
│   │   ├── Single Redis node đủ → SET NX EX + Lua release (bài 43–45)
│   │   └── Multi-node fault tolerance → Redlock (bài 47, hiếm dùng)
│   └── Correctness lock (nếu 2 worker chạy cùng = sai data)?
│       → Fencing token + DB conditional write (bài 46)
│         HOẶC idempotency key + DB unique constraint
│         HOẶC consensus system: Zookeeper / etcd
│         (KHÔNG chỉ dựa Redis TTL lock)
│
├── Giới hạn N worker đồng thời (rate limit resource)?
│   └── → Distributed semaphore — ZSet + timeout (bài 50)
│
├── Chọn 1 instance làm leader, failover tự động?
│   └── → Leader election — lock + renewal (bài 49)
│       Lưu ý: task của leader phải idempotent
│
├── Cron job chạy đúng 1 lần per fire, multi-instance?
│   └── → Singleton worker — lock per fire với TTL = interval (bài 51)
│
└── Multi-node fault tolerance cho efficiency lock (hiếm)?
    └── → Redlock (bài 47)
        Chỉ dùng khi single Redis node không đủ fault tolerant
        và correctness được đảm bảo ở resource layer

Nguyên tắc tổng quát: bắt đầu từ lựa chọn đơn giản nhất (atomic Redis command), leo lên khi bài toán thực sự cần phức tạp hơn. Over-engineering coordination là anti-pattern phổ biến hơn under-engineering.

Bảng tham chiếu nhanh

Bài toán Primitive Bài tham chiếu
Counter, set conditional INCR / SET NX / Lua Bài 43
Request dedup, payment Idempotency key Bài 48
Mutual exclusion (efficiency) Single Redis lock Bài 43–45
Mutual exclusion (correctness) Fencing + DB constraint Bài 46
N concurrent limit Distributed semaphore Bài 50
1 instance chạy liên tục Leader election Bài 49
Cron chạy 1 lần per fire Singleton worker Bài 51
Multi-node efficiency lock Redlock Bài 47
8

Checklist Production-Ready Coordination

Lock cơ bản

  • SET NX EX — acquire và TTL trong một lệnh atomic.
  • Unique token (UUID v4) per acquire — không tái sử dụng.
  • Release dùng Lua check-and-delete (kiểm tra ownership trước DEL).
  • TTL lớn hơn worst-case job time, không phải p99.
  • Watchdog renewal nếu job time biến động lớn.

Correctness-critical operation

  • Có ít nhất một trong ba lớp sau:
  • Idempotency key gửi tới external service (payment, webhook).
  • DB unique constraint trên operation identifier (order_id, request_id).
  • Fencing token + conditional write ở resource layer.
  • Không chỉ dựa Redis TTL lock cho correctness.
  • Đã test chaos: inject delay lớn hơn TTL vào external dependency và kiểm tra kết quả.

Tránh over-engineering

  • Nếu operation đã atomic (INCR, SET NX) — không thêm lock.
  • Nếu DB transaction đủ — không cần distributed lock.
  • Nếu idempotency key đủ cho dedup — không cần lock chống concurrent.

Operational monitoring

  • Alert khi lock expire trước khi được release (metric: lock_expired_before_release).
  • Alert khi leadership flapping (leader thay đổi quá nhiều lần trong cửa sổ ngắn).
  • Alert khi semaphore holder count không giảm theo thời gian (leak).
  • Alert khi cùng 1 operation (cron fire, payment) được thực hiện nhiều hơn 1 lần (duplicate execution).
  • Dashboard theo dõi: lock acquire rate, lock contention (fail acquire / total attempts), hold time distribution.

Trước khi deploy

  • Code review xác nhận: loại lock (efficiency hay correctness) được ghi chú rõ.
  • Runbook: khi lock bị stuck (client crash, không release), operator unlock thế nào.
  • Load test với concurrency cao để kiểm tra contention behavior.
9

Process Pause — Sự Thật Phải Chấp Nhận

Bất kỳ process nào trong môi trường thực tế đều có thể bị "đóng băng" không báo trước:

  • GC stop-the-world: JVM, Go GC có thể pause hàng trăm millisecond đến vài giây.
  • VM live migration: hypervisor suspend VM rồi resume trên host khác, process không biết.
  • OS swap: process bị swap ra disk khi memory pressure cao.
  • Network partition: process không thể renew lock vì mất kết nối Redis.

Trong thời gian pause, lock TTL vẫn đếm ngược. Khi process tỉnh dậy, lock có thể đã expire và được acquire bởi process khác. Process không biết điều này cho đến khi nó tự kiểm tra.

Hệ quả thiết kế

Mọi lock dựa TTL không thể đảm bảo mutual exclusion tuyệt đối khi có process pause dài hơn TTL. Đây không phải bug của Redis — đây là giới hạn căn bản của bất kỳ TTL-based lock nào, kể cả Redlock.

Hệ quả: resource layer phải là arbiter cuối cùng. DB constraint, fencing, idempotency không phải "best practice" tùy chọn — chúng là yêu cầu bắt buộc cho correctness khi có process pause.

Lock TTL  → giảm xác suất conflict + coordination signal
Resource  → đảm bảo correctness (DB constraint, fencing, idempotency)

Chỉ dùng lock TTL → đủ cho efficiency.
Cần correctness  → lock TTL + resource layer.

Martin Kleppmann trong "Designing Data-Intensive Applications" (2017, chương 8–9) và bài blog "How to do distributed locking" (2016) đã phân tích chi tiết vấn đề này, đặc biệt trong ngữ cảnh Redlock.

10

Module 4 — Tổng Kết Khái Niệm

Module 4 xây dựng từ nền tảng lên đến các primitive phức tạp:

Bài Chủ đề Khái niệm cốt lõi
42 Vì sao cần distributed lock Race condition trong distributed system; efficiency vs correctness lock
43 SET NX — lock cơ bản Atomic acquire + TTL; SETNX + EXPIRE riêng là anti-pattern
44 Lock expiration TTL là lưới an toàn; watchdog renewal; TTL < job time là lỗ hổng
45 Unlock an toàn Ownership check + Lua atomic delete; DEL không check owner là anti-pattern
46 Fencing token Monotonic counter; resource-layer check; correctness không thể chỉ dựa TTL
47 Redlock debate Multi-node lock; giới hạn với clock drift và process pause; efficiency only
48 Idempotency key Request dedup; store-and-check pattern; payment dedup
49 Leader election Acquire = leader; renewal giữ leadership; split brain; consensus alternative
50 Distributed semaphore N concurrent limit; ZSet + timeout; INCR/DECR leak anti-pattern
51 Singleton worker Cron dedup multi-instance; lock per fire với TTL = interval
52 Anti-patterns + checklist Double charge incident; decision tree; production checklist

Thread xuyên suốt module

Bài 42–43 đặt nền: tại sao lock, cách acquire đúng. Bài 44–46 mở rộng: điều gì xảy ra khi lock expire hoặc release sai, và correctness lock cần gì hơn. Bài 47 kiểm tra giới hạn của Redis lock với multi-node. Bài 48 giới thiệu một lớp bảo vệ khác không phải lock: idempotency. Bài 49–51 áp dụng các primitive vào bài toán cụ thể (leader election, semaphore, singleton cron). Bài 52 tổng hợp: incident, anti-pattern, decision tree, checklist.

11

Self-Assessment Trước Module 5

Trả lời được các câu hỏi sau mà không cần tra lại tài liệu:

  • Phân biệt efficiency lock và correctness lock. Cho ví dụ cụ thể của mỗi loại.
  • Giải thích tại sao lock TTL không đủ cho correctness khi có process pause.
  • Mô tả fencing token hoạt động thế nào và tại sao nó giải quyết được vấn đề lock TTL.
  • Mô tả idempotency key pattern và khi nào nó là lựa chọn tốt hơn lock.
  • Cho một bài toán mới, chọn được primitive phù hợp (dùng decision tree ở bài 52).
  • Biết ít nhất 3 trường hợp nên tránh lock (dùng atomic command, DB transaction, idempotency đủ).
  • Nêu được ít nhất 2 chỉ số cần monitor cho distributed lock trong production.

Bài tập thực hành

  1. Viết lại process_payment trong incident bài này với đầy đủ 3 layer bảo vệ: idempotency key, DB unique constraint, và TTL phù hợp.
  2. Với một cron job gửi email digest hàng ngày trên 4 instance: thiết kế singleton worker. Xác định TTL, lock key pattern, và behavior khi 1 instance crash.
  3. Review một distributed lock trong codebase hiện tại (hoặc codebase công khai). Xác định loại lock (efficiency/correctness) và kiểm tra nó có đủ lớp bảo vệ phù hợp không.
12

Quiz & Đáp Án Gợi Ý

Quiz

  1. Trong incident double charge, nếu payment gateway hỗ trợ idempotency key và code đã truyền đúng order_id làm key, incident này có xảy ra không? Giải thích.
  2. Anti-pattern số 3 (correctness lock chỉ dựa TTL) và anti-pattern số 7 (Redlock không fencing) đều liên quan đến process pause. Tại sao Redlock không giải quyết được vấn đề correctness dù dùng nhiều node?
  3. Bạn được giao thiết kế giới hạn: tối đa 5 worker đồng thời xử lý video encoding (mỗi job tốn ~3 phút, có thể crash giữa chừng). Primitive nào phù hợp? Cần lưu ý gì đặc biệt?
  4. Cron job tạo báo cáo tháng chạy vào 0:00 ngày 1 hàng tháng. App deploy 3 instance. Nếu dùng singleton worker với TTL = 60s và instance A acquire lock lúc 0:00:00, instance A crash lúc 0:00:45 (trước khi job xong). Điều gì xảy ra? Có vấn đề gì không?
  5. Một team muốn dùng Redis lock để bảo vệ việc debit tài khoản ngân hàng (số dư không được âm). Họ plan: acquire lock per user_id, check balance, debit, release. Đây là efficiency hay correctness lock? Đủ chưa? Nên thêm gì?

Đáp án gợi ý

  1. Không xảy ra. Gateway nhận lần gọi thứ 2 với cùng idempotency key (order_id) → tra cứu kết quả lần 1 và trả lại response đó mà không charge lại. Worker B nhận response thành công nhưng thực tế không có charge mới. Đây là lý do idempotency key là Layer 1 quan trọng nhất cho payment.
  2. Redlock yêu cầu acquire lock trên đa số node (3/5 hoặc 4/7). Nhưng vấn đề process pause xảy ra sau khi lock đã được acquire hợp lệ: process A acquire thành công, bị pause dài hơn TTL, tất cả node expire lock đồng thời. Process B acquire. Process A tỉnh dậy, vẫn tin mình giữ lock (chưa renew, chưa biết mất). Cả A và B cùng chạy. Redlock không thay đổi điều này vì vấn đề không nằm ở số node mà ở hành vi của client khi bị pause.
  3. Distributed semaphore (bài 50) với capacity = 5. Dùng ZSet với timestamp làm score và cleanup member expired (job crash không decrement). Set timeout = max job time (ví dụ 10 phút — lớn hơn 3 phút để có buffer). Cần chú ý: job phải có heartbeat refresh để giữ slot sống; cleanup phải chạy đủ thường xuyên để không block slot quá lâu sau crash.
  4. Lock expire (T=45s, còn 15s TTL). Lúc T=60s lock expire, instance B hoặc C acquire lock mới và chạy lại job. Job sẽ được hoàn thành bởi instance khác — failover tự động. Vấn đề: nếu job partial state (đã write một phần vào DB), job mới có thể duplicate một số step. Cần đảm bảo job idempotent (hoặc checkpoint/resume). Nếu job không idempotent, báo cáo có thể bị tạo 2 lần partial.
  5. Correctness lock — nếu 2 worker cùng debit cùng lúc, có thể cả 2 đều thấy balance đủ và cùng debit → số dư âm. Chưa đủ. Cần thêm: DB-level check-and-update atomic (UPDATE accounts SET balance = balance - amount WHERE user_id = ? AND balance >= amount) — database xử lý concurrency đúng hơn distributed lock. Lock ở Redis là redundant nếu DB transaction đủ mạnh. Nếu vẫn giữ lock, cần DB unique constraint hoặc optimistic lock (version column) để bắt race condition khi lock fail.

Bài tiếp theo

Module 5 bắt đầu với bài 53: vì sao cần queue, async processing và các bài toán mà synchronous call không đủ — nền cho List queue, Redis Streams, Consumer Groups trong các bài sau.

Tham khảo