Nội dung bài viết
Video học lập trình mỗi ngày
Từng tham gia dự án với lượng users tranh chấp nhau rất cao thì đương nhiên cái đầu tiên chính là tuyến phỏng thủ với hystrix + sentinel. Lúc này lượng requets đổ về api giảm đi đáng kể.
Tiếp đến là tuyến phòng thủ thử hai đó chính là các cơ chế xác định user chân chính (Người hay Ma...).
Khuyến nghị: JAVA DDD (30): Cancel Order là quá trình ngược lại với việc PlaceOrder nhưng nó phức tạp hơn...
Thứ ba chính là vấn đề cốt lõi đó là kiểm tra số lượng cổ phiếu còn hay không? Mà cách này phổ biến nhất thì có thể sử dụng LUA or LOCK DISTRIBUTED. Có nghĩa là trước khi bán cổ phiếu trong Oracle thì hãy xác định cổ này còn trong Cached hay không?
Mà số lượng Users như quân nguyên thì việc check thể nào đảm bảo ba điều sau.
Hệ thống ổn (Do các tuyền phòng thủ đã nói).
Chính là bán cho đúng (tính nhất quán CUỐI CÙNG, nhớ là cuối cùng - Vì sao các hệ thống cổ phiếu lại có tổng hợp cuối ngày..)
Hiệu suất mua phải CAO. Chứ mua chậm thì dẹp mịa nó đi.
Vậy thì sẽ có 3 cách để phát triển tối ưu MUA này. Mỗi cách sau sẽ tiên tiến hơn cách trước. Đầu tiên là CAS... Nhưng tạm đây đã, vì hãy nói về LUA + LOCK.
Tôi đã vẽ Architecture. Còn dự án mô phỏng cũng đã có trong GO, JAVA, NESTJS và NODEJS (Tất cả có GIT TẠI ĐÂY).
Xin mời các bạn check. Quan trọng là phải setup được SENTINEL + CLUSTER. Điều này mới quan trọng, không nên đọc LÝ THUYẾT quá nhiều và đừng lệ thuộc vào AI.
Tôi đã xây dựng những sections thực hành xây dựng Redis Sentinel + Cluster + LUA từ level thấp đến cao trong Series này: Cached - Redis Sentinel + Cluster + LUA bạn có thể đọc lại nếu muốn.
1. Bản chất của hai cơ chế
1.1 Redis Lua Script — Atomic CAS
Redis là single-threaded event loop. Khi một Lua script đang thực thi, toàn bộ Redis engine bị block — không có command nào khác chen vào được. Đây không phải lock theo nghĩa truyền thống, mà là atomic execution by design.
Thread A gửi Lua script
Thread B gửi Lua script
Thread C gửi SET key value
│
▼
Redis event loop (single-threaded)
┌──────────────────────────────────────┐
│ Queue: [A_lua] [B_lua] [C_set] │
│ │
│ Xử lý tuần tự, mỗi item atomic: │
│ 1. A_lua chạy xong (GET+SET atom.) │
│ 2. B_lua chạy xong │
│ 3. C_set chạy xong │
└──────────────────────────────────────┘
│
Không có race condition.
Không có thread nào bị block chờ.
Mỗi op xong trong ~0.2ms rồi nhường.
1.2 Redisson Distributed Lock — SETNX + TTL
Distributed lock dùng Redis như một coordination point, nhưng bản chất là client-side locking:
Thread A: SETNX lock_key "A" EX 5 → OK, A giữ lock
Thread B: SETNX lock_key "B" EX 5 → FAIL (key tồn tại)
Thread C: SETNX lock_key "C" EX 5 → FAIL
Thread B, C: subscribe vào keyspace event hoặc poll/sleep
→ CHỜ đến khi A DEL lock_key hoặc TTL hết
→ Sau đó race lại với SETNX
Thread A: xử lý xong → DEL lock_key
Thread B: thắng SETNX → tiếp tục
Thread C: thua → chờ tiếp
Thread B và C thực sự bị block (park virtual thread, hoặc spin-wait) trong thời gian A giữ lock. Nếu A giữ lock 5 giây, B và C chờ 5 giây.
2. Kiến trúc so sánh với kịch bản stock deduction

2.1 Cách hiện tại — Lua Script (CAS/MQ)
500 requests đồng thời
│
▼
┌─────────────────────────────────────────────────┐
│ Redis Event Loop │
│ │
│ [req_1 lua] → GET TICKET:23:STOCK (=5000) │
│ → SET TICKET:23:STOCK 4999 │
│ → return 1 ✓ (~0.2ms) │
│ │
│ [req_2 lua] → GET TICKET:23:STOCK (=4999) │
│ → SET TICKET:23:STOCK 4998 │
│ → return 1 ✓ (~0.2ms) │
│ ... │
│ [req_5000 lua] → GET = 1 → SET = 0 → return 1 │
│ │
│ [req_5001 lua] → GET = 0 → return 0 ✗ │
│ [req_5002 lua] → GET = 0 → return 0 ✗ │
│ ... (95,000 requests còn lại trả về ngay) │
└─────────────────────────────────────────────────┘
Tổng thời gian Redis xử lý 100K requests:
100,000 × 0.2ms = 20 giây (worst case nếu tuần tự hoàn toàn)
Thực tế Redis ~100K ops/s → ~1 giây
Không có thread nào bị block chờ thread khác.
2.2 Giả sử thêm Distributed Lock vào CAS
500 requests đồng thời
│
▼
Tất cả cùng gọi: redisDistributedService.getDistributedLock("STOCK_LOCK:23")
lock.tryLock(wait=1s, hold=5s)
┌─────────────────────────────────────────────────────────────┐
│ Request_1: SETNX STOCK_LOCK:23 → OK, giữ lock │
│ Request_2: SETNX STOCK_LOCK:23 → FAIL → CHỜ (up to 1s) │
│ Request_3: SETNX STOCK_LOCK:23 → FAIL → CHỜ (up to 1s) │
│ ... │
│ Request_500: SETNX → FAIL → CHỜ (up to 1s) │
└─────────────────────────────────────────────────────────────┘
│
│ Request_1 xử lý: Lua(0.2ms) + MySQL TX(20ms) = 20.2ms
│ DEL STOCK_LOCK:23
│
▼
┌────────────────────────────────────────────────────────────┐
│ 499 requests race SETNX → Request_7 thắng │
│ 498 requests còn lại chờ tiếp │
└────────────────────────────────────────────────────────────┘
Throughput: 1 request / 20ms = 50 requests/giây
Thời gian xử lý 500 requests: 500 / 50 = 10 giây
Requests bị timeout (tryLock wait=1s):
Chỉ 1s/20ms = 50 requests kịp qua trong 1 giây đầu
→ 450 requests bị tryLock return false → fail ngay
→ Chỉ ~50 requests được xử lý, 450 bị từ chối!
2.3 Điều gì xảy ra cụ thể khi CAS thêm Lock
// Giả sử thêm distributed lock vào placeOrderCAS:
public PlaceOrderResponse placeOrderCAS(Long ticketId, int quantity) {
RedisDistributedLocker lock =
redisDistributedService.getDistributedLock("STOCK_LOCK:" + ticketId);
boolean isLocked = lock.tryLock(1, 5, TimeUnit.SECONDS); // wait 1s, hold 5s
if (!isLocked) {
return PlaceOrderResponse.failed("BUSY", "Hệ thống đang xử lý, thử lại sau");
}
try {
// ... Lua + MySQL ...
} finally {
lock.unlock();
}
}
Section thứ 31 tôi sẽ đưa ra một kịch bản 500 users đồng thời spam button BUYTICKET. Với 100K requests đồng thời và hãy phân tích case này? Nó rất đáng để tìm hiểu với một Senior....

