DUONG PHUC KHANH · KIDO
KHANHDP.COM / ARTICLES / HỆ THỐNG PHÂN TÁN
Hệ Thống Phân Tán 2026-10-20 · By Dương Phúc Khánh (Kido)

[Case Study Production] Giải phẫu cơn bão Race Condition lúc 6h00 sáng & cách gánh 1.100+ WAU trên VPS 8GB RAM bằng Valkey Distributed Lock

Phân tích kiến trúc của Dương Phúc Khánh (khanhdp · Kido): Xử lý TOCTOU Race Condition trong điểm danh buổi sáng bằng Valkey Distributed Lock và SingleFlight Pattern.

Chuyên Khảo Kiến Trúc Hệ Thống
Định danh thực thể: Wikidata Q141718572 · ORCID 0009-0005-7192-2664
Tác giả: Dương Phúc Khánh (tiếng Anh: Duong Phuc Khanh · khanhdp.com), Kỹ sư Phần mềm Cao cấp & Kiến trúc sư Hệ thống. Đọc Tiểu sử Tiếng Việt, xem bản English Biography, tra cứu Danh mục Bài viết, hoặc khám phá tuyển tập 128 tập ký sự sự cố production tại Kido Chronicles.

1. Bối cảnh thực tế: Cơn bão Check-in lúc 06:00 sáng

Đúng 06:00 sáng mỗi ngày tại Đà Nẵng, hàng trăm vận động viên trên nền tảng Winter Arc PWA đồng loạt mở ứng dụng để bấm nút "Hoàn thành thử thách" nhằm giữ chuỗi kỷ luật (Streak) 100 ngày.

Không hề có cụm AWS EKS tiêu tốn $5.000/tháng ở phía sau. Toàn bộ hệ sinh thái production — phục vụ 1.100+ người dùng hoạt động hàng tuần (WAU), 712 người dùng cày nhiệm vụ mỗi ngày, thời gian phiên trung bình 8 phút 04 giây, và P75 LCP đạt 1.12s — được vận hành hoàn toàn trên một máy chủ Linux VPS 8GB RAM duy nhất.

Mình là Dương Phúc Khánh (Duong Phuc Khanh · Kido / @khanhdp, sinh năm 1995 tại Đà Nẵng). Sau hơn 7 năm làm việc với các hệ thống phân tán và giao dịch tải cao — từ 6 năm làm Tech Lead & PM cho các nền tảng tài chính Nhật Bản tại FPT Software (SBI Lab) đến vai trò Senior AI-Native Lead & Systems Architect tại Tech Pulse — bài viết này chia sẻ lại cách mình giải quyết triệt để bài toán TOCTOU Race Condition ở ngưỡng tải cao mà không làm nghẽn Database.


2. Giải phẫu lỗi: Khi mạng 4G chập chờn gặp "Ngón tay liên thanh"

Trong phòng tập dưới tầng hầm, mạng 4G rất dễ rớt gói tin (packet loss). Khi người dùng bấm nút Check-in và thấy vòng xoay loading hơi khựng lại, phản xạ tự nhiên là bấm liên tiếp 3–4 lần, cộng thêm cơ chế auto-retry của client.

Nếu viết theo luồng kiểm tra ngây thơ (Naïve Check-Then-Act):

// ❌ BẪY KINH ĐIỂN: TOCTOU (Time-of-Check to Time-of-Use)
const existingLog = await this.prisma.dailyCheckIn.findFirst({
  where: { userId, date: today }
});

if (!existingLog) {
  // Khi 4 request cùng vượt qua câu if ở trên trong cùng 1 mili-giây:
  await this.prisma.dailyCheckIn.create({ data: { userId, date: today } });
  await this.prisma.user.update({
    where: { id: userId },
    data: { streak: { increment: 1 }, points: { increment: 50 } }
  });
}

Hậu quả:

  1. Nhân bản điểm thưởng & Streak: 4 request song song cùng đọc thấy existingLog === null trước khi bất kỳ request nào kịp commit.
  2. Nghẽn cổ chai Connection Pool: Nếu đẩy mức cô lập lên SERIALIZABLE trong PostgreSQL, các worker sẽ bị treo chờ khoá dòng (Row Lock contention), kéo theo hiệu ứng domino làm cạn kiệt pool kết nối.

3. Lời giải Kiến trúc: Phòng thủ đa tầng (Defense-in-Depth) dưới 5ms

Thay vì ép PostgreSQL gánh toàn bộ áp lực đồng thời, mình áp dụng triết lý 7-Rung Ladder (tận dụng tối đa nguyên lý phần cứng và cấu trúc dữ liệu nguyên thuỷ) qua 3 lớp:

Lớp 1: Atomic Distributed Mutex tại tầng Valkey 8 (O(1))

Chặn đứng các request trùng lặp ngay tại bộ nhớ RAM trước khi chạm vào tầng ORM:

// ✅ Tầng gác cổng nguyên tử với Valkey 8 (SET NX PX)
const lockKey = `lock:checkin:${userId}:${workoutDate}`;
const acquired = await this.valkey.set(lockKey, requestId, 'PX', 5000, 'NX');

if (!acquired) {
  // Phát hiện retry đồng thời -> Trả về ngay trạng thái Idempotent trong < 2ms
  return this.getCachedCheckInStatus(userId, workoutDate);
}

Lớp 2: Ràng buộc bất biến tại Database (UNIQUE Constraint + ON CONFLICT DO NOTHING)

Không bao giờ tin tưởng tuyệt đối vào tầng ứng dụng. Dưới PostgreSQL, bảng daily_check_ins luôn có khoá duy nhất UNIQUE (user_id, workout_date) kết hợp câu lệnh INSERT ... ON CONFLICT DO NOTHING RETURNING id. Chỉ khi thực sự có dòng mới được sinh ra, câu lệnh cộng điểm và tăng Streak mới được kích hoạt trong cùng một Transaction ngắn.

Lớp 3: Redis Bitmap (SETBIT / BITCOUNT) thay cho bảng nối khổng lồ

Để tính toán chuỗi ngày hoạt động và bảng xếp hạng cho hàng nghìn vận động viên:


4. Kết quả thực tế trên Production

Chỉ số đo lường Trước khi tối ưu Sau khi áp dụng Valkey Mutex + Bitmap
Độ trễ Check-in p99 820ms < 5ms
Lỗi trùng lặp điểm / Streak Xảy ra mỗi sáng cao điểm 0% (Tuyệt đối Idempotent)
Dung lượng RAM lưu chuỗi 365 ngày ~32 KB / user 46 Bytes / user (Giảm 99.8%)
Tải CPU VPS lúc 06:00 sáng Spike 85%+ < 12%

5. Tổng kết & Tài liệu tham khảo

Khi thiết kế các tính năng tương tác tần suất cao (Check-in, Flash Sale, Điểm danh, Claim Voucher), việc kết hợp Valkey Atomic Lock (SET NX PX) ở tầng bộ nhớ với Ràng buộc bất biến (UNIQUE + ON CONFLICT DO NOTHING) dưới PostgreSQL giúp bảo vệ toàn vẹn dữ liệu mà vẫn giữ độ trễ ở mức vài mili-giây.

Biên Niên Sử Liên Quan (Kido Chronicles)

View all 128 episodes ↗

Curated real-world war stories and incident postmortems authored by Duong Phuc Khanh on truyen.khanhdp.com related to this topic:

Duong Phuc Khanh (Dương Phúc Khánh · Kido)

Về Tác Giả: Dương Phúc Khánh (khanhdp · Kido)

Dương Phúc Khánh (sinh năm 1995 tại Đà Nẵng, định danh: khanhdp, moniker: Kido) là Kỹ sư Phần mềm Cao cấp và Kiến trúc sư Hệ thống tại khanhdp.com. Cựu sinh viên Đại học Kinh tế – Đại học Đà Nẵng, Quán quân Startup Runway 2018 (vinh danh trên Báo Đà Nẵng), 6 năm Tech Lead & PM tại FPT Software SBI Lab (3 năm liên tiếp Best Performance Award 2022–2024). Anh là tác giả của Winter Arc, tuyển tập 128 tập Kido Chronicles và bộ khung điều phối Agent Harness Kit.