Trong quá trình phát triển ứng dụng trên cloud, chúng ta đều từng sử dụng một hình thức khóa (locking) nào đó, hoặc đã từng gặp những vấn đề về kết quả sai lệch trong một số trạng thái khó reproduce. Khi phải quản lý trong cùng một tiến trình, hoặc nhiều tiến trình nhưng nằm trên cùng một máy, mọi thứ không quá phức tạp. Tuy nhiên, hiện nay, phần lớn chúng ta đều làm việc với ứng dụng/dịch vụ cloud-native, nơi có nhiều phiên bản (instance) của cùng một dịch vụ — có thể để đảm bảo tính sẵn sàng cao hoặc cân bằng tải.
Khi có nhiều instance dịch vụ cùng chạy, mọi thứ trở nên phức tạp hơn — đặc biệt khi ta phải đảm bảo rằng một số thao tác nhất định phải được thực hiện đồng bộ, và không chỉ giữa các luồng hoặc tiến trình, mà còn giữa nhiều pod hoặc node trong một môi trường phân tán.
Bài viết này sẽ trình bày chi tiết khía cạnh đó, giúp bạn hiểu rõ các thách thức và lựa chọn được phương án phù hợp. Trọng tâm sẽ nằm ở phần khái niệm, gồm mục đích của locking, các thách thức, lựa chọn hiện có và các yếu tố ảnh hưởng đến quyết định.
Giả định
Giả định rằng người đọc đã hiểu vấn đề race condition và tầm quan trọng của việc xử lý chúng trong những tình huống cụ thể.
Một giả định khác là ứng dụng có nhiều instance chạy trong môi trường phân tán, tạo ra các yêu cầu đồng thời, nơi cần sử dụng khóa (locking) để đảm bảo tác vụ được thực hiện đúng như mong đợi.
Vấn đề
Có hai lý do chính khiến bạn cần cơ chế locking:
1. Hiệu quả (Efficiency)
Trong trường hợp này, bạn muốn ngăn việc nhiều bản sao của cùng một thao tác/công việc chạy song song — mặc dù điều đó không gây ra lỗi, nhưng lại lãng phí tài nguyên, và kết quả cuối cùng cũng không thay đổi.
Ví dụ: có một tác vụ sao chép file từ thư mục A sang thư mục B. Khi nhiều instance cùng chạy, tất cả đều có thể thực hiện thao tác đó đồng thời. Dù điều này không gây sai lệch trạng thái, nhưng là dư thừa vì kết quả cuối cùng vẫn như nhau.
2. Độ chính xác (Correctness)
Trong trường hợp này, nếu cho phép các thao tác đồng thời trên cùng một trạng thái, kết quả có thể sai lệch và phải được ngăn chặn.
Nói cách khác, mọi tình huống mà race condition có thể dẫn đến kết quả sai đều phải được bảo vệ.
Ví dụ: nếu bạn mở hai phiên giao dịch ngân hàng trực tuyến cùng lúc và cố gắng chuyển tiền từ cùng một tài khoản, cần phải có locking để tránh việc kết quả giao dịch bị sai.
Các mức độ khóa
Việc khóa có thể xảy ra ở các mức khác nhau — đôi khi do framework hoặc công nghệ nền tảng tự xử lý, đôi khi cần lập trình viên thực hiện thủ công do logic nghiệp vụ yêu cầu.
Ví dụ:
- Được cơ sở dữ liệu tự động xử lý:
UPDATE employee SET daalchini_balance = daalchini_balance - order_amount WHERE employeeid = xyz;
Trong trường hợp này, ngay cả khi lệnh truy vấn chạy đồng thời, kết quả vẫn đúng vì database tự quản lý cơ chế khóa. - Khóa thủ công (explicit locking):
Giả sử bạn có hệ thống cấp thẻ đỗ xe cho nhân viên, chỉ cấp khi người đó chưa có thẻ và vẫn còn thẻ trống.
Có hai bảng:- Một bảng lưu tổng số thẻ.
- Một bảng lưu thông tin nhân viên, xe, và thẻ đã cấp.
Lúc này, cần có explicit lock để tránh race condition trong bước kiểm tra – đọc và cập nhật dữ liệu (“đã cấp thẻ hay chưa?”).
Ghi chú
- Cơ chế khóa có thể cần ngay cả trong những hệ thống không dùng database — ví dụ trên chỉ để minh họa.
- Việc khóa có chi phí: ảnh hưởng đến độ trễ và thông lượng. Vì vậy:
- Thời gian giữ khóa càng ngắn càng tốt.
- Khóa càng chi tiết (granular) càng tốt. Ví dụ:
- Ưu tiên row-level lock hơn table-level lock.
- Có thể kết hợp: như ví dụ cấp thẻ đỗ xe, khóa theo employeeid ở tầng nghiệp vụ, đồng thời tận dụng lock của database khi cập nhật bản ghi.
- Bài viết không tập trung vào tính nguyên tử (atomicity), nhưng lưu ý rằng tính nguyên tử vẫn phải được duy trì ở nơi cần thiết.
Thách thức
Cung cấp cơ chế khóa trong môi trường phân tán, nơi nhiều instance của dịch vụ chạy trên các node khác nhau (thậm chí khác vùng, khác khu vực), là việc phức tạp hơn nhiều.
Thông tin về khóa cần phải sẵn sàng cao (highly available) và chống chịu lỗi (fault-tolerant).
Cách tiếp cận có thể là:
- Tự xây dựng cơ chế khóa bên trong dịch vụ hoặc tạo dịch vụ khóa chuyên biệt (external locking service).
- Hoặc sử dụng dịch vụ bên thứ ba đã có sẵn trong hệ sinh thái, ví dụ: MySQL, GCP Cloud Storage, Redis, v.v.
Dù chọn cách nào, cơ chế đó vẫn phải đáp ứng đầy đủ yêu cầu của môi trường phân tán.
Đặc trưng cơ bản của hệ thống phân tán (trong ngữ cảnh này):
- Có thể xảy ra độ trễ hoặc gián đoạn mạng.
- Có thể mất dữ liệu do failover (sao chép thường là bất đồng bộ vì hiệu năng).
- Bất kỳ instance nào cũng có thể bị dừng đột ngột vào một thời điểm bất kỳ.
Một số tình huống
- Timeout và instance trễ:
- Instance 1 lấy được khóa từ dịch vụ ngoài nhưng gặp sự cố nên dừng lại.
- Sau một khoảng thời gian, timeout xảy ra, instance 2 nhận được khóa.
- Cả hai cùng thay đổi cùng một trạng thái → dẫn đến kết quả sai.
Nếu chủ sở hữu ban đầu không thể giải phóng khóa đúng hạn, nó phải hoàn thành hoặc hủy bỏ công việc trước khi hết hạn, không được commit sau timeout.
Timeout quá dài lại gây vấn đề ngược lại: khi một instance bị crash, instance khác không thể lấy khóa trong thời gian dài. - Mất thông tin khóa do failover:
- Instance 1 đã lấy khóa.
- Dịch vụ quản lý khóa gặp failover → thông tin khóa chưa kịp sao chép sang node khác.
- Instance 2 có thể vô tình nhận khóa.
- Hai instance thay đổi cùng trạng thái → kết quả sai.
Một số tình huống phổ biến khác:
- At-Least-Once Messaging dẫn tới việc một message được giao lặp lại; nhiều consumer đều xử lý nó như thể nó là duy nhất.
- Tài nguyên toàn cục chỉ hỗ trợ một writer tại một thời điểm, như một file lớn hoặc một endpoint API bên ngoài.
- Scheduled jobs: Nếu bạn lên lịch job trên nhiều node để tăng tin cậy, bạn vẫn chỉ muốn node đầu tiên nắm lock chạy job, tránh trùng lặp.
Lock thường hoạt động thế nào

Yêu cầu Lock
Một node nhận ra nó cần quyền truy cập độc quyền vào một tài nguyên chia sẻ và giao tiếp với lock manager (ví dụ Redis, ZooKeeper, hoặc một cơ sở dữ liệu) có hỗ trợ TTL hoặc ephemeral lock.
Thử lấy Lock (Acquire Attempt)
- Nếu chưa có lock, node sẽ tạo một lock, chẳng hạn bằng cách set một Redis key với TTL, hoặc tạo một ephemeral zNode trong ZooKeeper.
- Nếu node khác đang giữ lock, node này sẽ đợi, thất bại ngay, hoặc retry, tùy chính sách bạn chọn.
Vùng thao tác quan trọng (Critical Operation)
Sau khi lấy được lock, node tiến hành thao tác không được chạy song song — ví dụ cập nhật một dòng DB, ghi một file dùng chung, hoặc gọi một API ngoài có giới hạn.
Giải phóng chuẩn (Standard Release)
Khi hoàn tất, node xóa bản ghi lock (ví dụ xóa Redis key hoặc gọi hàm unlock). Từ thời điểm đó, các contender khác có thể lấy lock.
Sập hoặc giải phóng tự động (Crash or Automatic Release)
Nếu node lỗi hoặc mất session, TTL hết hạn hoặc cơ chế ephemeral phát hiện node vắng mặt. Lock manager loại bỏ lock, hội tụ vào cùng đường đi như release chuẩn để node mới có thể lấy lock. Điều này tránh tình trạng “zombie lock” không bao giờ được giải phóng.
Các lựa chọn khóa phân tán
1. RDBMS
Kiểm tra xem cơ sở dữ liệu của bạn có hỗ trợ lock độc lập với schema/table không.
Tức là, ngay cả khi bạn không dùng database cho lưu trữ dữ liệu, vẫn có thể dùng nó như Distributed Lock Manager (DLM).
Có hai cách chính:
- Row-level locks (ví dụ
SELECT ... FOR UPDATE
) - Advisory locks (như
pg_advisory_lock
hoặcGET_LOCK
)
Chúng phục vụ các nhu cầu khác nhau, nhưng đều cho phép bạn nói: “Tôi muốn truy cập độc quyền vào một thứ gì đó” nhờ DB hiện có.
Row-Level Locks với SELECT ... FOR UPDATE
Bạn có thể khóa các dòng cụ thể:
BEGIN; SELECT * FROM locks WHERE lock_id = @loc_key FOR UPDATE; /* thực hiện thay đổi */ COMMIT;
- Bạn bao lệnh trong transaction (
BEGIN/COMMIT
).FOR UPDATE
bảo DB: “Khóa dòng này để không ai khác sửa cho tới khi tôi xong.” Ai khác muốn sửa dòng ấy sẽ phải đợi. - Lock tồn tại cho tới khi commit hoặc rollback.
- Bạn cũng có thể dùng các dòng “thật” thay vì bảng
locks
riêng nếu vấn đề đồng thời gắn với dòng dữ liệu cụ thể. Với trường hợp khác, bạn cần định nghĩa key đại diện phạm vi lock.
Advisory Locks (như pg_advisory_lock
hoặc GET_LOCK
)
Thay vì gắn lock vào dòng cụ thể, bạn khóa một định danh tùy ý:
-- PostgreSQL SELECT pg_advisory_lock(12345); /* vùng độc quyền */ SELECT pg_advisory_unlock(12345); -- MySQL SELECT GET_LOCK('readModel', 10); /* vùng độc quyền */ SELECT RELEASE_LOCK('readModel');
- Bạn truyền một số nguyên (
12345
) hoặc chuỗi ('readModel'
) để yêu cầu lock từ lock manager nội bộ của DB. Nếu “trống”, bạn nhận lock; nếu đã bị giữ, bạn đợi hoặc thất bại (tùy timeout). - Lock gắn với session: thuộc về phiên DB hiện tại. Khi phiên kết thúc (crash hoặc thoát bình thường), lock tự giải phóng. Nhược điểm: nếu phiên “treo” ở trạng thái lạ, lock có thể tồn tại không mong muốn.
- Những lock này nhẹ hơn row-level (vd PostgreSQL Advisory Locks). Một số yêu cầu truyền integer, bạn có thể cần băm chuỗi thành số (consistent hashing).
Tham khảo:
https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html
2. Redis
Redis cũng có thể được dùng cho distributed locking.
- Nếu dùng cho efficiency, Redis có thể là lựa chọn tốt vì rất tốc độ.
Trong một số trường hợp hiếm khi hai instance cùng nắm giữ khóa, điều này vẫn chấp nhận được. - Nếu dùng cho correctness, nên áp dụng thuật toán Redlock.
Việc hỗ trợ thuật toán này phụ thuộc vào việc bạn dùng Redis tự host hay Redis quản lý (Managed) như trên GCP.
Redis chạy mọi lệnh trong một thread, nên không có nguy cơ hai lệnh “chen ngang” nhau giữa chừng. Nó cung cấp các lệnh atomic để “nhận” một key chỉ khi key chưa tồn tại, và hỗ trợ TTL. Redis cho phép nhiều node phối hợp quanh một không gian key chung. Về bản chất, nếu một node set thành công “lock key”, key đó báo hiệu cho phần còn lại rằng lock đã bị giữ.
Redis xử lý tuần tự tuyệt đối: nếu hai client cùng lúc cố tạo cùng một lock key, hoặc client A thắng hoặc client B thắng — không bao giờ cả hai.
Để tạo lock, bạn tạo “lock key” bằng lệnh Redis, ví dụ:
SET lockKey node123 NX EX 30
Nghe có vẻ bí hiểm, nhưng bản chất là:
- Tạo key nguyên tử với
NX
:NX
nghĩa là “chỉ set nếu key chưa tồn tại”. Redis kiểm tra và tạo key nếu không thấy. Tất cả thực hiện nguyên tử, đảm bảo chỉ một client thành công. - Thời hạn với
EX
:EX
(expire sau X giây) yêu cầu Redis tự xóa key khi hết hạn. Nếu client (chủ lock) crash, tới giờ, Redis xóa key, ngăn lock “kẹt” vĩnh viễn. - Quyền sở hữu lock: Nếu Redis trả
OK
, client được xem là sở hữu lock. Nếu trảnil/rỗng
, đã có client khác giữ. - Gia hạn: Nếu cần thêm thời gian, bạn có thể gia hạn TTL bằng
PEXPIRE lockKey newTTL
, hoặc cẩn trọng dùng lạiSET ... NX EX ...
(khuyến nghị lưu giá trị định danh duy nhất trong key để xác thực chủ lock trước khi gia hạn). - Nhả lock: Khi xong, client xóa key (thường
DEL lockKey
). Rất quan trọng: kiểm tra đúng chủ trước khi xóa (lưu một token duy nhất và so sánh trước khiDEL
). - Xử lý partition: Với single-node Redis, network partition có thể gây rối: chủ lock bị cô lập, vẫn nghĩ rằng mình giữ lock, trong khi phía Redis hết hạn key và client khác lấy lock. Với cluster lớn, xem xét Redlock để phối hợp nhiều node giảm khả năng sở hữu lock xung đột khi mạng tách.
Cách tiếp cận này đủ tốt cho “best effort” lock trong đa số tình huống hằng ngày. Nhưng nếu môi trường dễ partition hoặc cần cam kết nghiêm ngặt, hãy dùng cấu hình/công cụ nâng cao hơn.
Tham khảo:
- Redis Distributed Locks – Correctness
- Implementation code based on Redis distributed lock – Efficiency
- Distributed Lock Implementation with Redis – DZone
3. Google Cloud Storage
Đây là một cách thú vị để tận dụng Google Cloud Storage cho mục đích khóa.
Nếu bạn đang dùng GCP và Cloud Storage trong hệ sinh thái, nhưng không có lựa chọn nào khác như MySQL hay Redis, bạn có thể cân nhắc phương án này.
4. Các lựa chọn khác
Ngoài ra, còn nhiều công cụ khác có thể dùng làm hệ thống quản lý khóa phân tán như:
Zookeeper, Hazelcast, etcd, Hashicorp Consul, v.v.
ZooKeeper / etcd: Những công cụ này giữ dữ liệu nhất quán mạnh trên một nhóm server. Bạn ghi một bản ghi nhỏ “tôi sở hữu lock”, và nếu bạn offline, hệ thống tự nhận ra và xóa bản ghi. ZooKeeper và etcd thường dùng cho phối hợp cluster nâng cao hoặc leader election, nên phức tạp hơn Redis để thiết lập, nhưng đổi lại có bảo đảm nhất quán mạnh.
Cảnh báo
- Locking có chi phí — hãy lựa chọn cẩn trọng dựa trên nhu cầu và phân biệt rõ liệu bạn cần hiệu quả hay độ chính xác.
- Sử dụng khóa càng chi tiết càng tốt, và thời gian giữ khóa càng ngắn càng tốt.
- Nên thực hiện kiểm thử hiệu năng với các yêu cầu đồng thời trên cùng một trạng thái.
Để lại một bình luận
Bạn phải đăng nhập để gửi bình luận.