Concurrency là gì? Hiểu đúng để tránh bug production

1. Vấn đề thực tế
Giả sử bạn có API:
POST /buy
Với logic:
if (stock > 0) {
stock--;
}
Khi có nhiều request đồng thời:
-
nhiều request cùng đọc
stock = 1 -
tất cả đều pass điều kiện
Kết quả: oversell
Đây là một lỗi phổ biến trong backend và là ví dụ điển hình của vấn đề concurrency.
2. Concurrency và Parallelism
Concurrency (đồng thời) là khả năng hệ thống xử lý nhiều task trong cùng một khoảng thời gian.
Parallelism (song song) là việc nhiều task thực sự chạy cùng lúc trên nhiều CPU core.
| Khái niệm | Ý nghĩa |
|---|---|
| Concurrency | nhiều task được xử lý xen kẽ |
| Parallelism | nhiều task chạy cùng lúc thật sự |
Ví dụ:
-
Một CPU xử lý nhiều request bằng cách chuyển qua lại → concurrency
-
Nhiều core xử lý nhiều request cùng lúc → parallelism
Trong backend, hai khái niệm này thường cùng tồn tại.
3. Race Condition
Race condition xảy ra khi nhiều thread cùng truy cập và thay đổi dữ liệu dùng chung, dẫn đến kết quả không xác định.
Ví dụ:
int count = 0;
Thread A: count++;
Thread B: count++;
Kết quả có thể là:
-
1 (sai)
-
2 (đúng)
Nguyên nhân là vì count++ không phải một thao tác đơn lẻ mà gồm nhiều bước (đọc → tính toán → ghi).
4. Atomic
Atomic nghĩa là các biến hoặc thao tác không thể bị chia nhỏ hay gián đoạn bởi các luồng (thread) khác khi đang thực hiện. Nó đảm bảo tính toàn vẹn dữ liệu (thread-safe) mà không cần dùng synchronized nặng nề, thường sử dụng cơ chế CAS (Compare-And-Swap) để cập nhật giá trị an toàn.
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
Thao tác trên là atomic:
-
thực hiện như một bước duy nhất
-
không bị thread khác chen vào
Tuy nhiên, atomic chỉ đảm bảo tính đúng đắn ở mức operation, không đảm bảo logic nghiệp vụ.
5. Thread-safe
Một đoạn code được gọi là thread-safe khi:
nhiều thread chạy đồng thời nhưng kết quả vẫn luôn đúng
Ví dụ không thread-safe
class Counter {
int count = 0;
void increment() {
count++;
}
}
Vấn đề:
-
count++gồm nhiều bước (đọc → tăng → ghi) -
nhiều thread có thể chen vào giữa
→ dẫn đến race condition
Ví dụ thread-safe (cách 1: dùng Atomic)
class Counter {
AtomicInteger count = new AtomicInteger(0);
void increment() {
count.incrementAndGet();
}
}
Ở đây:
-
incrementAndGet()là atomic operation -
nên không bị race condition
→ method trở thành thread-safe
Ví dụ thread-safe (cách 2: dùng lock)
class Counter {
int count = 0;
synchronized void increment() {
count++;
}
}
Ở đây:
-
count++vẫn không atomic -
nhưng được bảo vệ bởi lock
→ không thread nào chen vào
→ vẫn thread-safe
Cách đạt thread-safe
Có nhiều cách để đảm bảo thread-safe, nhưng phổ biến nhất là:
-
Atomic (lock-free)
Sử dụng các operation atomic nhưAtomicInteger, phù hợp với logic đơn giản. -
Lock (synchronized, Lock)
Khóa một đoạn code để đảm bảo chỉ một thread truy cập tại một thời điểm.
Ngoài ra còn có các cách khác như:
-
sử dụng object bất biến (immutability)
-
cô lập dữ liệu theo từng thread (ThreadLocal)
Tránh hiểu nhầm
-
Thread-safe không đồng nghĩa với atomic
-
Atomic chỉ là một kỹ thuật để đạt thread-safe
Note:
“Atomic giải quyết một dòng code. Lock bảo vệ một đoạn code. Nhưng bug thực sự thường nằm ở nhiều đoạn code ghép lại với nhau.”
6. Concurrency-safe ở mức hệ thống
Một nhầm lẫn rất phổ biến là:
Thread-safe = hệ thống an toàn
Thực tế không phải vậy.
Ví dụ
Giả sử bạn có một method:
public void decreaseStock() {
stock.decrementAndGet(); // dùng AtomicInteger
}
Hoặc:
public void decreaseStock() {
synchronized (this) {
stock--;
}
}
Ở đây, method này là thread-safe:
-
không có race condition ở mức biến
stock -
mỗi lần gọi sẽ giảm đúng 1 đơn vị
Nhưng vấn đề nằm ở logic nghiệp vụ
Giả sử API của bạn là:
if (stock > 0) {
decreaseStock();
}
Và hệ thống đang có:
-
stock = 1 -
2 hoặc 1000 request cùng lúc gọi API
Điều gì xảy ra?
-
Nhiều request cùng đọc
stock > 0→ đều thấy đúng -
Tất cả đều đi vào
decreaseStock() -
decreaseStock()chạy thread-safe → mỗi request vẫn giảm 1 lần
👉 Kết quả:
stock = -999
👉 Oversell xảy ra
Tại sao lại sai?
Vì:
-
thread-safe chỉ bảo vệ từng operation riêng lẻ
-
nhưng logic của bạn gồm nhiều bước:
check (stock > 0)
→ then decrease
👉 Đây là 2 bước tách rời, không atomic
Kết luận
Trường hợp này là:
code thread-safe nhưng không concurrency-safe
Hiểu đơn giản
-
Thread-safe → từng dòng code chạy đúng khi nhiều thread cùng truy cập
-
Concurrency-safe → toàn bộ logic nghiệp vụ vẫn đúng khi có nhiều request cùng lúc
Insight quan trọng
Phần lớn bug production không nằm ở chỗ:
-
bạn dùng sai
Atomic -
hay thiếu
synchronized
Mà nằm ở chỗ:
bạn không bảo vệ được toàn bộ flow nghiệp vụ dưới concurrency
7. Ba cấp độ của Concurrency
1. Memory level
Liên quan đến:
-
atomic
-
visibility
-
instruction reordering
Công cụ:
-
volatile -
Atomic -
synchronized
2. Thread level
Liên quan đến:
-
race condition
-
deadlock
Công cụ:
-
lock
-
thread pool
3. System level
Liên quan đến:
-
oversell
-
duplicate request
-
lost update
Công cụ:
-
transaction database
-
distributed lock
-
message queue
8. Các cách xử lý Concurrency
Lock trong code
synchronized void buy() {
// critical section
}
Chỉ hiệu quả trong phạm vi một instance.
Database (atomic update)
UPDATE product
SET stock = stock - 1
WHERE id = 1 AND stock > 0;
Đảm bảo:
-
không oversell
-
tính atomic ở mức database
Distributed Lock
Sử dụng Redis hoặc hệ thống tương tự để:
-
khóa theo resource (ví dụ: productId)
-
đảm bảo chỉ một request xử lý tại một thời điểm
Queue
Xử lý request theo thứ tự:
Request → Queue → Consumer
Phù hợp với hệ thống có tải cao và yêu cầu tính nhất quán cao.
9. Sai lầm phổ biến
-
Cho rằng
synchronizedlà đủ -
Nhầm lẫn thread-safe với đúng logic nghiệp vụ
-
Hiểu atomic là rollback hoặc transaction
10. Kết luận
Concurrency không chỉ là xử lý nhiều request cùng lúc, mà là đảm bảo hệ thống vẫn đúng trong điều kiện đó.
Một hệ thống backend tốt không chỉ chạy nhanh mà còn phải:
-
tránh race condition
-
đảm bảo tính nhất quán dữ liệu
-
xử lý đúng dưới tải cao
Một nguyên tắc quan trọng:
Lỗi concurrency hiếm khi xuất hiện trong môi trường test, nhưng gần như chắc chắn sẽ xảy ra trong production.