Distributed Lock trong Microservices: Tại sao đơn giản nhưng rất dễ làm sai?
Khi Microservices cần một cái Lock: Sự phức tạp ẩn trong Distributed Systems
Khi hệ thống còn là monolith, việc đảm bảo chỉ có một process thực hiện một đoạn logic nào đó khá đơn giản.
Chúng ta có thể dùng:
mutexsynchronized- hoặc chỉ đơn giản là một biến lock trong memory
Nhưng mọi thứ thay đổi khi hệ thống chuyển sang distributed architecture.
Lúc này, cùng một service có thể chạy trên nhiều instance ở nhiều machine khác nhau. Và khi đó, in-memory lock hoàn toàn không còn tác dụng.
Một tình huống rất quen thuộc
Giả sử bạn có một job xử lý:
- refresh cache
- xử lý payment
- update inventory
- hoặc chạy cron job
Service của bạn được deploy với nhiều replicas để scale.
Điều gì có thể xảy ra?
Nhiều instance có thể cùng lúc thực hiện một logic đáng lẽ chỉ nên chạy một lần.
Ví dụ:
- Cron job chạy 5 lần thay vì 1 lần
- Payment bị trừ tiền hai lần
- Inventory bị trừ stock nhiều lần
Đây là một vấn đề concurrency rất phổ biến trong microservices.
Distributed Lock ra đời để giải quyết vấn đề này
Distributed lock giúp đảm bảo rằng chỉ một service instance được phép thao tác trên một resource tại một thời điểm.
Một cách phổ biến để implement distributed lock là sử dụng Redis.
Ví dụ:
SET lock_key token NX PX ttl
Trong đó:
NXđảm bảo key chỉ được set khi chưa tồn tạiPXđặt TTL để tránh deadlock nếu process crash
Nếu instance set key thành công → acquire lock.
Các instance khác có thể:
- retry
- wait
- hoặc fail fast
Redis thường được dùng cho mục đích này vì:
- chạy in-memory nên rất nhanh
- có các atomic operations
- dễ tích hợp trong hệ thống distributed
Nhưng distributed lock không đơn giản như vẻ ngoài
Trong môi trường production, distributed lock có khá nhiều edge cases.
Process bị pause
Giả sử một service acquire lock với TTL là 10 giây.
Sau đó xảy ra:
- GC pause
- container bị CPU throttling
- hoặc OS scheduling delay
Process bị pause 15 giây.
Trong thời gian đó:
- lock đã expire
- instance khác acquire lock
Khi process ban đầu resume:
Hai instance đều nghĩ rằng mình đang giữ lock. Logic quan trọng có thể chạy hai lần.
Network latency hoặc network partition
Distributed systems luôn phải đối mặt với:
- network delay
- packet loss
- partial failure
Những tình huống này đôi khi khiến nhiều node cùng nghĩ rằng mình đang giữ lock.
Redis failover
Nếu Redis master crash trước khi lock được replicate sang replica:
- replica có thể được promote thành master
- lock có thể bị mất
- Một client khác có thể acquire lại lock.
Mutual exclusion có thể bị phá vỡ.
Những thứ dev thường phải xử lý khi tự implement distributed lock
Trong thực tế, một distributed lock đúng nghĩa thường cần thêm nhiều thứ:
- retry khi acquire lock
- TTL để tránh deadlock
- đảm bảo chỉ owner mới được release lock
- extend lock nếu job chạy lâu
- xử lý crash hoặc network delay
Chỉ riêng việc release lock an toàn cũng cần dùng Lua script để đảm bảo atomic check.
Vì vậy mình đã viết một thư viện nhỏ
Sau khi gặp pattern này nhiều lần khi làm việc với microservices, mình đã viết một thư viện nhỏ cho Node.js:
Mục tiêu rất đơn giản: một distributed lock nhẹ, dễ dùng, dựa trên Redis.
Thư viện cung cấp:
- Redis-based distributed lock
- token-based safe release bằng Lua script
- retry mechanism khi acquire lock
- auto-extend lock cho các job chạy lâu
- helper
using()để chạy critical section một cách an toàn
Ví dụ:
await lock.using("order:123", async () => {
await processOrder()
})
Hoặc cách chi tiết hơn:
const token = await lock.acquire("order:123", {
ttl: 5000,
retryCount: 10,
retryDelay: 200
})
if (!token) {
throw new Error("Failed to acquire lock")
}
try {
await processOrder()
} finally {
await lock.release("order:123", token)
}
Điều này đảm bảo rằng chỉ một instance của service xử lý order tại một thời điểm.
Nếu bạn đang làm việc với Node.js microservices và cần một giải pháp distributed lock đơn giản, bạn có thể thử:
👉 https://www.npmjs.com/package/simple-distributed-lock
Mọi feedback hoặc góp ý đều rất welcome.
All rights reserved