Tuần trước team mình có một sự cố khá đau đầu: hệ thống order bị duplicate — cùng một đơn hàng nhưng bị trừ stock hai lần. Sau khi trace log, nguyên nhân là hai service instance xử lý cùng một request trong vòng chưa đến 50ms, cả hai đều đọc số tồn kho trước khi bên kia kịp ghi xuống DB. Classic race condition trong môi trường microservices.
Giải pháp mình chọn cuối cùng là Redis Distributed Lock — đơn giản, hiệu quả, và đã chạy ổn định từ đó đến nay. Bài này mình ghi lại cách implement để anh em trong team tham khảo.
Race Condition trong Microservices trông như thế nào?
Hãy hình dung bạn có API endpoint /api/orders/create và load balancer đang phân phối request lên 3 instance. Một khách hàng double-click vào nút “Đặt hàng” — hai request gần như đồng thời hit hai instance khác nhau. Cả hai bắt đầu chạy logic kiểm tra tồn kho trong cùng một khoảnh khắc:
# Instance A và Instance B đều chạy đoạn này gần như cùng lúc
stock = db.query("SELECT stock FROM products WHERE id = ?", product_id)
if stock > 0:
db.execute("UPDATE products SET stock = stock - 1 WHERE id = ?", product_id)
create_order(product_id, user_id)
Instance A đọc được stock = 1, Instance B cũng đọc được stock = 1. Cả hai thấy “còn hàng”, cả hai tạo order. Kết quả: stock = -1 và khách hàng nhận 2 xác nhận đơn hàng cho 1 sản phẩm duy nhất.
Single-server? Database transaction + row locking giải quyết được. Nhưng với nhiều service, nhiều instance, chạy trên nhiều server — bạn cần cơ chế khóa ở tầng cao hơn. Đó là Distributed Lock.
Redis Distributed Lock hoạt động như thế nào?
Trước khi xử lý critical operation, service phải “giành” được một lock key trên Redis. Giành được thì xử lý, xong thì release. Không giành được — vì instance khác đang giữ — thì chờ hoặc retry. Nghe đơn giản vậy thôi, nhưng cái hay là Redis đảm bảo bước giành lock này hoàn toàn atomic.
Redis cung cấp lệnh SET NX PX để thực hiện điều này:
# SET key value NX PX milliseconds
# NX = chỉ set nếu key chưa tồn tại
# PX = TTL tính bằng milliseconds
SET order:lock:product-123 "unique-token-abc" NX PX 5000
# Trả về OK nếu giành được lock
# Trả về nil nếu key đã tồn tại (service khác đang giữ)
Lệnh này atomic — dù có 100 instance gọi cùng lúc, chỉ đúng 1 instance SET thành công. Đây là điểm khác biệt then chốt so với cách check-then-set bằng hai lệnh riêng: khoảng trống giữa hai lệnh đó chính là nơi race condition chui vào.
TTL không phải tùy chọn — đây là thứ ngăn hệ thống bị deadlock vĩnh viễn. Nếu service crash ngay sau khi giành được lock mà chưa kịp release, Redis tự xóa lock sau khoảng thời gian đặt trước. Không có TTL, một service chết có thể làm tê liệt cả pipeline.
Implement Redis Distributed Lock với Python
Cài đặt
pip install redis
Class RedisLock
import redis
import uuid
import time
class RedisLock:
def __init__(self, redis_client: redis.Redis, lock_key: str, ttl: int = 5000):
self.redis = redis_client
self.lock_key = lock_key
self.ttl = ttl # milliseconds
self.token = str(uuid.uuid4()) # unique token mỗi instance lock
def acquire(self, retry: int = 3, retry_delay: float = 0.1) -> bool:
for attempt in range(retry):
result = self.redis.set(
self.lock_key,
self.token,
nx=True,
px=self.ttl
)
if result:
return True
if attempt < retry - 1:
time.sleep(retry_delay)
return False
def release(self):
# Lua script đảm bảo chỉ release lock của chính mình
lua_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
self.redis.eval(lua_script, 1, self.lock_key, self.token)
def __enter__(self):
if not self.acquire():
raise RuntimeError(f"Không thể lấy lock: {self.lock_key}")
return self
def __exit__(self, *args):
self.release()
Release lock mình dùng Lua script thay vì gọi thẳng DEL vì một lý do cụ thể: nếu lock vừa expire đúng lúc bạn chuẩn bị xóa, và instance khác đã giành được lock mới, bạn không được phép xóa của người ta. Script kiểm tra token trước khi xóa — atomic, không có khoảng hở để race condition chen vào.
Áp dụng vào xử lý Order
import redis
redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
def create_order(product_id: str, user_id: str):
lock_key = f"order:lock:{product_id}"
with RedisLock(redis_client, lock_key, ttl=10000) as lock:
# Chỉ một service được chạy đoạn này tại một thời điểm
stock = db.query("SELECT stock FROM products WHERE id = ?", product_id)
if stock <= 0:
raise Exception("Hết hàng")
db.execute(
"UPDATE products SET stock = stock - 1 WHERE id = ?",
product_id
)
order_id = db.insert("INSERT INTO orders ...")
return order_id
# Lock tự động được release khi ra khỏi with block
Xử lý khi không giành được lock
from fastapi import HTTPException
def create_order_endpoint(product_id: str, user_id: str):
lock_key = f"order:lock:{product_id}"
lock = RedisLock(redis_client, lock_key, ttl=10000)
if not lock.acquire(retry=5, retry_delay=0.2):
raise HTTPException(
status_code=429,
detail="Hệ thống đang xử lý request khác, vui lòng thử lại sau"
)
try:
result = process_order(product_id, user_id)
return result
finally:
lock.release() # Luôn release dù có exception hay không
Những điểm cần chú ý khi dùng thực tế
1. Đặt TTL phù hợp
TTL quá ngắn → lock expire trước khi xử lý xong → race condition quay lại.
TTL quá dài → nếu service crash, các service khác phải ngồi chờ mòn mỏi.
Công thức mình đang dùng: TTL = thời gian xử lý ước tính × 3, tối thiểu 3 giây. Không chắc thời gian xử lý? Log duration trong 1–2 ngày, lấy p99 rồi nhân 3. Tránh đặt bằng tay theo cảm tính.
2. Chọn lock key đủ granular
Lock key quá rộng tạo bottleneck không cần thiết — 1000 sản phẩm khác nhau mà chỉ có 1 lock key global thì mỗi lúc chỉ xử lý được đúng 1 order:
# Quá rộng — block tất cả order
lock_key = "order:lock:global"
# Vừa đủ — chỉ block order cho product cụ thể
lock_key = f"order:lock:product:{product_id}"
# Granular hơn nếu business logic cho phép
lock_key = f"order:lock:product:{product_id}:warehouse:{warehouse_id}"
3. Kết hợp với Idempotency Key
Distributed lock chặn race condition, nhưng không chặn được duplicate khi client retry sau timeout. Hai cơ chế này bổ trợ nhau — dùng cả hai nếu hệ thống cần idempotency thực sự.
4. Monitoring lock trên Redis
# Xem tất cả lock key đang active
redis-cli KEYS "order:lock:*"
# Xem TTL còn lại của một lock
redis-cli TTL "order:lock:product-123"
# Xem giá trị (token) của lock
redis-cli GET "order:lock:product-123"
Khi debug, mình hay format và kiểm tra JSON payload trước khi set vào Redis tại toolcraft.app/vi/tools/developer/json-formatter — tiện hơn nhiều so với cài extension, không cần đăng ký, mở là dùng được ngay.
Kết luận
Sau vụ duplicate order hồi tuần trước, mình dùng Redis Distributed Lock và không thấy sự cố tương tự từ đó. Không phải vì nó là giải pháp hoàn hảo, mà vì nó đủ đơn giản để hiểu, đủ đáng tin để deploy production, và không cần thêm infrastructure nếu Redis đã có trong stack.
Bài học lớn nhất: đừng assume database transaction là đủ khi có nhiều service chạy song song. Concurrency trong môi trường distributed khác hẳn single-server — bạn phải nghĩ đến nó ở nhiều tầng. Redis Lock là cách nhanh nhất để bịt một trong những lỗ hổng phổ biến nhất.
Đang dùng Node.js? Xem thêm thư viện redlock. Go thì có go-redis với tính năng tương đương — cùng nguyên lý SET NX PX và Lua script release, chỉ khác syntax.

