Nội dung bài viết
Video học lập trình mỗi ngày
"Anh ơi, nếu 100.000 người cùng bấm mua 1.000 vé tàu trong 1 giây thì sao?"
Bài viết này được đưa vào Series: Hành trình của INTERN đánh bại SENIOR thông qua dự án DDD - vetautet.com có lượng đồng thời CAO.
Vì cốt truyện có tính logic nên nếu như bạn là một bạn mới ghé thăm vào Blog - Con đường của một backend thì trước tiên để hiểu nội dung thì mỗi bài viết tương ứng ở đây là một kỹ thuật được áp dùng trong dự án thì bạn hãy cố gắng xem những bài viết trước về dự án có lượng đồng thời CAO để có thêm thông tin về hành trình này.
Bạn có thể bỏ qua section Lộ trình Series: Bán vé tàu Tết - Từ Zero đến Production nếu như bạn không quan tâm đến hành trình này. Và nhảy đến phần Chương 27: DDD - vetautet.com | Flash Sale - Khi 100.000 người cùng muốn mua 1.000 vé tàu để xem bài viết này.
📚 Lộ trình Series: Bán vé tàu Tết - Từ Zero đến Production
"Tôi không dạy bạn lý thuyết suông. Tôi chia sẻ những gì chúng tôi đã làm, đã sai, và đã sửa. Tù 1000 QPS -> ~40.000 QPS với chi phí một instance vps"
Nếu bạn muốn theo dõi từ đầu, đây là lộ trình mà tôi khuyến khích. Mỗi bài là một mảnh ghép, ghép đúng thứ tự thì bức tranh mới hoàn chỉnh.
🏗️ Phần 1: Nền móng - Setup & Kiến trúc (Bài 01-03)
| # | Tiêu đề | Bạn sẽ học được gì? |
|---|---|---|
| 01 | Khởi đầu: Xây dựng dự án DDD bán vé tàu với kiến trúc đồng thời cao | Tư duy DDD, tại sao chọn kiến trúc này |
| 02 | Source Code: Video 0-4 - Cách chạy project | Hướng dẫn setup môi trường |
| 03 | Hoàn thành SETUP dự án theo kiến trúc Microservice | Cấu trúc project, các service cần có |
🛡️ Phần 2: Tuyến phòng thủ đầu tiên - Rate Limiting & Circuit Breaker (Bài 04-06)
| # | Tiêu đề | Bạn sẽ học được gì? |
|---|---|---|
| 04 | Circuit Breaker vs RateLimiter - Tuyến phòng thủ đầu tiên | Resilience4j, chặn request quá tải |
| 05 | Distributed Cache - Tuyến phòng thủ thứ hai (LUA vs Redis) | Redis Cluster, Lua Script |
| 06 | Tâm sự DEV: Vì sao tôi không dùng LUA Redis trong kịch bản Distributed | Trade-off thực tế, kinh nghiệm xương máu |
📊 Phần 3: Giám sát hệ thống - Monitoring & Observability (Bài 07-10)
| # | Tiêu đề | Bạn sẽ học được gì? |
|---|---|---|
| 07 | Setup hệ thống giám sát API với Prometheus - Giúp DEV ngủ ngon | Prometheus metrics, alerting |
| 08 | Đại ca Grafana - Giám sát hệ thống giúp DEV ngủ ngon | Dashboard visualization |
| 09 | Giám sát MySQL ONLINE qua hệ thống API | MySQL monitoring, slow query detection |
| 10 | Hệ thống giám sát Connects vs Performance Distributed Redis | Redis monitoring, connection pooling |
🚀 Phần 4: Tối ưu hiệu năng - Từ 5.000 đến 25.000 req/s (Bài 11-13)
| # | Tiêu đề | Bạn sẽ học được gì? |
|---|---|---|
| 11 | Họp TEAM: Vũ khí cho việc tăng tốc 20.000 req/s - 5 tiêu chí phải đạt | Checklist performance |
| 12 | Không những 20.000 mà là 25.000 req/s - Chúng tôi nhận thưởng | Local cache (Guava), optimization |
| 13 | Triển khai hệ thống phân tán LOGs ELK cho hệ thống LỚN | Elasticsearch, Logstash, Kibana |
🔄 Phần 5: Data Consistency - Bài toán nhất quán dữ liệu (Bài 14-17)
| # | Tiêu đề | Bạn sẽ học được gì? |
|---|---|---|
| 14 | Hãy dừng Code 5 phút - Tính nhất quán (Consistency) thực sự diễn ra thế nào? | CAP theorem thực tế |
| 15 | Proxy Nginx vs 2 server - Kịch bản mua vé đồng thời CAO StockAvailable | Load balancing, session handling |
| 16 | DEV SA (Solution Architecture) - Dữ liệu phân tán giờ đã nhất quán | Distributed data consistency |
| 17 | Triển khai mức độ nhất quán tốt nhất phù hợp với nhu cầu ứng dụng | Eventual vs Strong consistency |
📦 Phần 6: Tồn kho & Đặt hàng - Bài toán cốt lõi (Bài 18-23)
| # | Tiêu đề | Bạn sẽ học được gì? |
|---|---|---|
| 18 | StockDeduction - Tôi đã từng đổ máu, nhưng bạn tuyệt đối không nên | Pitfalls của stock deduction |
| 19 | 3 CÁCH TRỪ HÀNG TỒN KHO trong MySQL - Optimistic lock, Pessimistic lock, Redis | |
| 20 | Kiểm soát đồng thời cao và khấu trừ hàng tồn kho của Senior | High concurrency stock management |
| 21 | Sau OpenTicket, API nhất quán dữ liệu về Khấu trừ tồn kho PlaceOrder | Order flow, inventory sync |
| 22 | Luận bàn về 100 trung tâm bán 5000 ticket/ngày - Chia dữ liệu Order | Database sharding, partitioning |
| 23 | Cách Senior lưu trữ dữ liệu cho vetautet hiệu quả cao, hiệu suất tốt | Data modeling, indexing strategy |
💳 Phần 7: Transaction & Payment - Distributed Transactions (Bài 24-25)
| # | Tiêu đề | Bạn sẽ học được gì? |
|---|---|---|
| 24 | PATTERN SAGA - Distributed Transactions cho Senior BE | Saga pattern, compensating transactions |
| 25 | PATTERN SAGA - Order, Payment is DONE ✅✅✅ | Complete payment flow |
📈 Phần 8: Scaling & Production - Triển khai thực tế (Bài 26-27)
| # | Tiêu đề | Bạn sẽ học được gì? |
|---|---|---|
| 26 | Triển khai TINH TẾ một TABLE có dữ liệu tăng CHỤC TRIỆU order/tháng | Table partitioning, archiving |
| 27 | JAVA DDD Source Code: Bán Vé Từ Video Section 17 - 26 How to run() | Source Code |
💡 Lời khuyên của tôi: Đừng nhảy cóc. Series này được thiết kế theo lộ trình từ dễ đến khó, từ nền tảng đến nâng cao. Nếu bạn nhảy thẳng vào bài 24 mà chưa hiểu bài 04-06, bạn sẽ thấy khó nuốt.
Trước khi bắt đầu: "Anh ơi, làm sao chặn được bọn BOT?"
Một anh chàng hỏi tôi qua Discord: "Anh ơi, hệ thống Flash Sale của em bị BOT quét sạch hàng trong 0.3 giây. User thật không mua được gì cả. Làm sao hạn chế rủi ro từ những lệnh giả mạo vậy anh?"
Fraud Ecosystem - Hệ sinh thái gian lận. Nghe thì có vẻ xa lạ, nhưng thực tế nó đang diễn ra hàng ngày trong các hệ thống e-commerce của chúng ta.
Kẻ thù không phải script kiddie, mà là một TỔ CHỨC có hệ thống
Đừng nghĩ rằng đối thủ của bạn là mấy anh chàng rảnh rỗi viết script để "nghịch". Không. Đây là một hệ sinh thái hoàn chỉnh với 3 tầng:
┌─────────────────────────────────────────────────────────────────┐
│ HỆ SINH THÁI ĐEN-XÁM │
├─────────────────────────────────────────────────────────────────┤
│ UPSTREAM (Cung cấp tài nguyên) │
│ ├── SIM rác (hàng triệu số điện thoại thật) │
│ ├── CMND/CCCD thật (mua từ chợ đen) │
│ ├── Proxy xoay IP (hàng nghìn IP khác nhau) │
│ └── Device fingerprint giả mạo │
├─────────────────────────────────────────────────────────────────┤
│ MIDSTREAM (Phát triển công cụ) │
│ ├── Auto-register bot (đăng ký hàng loạt account) │
│ ├── Flash sale sniper (bắn request trong milliseconds) │
│ ├── Captcha bypass (AI + nhân công giải captcha) │
│ └── Behavior simulation (giả lập hành vi người dùng) │
├─────────────────────────────────────────────────────────────────┤
│ DOWNSTREAM (Biến thành tiền) │
│ ├── Bán lại vé/hàng với giá cao │
│ ├── Thu thập coupon rồi bán │
│ └── Rửa điểm thưởng, cashback │
└─────────────────────────────────────────────────────────────────┘
Bạn thấy chưa? Đây không phải là cuộc chiến với một cá nhân, mà là với một chuỗi cung ứng hoàn chỉnh. Họ có SIM thật, CCCD thật, vượt qua mọi xác thực OTP. Họ có AI giải captcha trong 2-3 giây, nhanh hơn cả người thật. Họ có hàng nghìn IP khác nhau, rate limiting theo IP trở nên vô nghĩa.
Họ không phá hệ thống, họ "CHUI VÀO" hệ thống để kiếm lợi
Điều đáng sợ nhất là: BOT của họ không tấn công theo kiểu DDoS hay SQL Injection. Chúng giả làm user thật và thực hiện giao dịch hoàn toàn hợp lệ - chỉ là nhanh hơn, nhiều hơn, và có tổ chức hơn.
Khi Flash Sale bắt đầu, trong khi user thật còn đang load trang, thì BOT đã:
- Gửi 1000 request trong 100ms đầu tiên
- Từ 500 IP khác nhau
- Với 500 account khác nhau (đều có số điện thoại thật)
- Mỗi account mua đúng 2 vé (không vi phạm rule "tối đa 2 vé/account")
Kết quả? 1000 vé hết sạch trong 0.3 giây. User thật chưa kịp nhấn nút "Mua ngay".
Vậy chúng ta đánh lại như thế nào?
Câu trả lời ngắn gọn: Không có biện pháp nào là tối ưu hoàn toàn. Đây là cuộc chiến dài hơi, và chúng ta cần nhiều lớp phòng thủ:
| Lớp phòng thủ | Kỹ thuật | Hiệu quả |
|---|---|---|
| Tầng 1: Network | WAF, DDoS Protection, IP Reputation | Chặn được 60-70% BOT cơ bản |
| Tầng 2: Application | Rate Limiting, Device Fingerprint | Chặn thêm 15-20% |
| Tầng 3: Business | Hành vi bất thường, Cùng địa chỉ giao hàng, Cùng thanh toán | Chặn thêm 5-10% |
| Tầng 4: Post-purchase | Review đơn hàng bất thường, Cancel đơn của BOT | Xử lý phần còn lại |
Trong bài viết hôm nay, chúng ta sẽ tập trung vào Tầng 2 & 3 - những gì backend developer có thể làm được. Và tin tôi đi, nếu làm tốt 2 tầng này, bạn đã chặn được 80% kẻ xấu rồi.
Được rồi, quay lại câu chuyện chính...
Một buổi chiều thứ 6 định mệnh
Chiều thứ 6, khi mọi người đang chuẩn bị tinh thần cho weekend thì email từ Product Owner bay vào inbox của team:
"Team ơi, tuần sau mình có Flash Sale vé tàu Tết. Dự kiến 100.000 người truy cập đồng thời để tranh nhau 1.000 vé. Mọi người chuẩn bị tinh thần nhé!"
Cả phòng im lặng. David buông cốc cà phê xuống bàn. Tôi nhìn sang INTERN, cậu ta đang... cười?
"Em có ý tưởng gì à?" - Tôi hỏi.
"Dạ anh, em nghĩ có 3 cách để xử lý bài toán này. Nhưng mà... để em vẽ lên bảng cho anh em xem đã."
INTERN đứng dậy, cầm bút whiteboard và bắt đầu...
Cách 1: Đồng bộ MySQL + Redis - "Cách của người mới"
"Đầu tiên, cách đơn giản nhất mà ai cũng nghĩ đến..." - INTERN nói.
User Request → Check Redis (còn vé?) → Trừ Redis → Ghi MySQL → Response
INTERN viết code lên bảng:
@Service
@RequiredArgsConstructor
public class FlashSaleServiceV1 {
private final RedisTemplate<String, Integer> redisTemplate;
private final OrderRepository orderRepository;
/**
* Cách 1: Đồng bộ - Đơn giản nhưng chậm
* Vấn đề: Blocking tại MySQL, throughput thấp
*/
public OrderResult purchaseTicket(Long userId, Long ticketId) {
String stockKey = "flash:stock:" + ticketId;
// Bước 1: Check tồn kho trong Redis
Integer stock = redisTemplate.opsForValue().get(stockKey);
if (stock == null || stock <= 0) {
return OrderResult.fail("Hết vé rồi bạn ơi!");
}
// Bước 2: Trừ tồn kho Redis
Long newStock = redisTemplate.opsForValue().decrement(stockKey);
if (newStock < 0) {
// Đã bị người khác mua mất, hoàn lại
redisTemplate.opsForValue().increment(stockKey);
return OrderResult.fail("Có người nhanh tay hơn bạn!");
}
// Bước 3: Ghi đơn hàng vào MySQL (BLOCKING!)
try {
Order order = Order.builder()
.userId(userId)
.ticketId(ticketId)
.status(OrderStatus.PENDING)
.createdAt(LocalDateTime.now())
.build();
orderRepository.save(order);
return OrderResult.success(order.getId());
} catch (Exception e) {
// Rollback Redis nếu MySQL fail
redisTemplate.opsForValue().increment(stockKey);
return OrderResult.fail("Hệ thống bận, thử lại sau!");
}
}
}
David nhíu mày: "Cách này có vấn đề gì?"
INTERN gật đầu: "Dạ, vấn đề là ở bước 3 - Ghi MySQL. Khi 100.000 request cùng đến, tất cả đều phải chờ MySQL ghi xong mới trả về. MySQL sẽ nghẽn, response time tăng vọt, user thấy loading mãi... rồi timeout."
"Throughput?" - Tôi hỏi.
"Dạ, khoảng 2.000-3.000 req/s là max rồi anh. Không đủ cho 100.000 người."
Cách 2: Bất đồng bộ với Message Queue - "Cách của người biết nghĩ"
INTERN xoá bảng và vẽ tiếp:
User Request → Check Redis → Trừ Redis → Đẩy vào Kafka → Response "Đang xử lý"
↓
Consumer ← Kafka → Ghi MySQL (từ từ)
"Ý tưởng là: Nhận đơn thật nhanh, xử lý sau. Giống như lúc đặt bàn nhà hàng, nhân viên ghi tên bạn vào sổ rồi bảo 'Chờ chút nhé', chứ không bắt bạn đứng đợi họ dọn bàn."
@Service
@RequiredArgsConstructor
public class FlashSaleServiceV2 {
private final RedisTemplate<String, Integer> redisTemplate;
private final KafkaTemplate<String, OrderMessage> kafkaTemplate;
private static final String FLASH_SALE_TOPIC = "flash-sale-orders";
/**
* Cách 2: Async với Kafka
* Ưu điểm: Throughput cao, không blocking
* Nhược điểm: Eventually consistent, user không biết ngay kết quả cuối
*/
public OrderResult purchaseTicketAsync(Long userId, Long ticketId) {
String stockKey = "flash:stock:" + ticketId;
// Bước 1: Check và trừ tồn kho Redis (vẫn giữ nguyên)
Integer stock = redisTemplate.opsForValue().get(stockKey);
if (stock == null || stock <= 0) {
return OrderResult.fail("Hết vé rồi bạn ơi!");
}
Long newStock = redisTemplate.opsForValue().decrement(stockKey);
if (newStock < 0) {
redisTemplate.opsForValue().increment(stockKey);
return OrderResult.fail("Có người nhanh tay hơn bạn!");
}
// Bước 2: Tạo orderId trước, đẩy message vào Kafka
String orderId = UUID.randomUUID().toString();
OrderMessage message = OrderMessage.builder()
.orderId(orderId)
.userId(userId)
.ticketId(ticketId)
.timestamp(System.currentTimeMillis())
.build();
kafkaTemplate.send(FLASH_SALE_TOPIC, ticketId.toString(), message);
// Bước 3: Trả về NGAY LẬP TỨC - không chờ MySQL
return OrderResult.pending(orderId, "Đơn hàng đang được xử lý!");
}
}
Và Consumer xử lý phía sau:
@Component
@RequiredArgsConstructor
@Slf4j
public class FlashSaleOrderConsumer {
private final OrderRepository orderRepository;
private final RedisTemplate<String, Integer> redisTemplate;
private final NotificationService notificationService;
@KafkaListener(topics = "flash-sale-orders", groupId = "flash-sale-consumer")
public void processOrder(OrderMessage message) {
try {
// Ghi vào MySQL - từ từ, không vội
Order order = Order.builder()
.id(message.getOrderId())
.userId(message.getUserId())
.ticketId(message.getTicketId())
.status(OrderStatus.CONFIRMED)
.createdAt(Instant.ofEpochMilli(message.getTimestamp())
.atZone(ZoneId.systemDefault()).toLocalDateTime())
.build();
orderRepository.save(order);
// Thông báo cho user qua WebSocket/Push Notification
notificationService.notifyUser(message.getUserId(),
"Chúc mừng! Đơn hàng " + message.getOrderId() + " đã được xác nhận!");
log.info("Order {} processed successfully", message.getOrderId());
} catch (Exception e) {
log.error("Failed to process order {}", message.getOrderId(), e);
// Hoàn lại stock nếu fail
redisTemplate.opsForValue().increment("flash:stock:" + message.getTicketId());
// Có thể đẩy vào Dead Letter Queue để retry sau
}
}
}
"Throughput?" - David hỏi.
"Dạ, 15.000-20.000 req/s không vấn đề anh. Vì mình chỉ check Redis rồi đẩy message, không chờ gì cả."
Tôi gật đầu: "Nhưng có vấn đề..."
INTERN hiểu ý: "Dạ, đúng rồi anh. Race Condition. Giữa lúc check stock > 0 và lúc decrement, có thể có 10 người khác cũng check thấy còn vé. Cuối cùng trừ xuống âm."
Cách 3: Lua Script - "Cách của người đi trước"
INTERN mỉm cười: "Và đây là cách mà em đã triển khai đêm qua..."
"Vấn đề của cách 1 và 2 là: Check và Trừ không phải atomic. 2 thao tác riêng biệt = có kẽ hở."
"Redis có một vũ khí bí mật: Lua Script. Toàn bộ logic được chạy trong Redis, single-threaded, atomic."
-- flash_sale.lua
-- KEYS[1] = stock key, ví dụ: "flash:stock:123"
-- ARGV[1] = user_id
-- ARGV[2] = order_id
local stock = tonumber(redis.call('GET', KEYS[1]))
-- Không còn vé
if stock == nil or stock <= 0 then
return {0, "SOLD_OUT"}
end
-- Trừ vé - ATOMIC!
redis.call('DECR', KEYS[1])
-- Optional: Lưu thông tin đơn hàng tạm vào Redis
local orderKey = "flash:order:" .. ARGV[2]
redis.call('HSET', orderKey,
'userId', ARGV[1],
'ticketId', KEYS[1],
'status', 'PENDING',
'createdAt', redis.call('TIME')[1]
)
redis.call('EXPIRE', orderKey, 3600) -- TTL 1 giờ
return {1, ARGV[2]}
Và code Java gọi Lua Script:
@Service
@RequiredArgsConstructor
@Slf4j
public class FlashSaleServiceV3 {
private final StringRedisTemplate redisTemplate;
private final KafkaTemplate<String, OrderMessage> kafkaTemplate;
// Load Lua script một lần khi khởi động
private final RedisScript<List> flashSaleScript;
@PostConstruct
public void init() {
// Script được load từ file hoặc define inline
}
/**
* Cách 3: Lua Script + Kafka
* Ưu điểm:
* - Atomic operation (no race condition)
* - Cực nhanh (Redis single-threaded)
* - Throughput cực cao
* Đây là cách được dùng trong các hệ thống Flash Sale thực tế
*/
public OrderResult purchaseTicketWithLua(Long userId, Long ticketId) {
String stockKey = "flash:stock:" + ticketId;
String orderId = UUID.randomUUID().toString();
// Một lần gọi duy nhất - Check + Trừ + Lưu tạm = ATOMIC
List<Object> result = redisTemplate.execute(
flashSaleScript,
Collections.singletonList(stockKey),
userId.toString(),
orderId
);
Integer status = (Integer) result.get(0);
String message = (String) result.get(1);
if (status == 0) {
return OrderResult.fail(message.equals("SOLD_OUT")
? "Hết vé rồi bạn ơi!"
: "Lỗi hệ thống!");
}
// Đẩy vào Kafka để ghi MySQL async
OrderMessage orderMessage = OrderMessage.builder()
.orderId(orderId)
.userId(userId)
.ticketId(ticketId)
.timestamp(System.currentTimeMillis())
.build();
kafkaTemplate.send("flash-sale-orders", ticketId.toString(), orderMessage);
return OrderResult.pending(orderId, "Đặt vé thành công! Đang xác nhận...");
}
}
Cả phòng im lặng.
David lên tiếng: "Vậy throughput...?"
INTERN mở laptop, chạy JMeter test:
10.000 req/s ✓
25.000 req/s ✓
50.000 req/s ✓
75.000 req/s ✓
...
100.000 req/s ✓
"Không một vé nào bị oversell, không một request nào bị race condition."
Vậy cuối cùng, chúng tôi chọn cách nào?
Nếu bạn hỏi tôi: "Anh ơi, vậy em nên dùng cách nào?" - Tôi sẽ trả lời thế này:
"Không có viên đạn bạc. Mỗi cách có chỗ đứng riêng của nó."
| Phương pháp | Throughput | Độ phức tạp | Khi nào tôi sẽ dùng? |
|---|---|---|---|
| Cách 1: Sync MySQL+Redis | ~3.000 req/s | Đơn giản | Khi bạn mới bắt đầu, traffic thấp, cần ship nhanh |
| Cách 2: Async với MQ | ~20.000 req/s | Trung bình | Production thông thường, đủ cho 80% use case |
| Cách 3: Lua Script | ~100.000+ req/s | Cao | Flash Sale, khi "sống chết" phụ thuộc vào performance |
Nhưng với vetautet.com - một hệ thống bán vé tàu Tết có lượng đồng thời cao - thì đáp án chỉ có một: Cách 3 + Kafka. Không có lựa chọn khác.
Kiến trúc tổng thể của vetautet.com Flash Sale
┌─────────────────┐
│ Load Balancer │
└────────┬────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ API - 1 │ │ API - 2 │ │ API - 3 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└────────────────────────┼────────────────────────┘
│
┌──────────────▼──────────────┐
│ Guava Local Cache │
│ (Từ chương trước - 25k/s) │
└──────────────┬──────────────┘
│ Cache Miss
┌──────────────▼──────────────┐
│ Redis Cluster │
│ (Lua Script - Atomic) │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Kafka / RabbitMQ │
│ (Async Processing) │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Consumer Workers │
│ (Ghi MySQL từ từ...) │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ MySQL (Master) │
│ (Source of Truth) │
└─────────────────────────────┘
Bonus: Những "chiêu" mà Senior không nói cho bạn
Nếu bạn đọc đến đây, tôi sẽ thưởng cho bạn vài thủ thuật mà chúng tôi đã học được sau nhiều đêm mất ngủ. Đây không phải lý thuyết đâu, đây là xương máu.
Chiêu 1: Pre-warm Redis - "Hâm nóng" trước khi Flash Sale bắt đầu
Đừng để Flash Sale bắt đầu mà Redis còn lạnh tanh. 5 phút trước giờ G, hãy load sẵn stock vào Redis:
@Scheduled(cron = "0 55 9 * * ?") // 9:55 sáng, trước Flash Sale 10:00
public void preWarmFlashSaleStock() {
List<FlashSaleItem> items = flashSaleRepository.findTodayItems();
items.forEach(item -> {
String key = "flash:stock:" + item.getTicketId();
redisTemplate.opsForValue().set(key, item.getStock());
// Set TTL = thời gian Flash Sale + buffer
redisTemplate.expire(key, Duration.ofHours(2));
});
log.info("Pre-warmed {} flash sale items", items.size());
}
Chiêu 2: Rate Limiting - "Gác cổng" chặn BOT
Như tôi đã nói ở đầu bài về Fraud Ecosystem - BOT sẽ spam hàng nghìn request trong vài giây. Đây là cách chúng tôi "gác cổng":
@Component
public class FlashSaleRateLimiter {
private final RedisTemplate<String, String> redisTemplate;
/**
* Mỗi user chỉ được request 5 lần / 10 giây
*/
public boolean isAllowed(Long userId) {
String key = "rate:flash:" + userId;
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, Duration.ofSeconds(10));
}
return count <= 5;
}
}
Chiêu 3: Sentinel - Biết từ chối sớm còn hơn chết cùng
Khi traffic vượt quá khả năng chịu đựng, thà từ chối sớm còn hơn để cả hệ thống sập. Đây là bài học đau thương mà chúng tôi đã học được:
@Component
public class FlashSaleSentinel {
private final AtomicInteger currentRequests = new AtomicInteger(0);
private static final int MAX_CONCURRENT = 50000;
public boolean tryAcquire() {
int current = currentRequests.incrementAndGet();
if (current > MAX_CONCURRENT) {
currentRequests.decrementAndGet();
return false; // Từ chối sớm, không cho vào
}
return true;
}
public void release() {
currentRequests.decrementAndGet();
}
}
Về đến nhà, tôi vẫn nghĩ...
Tối hôm đó khi về đến nhà, tôi ngồi lặng trước cửa sổ. Bóng đèn đường hắt vào căn phòng nhỏ.
Cậu INTERN đó... mới 3 tháng thôi. Vậy mà...
Có lẽ kiến thức không phân biệt tuổi tác hay số năm kinh nghiệm. Điều quan trọng là đam mê và dám làm.
Cậu ta không ngủ được vì "kiến trúc nhảy múa trong đầu". Cậu ta triển khai ngay trong đêm. Cậu ta không sợ sai.
Có lẽ... đó mới chính là tinh thần của một engineer thực sự.
Chương sau: Khi 10.000 người cùng thanh toán trong 1 giây - Distributed Transaction đã cứu chúng tôi như thế nào?
Lời kết: Từ tôi đến bạn
Nếu bạn đọc đến đây, tôi muốn nói một điều:
"Không có phép màu nào trong engineering cả. Chỉ có sự hiểu biết, kinh nghiệm, và dám thử sai."
Cậu INTERN trong câu chuyện này không phải thiên tài. Cậu ấy chỉ là một người dám thức đêm để code, dám sai để học, và dám hỏi khi không biết.
Bạn cũng có thể làm được như vậy. Tin tôi đi.
Source Code
Toàn bộ code của series này đã được push lên tại: github.com/tips-anonystick/vetautet-ddd
Nếu bạn thấy bài viết hữu ích hãy làm một nhát share cho anh em cùng đánh quái và nâng cấp. Lụm vật phẩm kakaka
Còn nếu bạn có câu hỏi, cứ comment bên dưới hoặc tìm tôi trên Discord. Tôi sẽ cố gắng trả lời khi có thời gian.
Happy coding! 🚀

