const { rowCount } = await pool.query(
`UPDATE shows SET remaining = remaining - $2
WHERE id = $1 AND remaining >= $2`,
[showId, qty],
);
if (rowCount === 0) throw new SoldOutError(); // MySQL: affectedRows; SQL Server: @@ROWCOUNT
Scenarios: Concurrency, Idempotency & Reliability
draftedMock interview tình huống production — oversell, double charge, rate limit, retry/circuit breaker, graceful shutdown trên K8s, webhook, idempotent consumer, deadlock, distributed lock, cron chạy 2 lần, OTP/MFA, race condition trong Node.
0/58 rated · mastery 0%
Bài học
7 bài · ~106 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~18 phút· 9 câuverify
Oversell & hot row: race trên một resource đếm được
Playbook cho oversell vé/tồn kho/voucher: nhận ra read-modify-write race (kể cả trong Node single-thread), fix nhỏ nhất bằng atomic conditional UPDATE, vì sao REPEATABLE READ cần retry, SKIP LOCKED cho pool, seat hold có hạn và payment về trễ, flash sale qua Redis Lua, hot row và sharded counter — đo thật trên PostgreSQL 17 + Redis 7.4 + Node 24.
- 02~14 phút· 7 câuverify
Deadlock, distributed lock & job chạy hai lần
Playbook cho các sự cố quanh lock: đọc deadlock log của Postgres, khoá theo thứ tự và giữ transaction ngắn, retry đúng lỗi, Redis lock có token và fencing, advisory lock cho cron, vì sao lock không chống được chạy lại tuần tự, cache rebuild lock, phản biện 'lock service chung' và cách bắt race trước production — chạy thật trên PostgreSQL 17 + Redis 7.4 + Node 24.
- 03~13 phút· 8 câuverify
Double charge: idempotency key, timeout của provider & reconciliation
Playbook chống trừ tiền hai lần: vì sao disable nút Pay không đủ, bốn lỗi của middleware idempotency 'chuẩn' trong note, fingerprint và scope, timeout là kết quả thứ ba, lease + recovery cho record kẹt 'processing', saga checkout với pivot transaction, reconciliation với settlement report và cách kể story STAR — chạy thật với Node 24, PostgreSQL 17 và một payment provider giả lập.
- 04~17 phút· 9 câuverify
At-least-once: webhook, idempotent consumer, ordering & read-your-writes
Webhook ack nhanh và verify chữ ký, idempotent consumer bằng ON CONFLICT trong cùng transaction, poison message và DLQ, ordering bị phá bởi Promise.all và retry, replica lag và read-your-writes — đo thật với Node 24, PostgreSQL 17 (primary + streaming replica) và Express.
- 05~15 phút· 11 câuverify
Rate limiting, fail-open/fail-closed & OTP abuse
Chọn thuật toán rate limit theo bài toán, tính fixed window và sliding window counter, tìm mọi bug của một token bucket Redis rồi viết lại bằng Lua, response 429 đúng, khi Redis chết thì fail-open hay fail-closed, OTP/MFA an toàn và SMS pumping — đo thật với Node 24 và Redis 7.4.
- 06~14 phút· 7 câuverify
Retry storm, deadline, circuit breaker & load shedding
Retry cái gì, full jitter, retry budget, khuếch đại 4^3 = 64, deadline propagation trong Node, circuit breaker cho downstream chậm, bulkhead/load shedding cho Black Friday và postmortem retry storm — đo thật với Node 24 và opossum 10.
- 07~15 phút· 7 câuverify
Deploy an toàn: graceful shutdown, signal & probe trên Kubernetes
502 khi rolling deploy, preStop và EndpointSlice, PID 1 và shell form, server.close với keep-alive, drain Kafka consumer, liveness vs readiness và restart loop.
Tóm tắt & cheat sheet của track
TL;DR
- Mọi scenario trong track này quy về 3 câu hỏi: (1) hai actor cùng chạm một resource thì ai thắng? (concurrency), (2) cùng một việc bị làm hai lần thì sao? (idempotency), (3) dependency chậm/chết/đang deploy thì hệ thống cư xử thế nào? (reliability).
- Sửa race condition = đẩy invariant xuống nơi có atomicity (một câu
UPDATE ... WHERE, unique constraint, Lua script), không phải "check trong code rồi ghi". - At-least-once là thực tế của mạng: client retry, provider retry webhook, Kafka redeliver sau rebalance. Mọi handler có side effect phải idempotent — dedup bằng key + unique constraint trong cùng transaction với side effect.
- Retry chỉ an toàn khi: lỗi transient, operation idempotent, có backoff + full jitter, có retry budget và deadline. Nếu không, retry biến một sự cố nhỏ thành retry storm.
- Vận hành: graceful shutdown trên K8s là bài toán race giữa SIGTERM và việc gỡ endpoint → readiness fail +
preStopsleep + đóng keep-alive + drain consumer. Liveness không bao giờ check DB.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Read-modify-write race | Đọc → quyết định trong app → ghi; khoảng giữa là nơi request khác chen vào | 2 request cùng đọc stock=1, cùng trừ |
| Atomic conditional UPDATE | Điều kiện nằm trong WHERE, DB kiểm và ghi trong 1 lệnh | UPDATE ... SET stock=stock-1 WHERE id=$1 AND stock>=1 → rowCount |
| Optimistic lock | Ghi kèm WHERE version=$old; 0 row → conflict → retry | version = version + 1 |
| Pessimistic lock | Khoá row khi đọc (FOR UPDATE), người khác chờ | NOWAIT = lỗi ngay, SKIP LOCKED = bỏ qua row đang bị khoá |
| Idempotency key | Client gửi 1 key/1 thao tác; server lưu key + fingerprint + response | Idempotency-Key: 7c3e... |
| Idempotent consumer | Dedup theo event_id bằng unique constraint trong cùng transaction với business write | INSERT INTO processed_events ... ON CONFLICT DO NOTHING |
| Token bucket | Bucket dung lượng B, nạp r token/s; request lấy 1 token | B=20, r=100/60 → burst 20, trung bình 100/phút |
| Full jitter | sleep = random(0, min(cap, base·2^n)) | n=3, base=100ms → random 0..800ms |
| Retry budget | Giới hạn tỉ lệ retry / request (vd ≤10%) | 1000 rps → tối đa 100 retry/s |
| Circuit breaker | Closed → Open (fail fast) → Half-open (thử) | opossum errorThresholdPercentage: 50 |
| Fencing token | Số tăng dần cấp kèm lock; storage từ chối token cũ | WHERE last_token < $token |
| Reservation/hold | Giữ tài nguyên có hạn (expires_at), hết hạn thì trả lại | Giữ ghế 10 phút khi checkout |
| Pivot transaction | Bước trong saga mà sau nó chỉ được đi tiếp (retry), không compensate | Capture payment thành công |
| Read-your-writes | User luôn thấy chính thứ mình vừa ghi | Route read về primary 5s sau write |
Cơ chế hoạt động
Chống double charge bằng idempotency key (có xử lý in-flight và crash)
Graceful shutdown của pod Node trên Kubernetes
Idempotent consumer + DLQ
Khung trả lời (dùng cho mọi scenario trong track)
- Clarify & đo: volume (QPS, số item), invariant nghiệp vụ ("không bao giờ bán quá 100 vé", "mỗi order tối đa 1 lần charge"), chấp nhận stale/lỗi đến đâu, hiện có metric/log gì.
- Gọi tên cơ chế: race (read-modify-write, check-then-act), duplicate (retry, redelivery), amplification (retry storm), race hạ tầng (SIGTERM vs endpoint removal).
- Fix theo lớp, rẻ nhất trước: constraint/atomic op ở storage → idempotency/dedup → lock (DB trước Redis) → thay đổi kiến trúc (queue, reservation, saga).
- Failure modes của chính cái fix: lock hết hạn, Redis chết, key "processing" kẹt, DLQ phá ordering, fail-open vs fail-closed.
- Chứng minh: test song song (k6/autocannon + assert invariant), metric (conflict rate, 409, DLQ size, retry ratio), reconciliation job định kỳ như lưới an toàn cuối.
Trade-offs
| Kỹ thuật | Throughput khi contention cao | Độ phức tạp | Khi nào dùng | Rủi ro chính |
|---|---|---|---|---|
| Atomic conditional UPDATE | Cao nhưng serialize trên 1 row | Thấp | Counter/tồn kho đơn giản | Hot row: mọi request xếp hàng trên 1 row lock |
| Optimistic (version) | Kém khi conflict nhiều (retry liên tục) | Trung bình | Logic phức tạp, conflict hiếm | Retry storm, starvation |
SELECT ... FOR UPDATE | Trung bình, giữ lock suốt transaction | Trung bình | Nhiều bước đọc/ghi cần nhất quán | Deadlock, pool cạn nếu transaction dài |
FOR UPDATE SKIP LOCKED | Cao | Trung bình | Phân phát item bất kỳ (ghế trống bất kỳ, job queue) | Không dùng được khi cần đúng 1 row cụ thể |
| Redis Lua pre-decrement | Rất cao (~100k ops/s một node, verify) | Cao | Flash sale | Lệch với DB → cần reconciliation |
| Sharded counter | Cao | Trung bình | Counter nóng (view, stock pool) | Đọc tổng tốn hơn; check "≥0" khó hơn |
| Redis lock | Cao | Trung bình | Tránh làm trùng việc tốn kém (efficiency) | Không an toàn cho correctness nếu thiếu fencing |
| Postgres advisory lock | Trung bình | Thấp | Job/cron singleton, đã có Postgres | Session lock + PgBouncer transaction mode = hỏng |
| Rate limit fail mode | Hành vi khi Redis chết | Hợp cho |
|---|---|---|
| Fail-open | Cho qua hết (có thể kèm local limiter) | API đọc thông thường, ưu tiên availability |
| Fail-closed | Chặn (429/503) | Login, OTP, gửi SMS, endpoint tốn tiền |
| Degraded local | Mỗi pod dùng limit/N in-memory | Hầu hết trường hợp — trung dung |
Pitfalls
- Kiểm tra trong code (
if (stock > 0)) rồi mớiUPDATE— dù dùng transaction ở READ COMMITTED vẫn race. - Idempotency key không có fingerprint (cùng key, body khác) và không có lease → key kẹt
processingvĩnh viễn sau crash. - Không truyền idempotency key xuống payment provider → timeout ở giữa vẫn double charge.
- Retry ở mọi layer (client + gateway + service + SDK) → amplification theo cấp số nhân.
server.close()rồi nghĩ là xong: keep-alive connection giữ process sống đến SIGKILL.CMD ["npm", "start"]hoặc shell form → SIGTERM không tới được Node.- Liveness probe gọi DB → DB chậm 30s = restart toàn bộ fleet.
- Verify HMAC trên
JSON.stringify(req.body)thay vì raw body; so sánh bằng===; không kiểm timestamp → replay. - Check
exists(event_id)ngoài transaction → hai consumer cùng pass check. - Redis lock release bằng
DELtrơn; lock hết hạn giữa chừng mà không có fencing token. - OTP tạo bằng
Math.random(), lưu plaintext, không giới hạn số lần thử, không throttle resend.
Cheat sheet
- Oversell:
UPDATE ... WHERE stock >= $qty+ checkrowCount; ghế cụ thể → unique(show_id, seat_id); flash sale → Redis Lua + queue + reconcile; hot row → sharded counter. - Postgres errors:
40001serialization_failure,40P01deadlock_detected,55P03lock_not_available (NOWAIT),23505unique_violation. SQL Server: deadlock = error 1205;WITH (UPDLOCK, ROWLOCK),READPAST≈ SKIP LOCKED. - Idempotency record:
key, scope(user/tenant), fingerprint(sha256 body), status, locked_until, response_code, response_body, created_at+ TTL ~24h (Stripe giữ 24h, verify). - Rate limit: 429 +
Retry-After; IETFRateLimit-Policy/RateLimitheaders (draft, verify). Tính toán trong Lua để atomic. - Retry: chỉ transient (timeout, 502/503/504, 429 có
Retry-After, ECONNRESET) + idempotent; full jitter; tối đa 3 attempt; retry ở một layer; budget 10%. - Shutdown: preStop sleep 5–10s → SIGTERM → readiness 503 →
server.close()+closeIdleConnections()→ drain → commit offset → exit.terminationGracePeriodSecondsmặc định 30s và bao gồm preStop. - Webhook: raw body HMAC-SHA256 + timestamp ±5 phút +
timingSafeEqual→ insert event (unique event_id) → 200 nhanh → worker → reconcile theo API. - Lock:
SET k token NX PX ttl+ Lua compare-and-delete + fencing token cho correctness; hoặcpg_try_advisory_xact_lock. - Cron: K8s
concurrencyPolicy: Forbid+ job idempotent (CronJob có thể tạo 2 job hoặc 0 job trong vài trường hợp — docs K8s). - OTP:
crypto.randomInt, hash + TTL 5 phút, ≤5 lần thử, resend cooldown 30–60s, single-use, binduser + purpose,timingSafeEqual; ưu tiên TOTP/WebAuthn.
References
- PostgreSQL — Transaction Isolation ↗
- PostgreSQL — Explicit Locking (row locks, advisory locks, deadlocks) ↗
- PostgreSQL — SELECT (FOR UPDATE, NOWAIT, SKIP LOCKED) ↗
- PostgreSQL — Error codes (Appendix A) ↗
- PostgreSQL — Monitoring replication (pg_stat_replication) ↗
- Node.js — http.Server close / closeIdleConnections / closeAllConnections ↗
- Node.js — crypto.timingSafeEqual / randomInt ↗
- Kubernetes — Pod termination lifecycle ↗
- Kubernetes — Container lifecycle hooks (preStop) ↗
- Kubernetes — Configure liveness, readiness and startup probes ↗
- Kubernetes — CronJob (concurrencyPolicy, job idempotency) ↗
- Apache Kafka — Producer configs (enable.idempotence, max.in.flight.requests.per.connection) ↗
- Redis — Distributed locks ↗
- Redis — Scripting with Lua (EVAL) ↗
- AWS Architecture Blog — Exponential Backoff and Jitter ↗
- AWS Builders' Library — Timeouts, retries, and backoff with jitter ↗
- AWS Builders' Library — Making retries safe with idempotent APIs ↗
- AWS Builders' Library — Using load shedding to avoid overload ↗
- RFC 9110 — HTTP Semantics (Retry-After, idempotent methods) ↗
- RFC 6585 — 429 Too Many Requests ↗
- IETF draft — RateLimit header fields for HTTP ↗
- IETF draft — The Idempotency-Key HTTP header field ↗
- RFC 6238 — TOTP ↗
- NIST SP 800-63B — Authentication and Lifecycle Management ↗
- OWASP — Multifactor Authentication Cheat Sheet ↗
- Martin Kleppmann — How to do distributed locking ↗
- opossum — Node.js circuit breaker ↗
Personal notes (Notion)
Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.
Interview Questions
Showing 58 / 58
CREATE UNIQUE INDEX uq_active_seat ON seat_holds (show_id, seat_id)
WHERE status IN ('held', 'booked');
-- SQL Server: filtered index cũng hỗ trợ WHERE; MySQL không có partial index → dùng generated column
async function withSerializableRetry<T>(fn: () => Promise<T>, max = 5): Promise<T> {
for (let attempt = 1; ; attempt++) {
try { return await fn(); }
catch (e: any) {
if (!['40001', '40P01'].includes(e.code) || attempt === max) throw e;
await sleep(Math.random() * 20 * 2 ** attempt); // full jitter
}
}
}
UPDATE promo_codes SET user_id = $1, claimed_at = now()
WHERE id = (
SELECT id FROM promo_codes
WHERE user_id IS NULL AND campaign_id = $2
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING code;
window [00:00–01:00) count=100 (all at 00:59.5)
window [01:00–02:00) count=100 (all at 01:00.2)
=> 200 requests within ~0.7 s
prev window [00:00-01:00) = 80
curr window [01:00-02:00) = 30, now = 01:15
weight = 1 - 15/60 = 0.75
estimate = 80*0.75 + 30 = 90 -> allow
async function callWithRetry<T>(fn: () => Promise<T>, max = 4): Promise<T> {
for (let i = 0; i < max; i++) {
try { return await fn(); }
catch (e) {
if (!isRetryable(e) || i === max - 1) throw e;
const base = 2 ** i * 100; // 100, 200, 400, 800ms
const jitter = Math.random() * 100;
await sleep(base + jitter);
}
}
throw new Error('unreachable');
}
import CircuitBreaker from 'opossum';
const breaker = new CircuitBreaker(fetchPrice, {
timeout: 1200, errorThresholdPercentage: 50,
rollingCountTimeout: 10_000, volumeThreshold: 20, resetTimeout: 30_000,
});
breaker.fallback((sku: string) => priceCache.getStale(sku));
breaker.on('open', () => metrics.increment('cb.pricing.open'));
spec:
terminationGracePeriodSeconds: 45
containers:
- name: api
lifecycle:
preStop:
sleep: { seconds: 10 } # older clusters: exec: { command: ["sleep", "10"] }
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
CMD npm start
ERROR: deadlock detected
DETAIL: Process 4211 waits for ShareLock on transaction 918273; blocked by process 4302.
Process 4302 waits for ShareLock on transaction 918270; blocked by process 4211.
Process 4211: UPDATE inventory SET qty = qty - 1 WHERE sku = 'B-2'
Process 4302: UPDATE inventory SET qty = qty - 1 WHERE sku = 'A-1'
HINT: See server log for query details.
CONTEXT: while updating tuple (12,7) in relation "inventory"
apiVersion: batch/v1
kind: CronJob
spec:
schedule: "0 2 * * *"
timeZone: "Asia/Ho_Chi_Minh"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 600
jobTemplate:
spec:
backoffLimit: 2
app.post('/vouchers/:code/redeem', async (req, res) => {
const v = await db.voucher.findUnique({ where: { code: req.params.code } });
if (!v || v.redeemedAt) return res.status(409).end();
await paymentService.applyCredit(req.user.id, v.amount); // await → yields
await db.voucher.update({ where: { code: v.code }, data: { redeemedAt: new Date() } });
res.json({ ok: true });
});
async function allow(userId: string): Promise<boolean> {
const now = Date.now();
const key = `rl:${userId}`;
const { tokens, ts } = await redis.hgetall(key);
const refill = Math.floor((now - ts) / 1000);
const current = Math.min(10, (tokens ?? 10) + refill);
if (current < 1) return false;
await redis.hset(key, { tokens: current - 1, ts: now });
return true;
}
-- KEYS[1]=bucket ARGV: capacity, refill_per_ms, cost
local cap, rate, cost = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
local t = redis.call('TIME'); local now = t[1] * 1000 + math.floor(t[2] / 1000)
local h = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(h[1]) or cap
local ts = tonumber(h[2]) or now
tokens = math.min(cap, tokens + (now - ts) * rate)
local allowed = 0
if tokens >= cost then tokens = tokens - cost; allowed = 1 end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(cap / rate))
local retry = allowed == 1 and 0 or math.ceil((cost - tokens) / rate)
return { allowed, math.floor(tokens), retry }
async function pay(req: Request) {
const key = req.headers['idempotency-key'] as string;
const existing = await db.idempotency.findByKey(key);
if (existing) return existing.response; // replay
const inserted = await db.idempotency.insertIfAbsent(key, 'processing');
if (!inserted) throw new ConflictError('in progress');
const result = await chargeGateway(req.body);
await db.idempotency.update(key, 'done', result);
return result;
}
UPDATE idempotency_keys
SET locked_until = now() + interval '60 seconds', attempt = attempt + 1
WHERE scope = $1 AND key = $2 AND status = 'processing' AND locked_until < now()
RETURNING recovery_point, provider_ref;
-- KEYS[1]=stock KEYS[2]=buyers ARGV[1]=user ARGV[2]=qty
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then return -1 end
local s = tonumber(redis.call('GET', KEYS[1]) or '0')
if s < tonumber(ARGV[2]) then return 0 end
redis.call('DECRBY', KEYS[1], ARGV[2])
redis.call('SADD', KEYS[2], ARGV[1])
return 1
-- 16 shards
UPDATE campaign_budget_shards
SET spent = spent + $2
WHERE campaign_id = $1 AND shard = floor(random() * 16)::int
AND spent + $2 <= shard_cap;
SELECT sum(spent) FROM campaign_budget_shards WHERE campaign_id = $1;
async function getWithLock(key: string): Promise<string> {
const val = await redis.get(key);
if (val) return val;
const lock = await redis.set(`lock:${key}`, '1', 'NX', 'EX', 10);
if (!lock) { await sleep(50); return getWithLock(key); }
const fresh = await queryDB(key);
await redis.set(key, fresh, 'EX', 300);
await redis.del(`lock:${key}`);
return fresh;
}
const deadline = Date.now() + 1900;
const signal = AbortSignal.any([AbortSignal.timeout(1900), clientAbort.signal]);
const left = () => Math.max(0, deadline - Date.now());
const a = await callA({ signal, timeoutMs: Math.min(800, left()) });
const [b, c] = await Promise.all([
callB(a, { signal: AbortSignal.any([signal, AbortSignal.timeout(Math.min(900, left()))]) }),
callC(a, { signal: AbortSignal.any([signal, AbortSignal.timeout(Math.min(900, left()))]) }),
]);
process.on('SIGTERM', async () => {
server.close(); // stop accepting
await drainInflightRequests(30_000);
await db.end();
await consumer.disconnect();
process.exit(0);
});
livenessProbe:
httpGet: { path: /health, port: 3000 } # checks DB + Redis
periodSeconds: 5
timeoutSeconds: 1
failureThreshold: 2
readinessProbe:
httpGet: { path: /health, port: 3000 }
app.use(express.json());
app.post('/webhooks/payments', async (req, res) => {
const sig = req.header('x-signature');
const expected = crypto.createHmac('sha256', SECRET)
.update(JSON.stringify(req.body)).digest('hex');
if (sig !== expected) return res.status(401).end();
await handleEvent(req.body); // 5–15 s of work
res.sendStatus(200);
});
async function handle(message: KafkaMessage) {
const eventId = message.headers!.eventId!.toString();
const seen = await db.processedEvents.exists(eventId); // check outside tx
if (seen) return;
await db.transaction(async (tx) => {
await applyBusinessLogic(tx, message);
await tx.processedEvents.insert(eventId);
});
await consumer.commitOffsets([...]);
}
async function transfer(from: number, to: number, amount: number) {
return retry(() => db.tx(async (tx) => {
await tx.query('SELECT * FROM accounts WHERE id = $1 FOR UPDATE', [from]);
await fraudService.check(from, to, amount); // HTTP, 200-2000 ms
await tx.query('SELECT * FROM accounts WHERE id = $1 FOR UPDATE', [to]);
await tx.query('UPDATE accounts SET balance = balance - $2 WHERE id = $1', [from, amount]);
await tx.query('UPDATE accounts SET balance = balance + $2 WHERE id = $1', [to, amount]);
}), { retries: 5, retryOn: () => true });
}
-- on primary, after the write commits
SELECT pg_current_wal_lsn(); -- e.g. 3/A1B2C3D0
-- on a replica, before serving the read
SELECT pg_last_wal_replay_lsn() >= '3/A1B2C3D0'::pg_lsn AS caught_up;
async function sendOtp(userId: string, phone: string) {
const code = Math.floor(100000 + Math.random() * 900000).toString();
await redis.set(`otp:${phone}`, code);
await sms.send(phone, `Your code is ${code}`);
}
async function verifyOtp(phone: string, code: string) {
return (await redis.get(`otp:${phone}`)) === code;
}