Cache::lock Là Gì? Tuyệt Chiêu "Khóa" Concurrency Ở Tầng Caching Siêu Nhẹ Nhàng
Chào anh em Viblo! 👋
Trong mấy bài viết trước, chúng ta đã bàn nát nước về Database Locks – thứ vũ khí tối thượng để bảo vệ toàn vẹn dữ liệu dưới DB. Thế nhưng, có một hôm cậu em Junior trong team chạy lại hỏi mình:
"Anh ơi, em làm tính năng bấm nút 'Nhận quà điểm danh'. Khách hàng bấm nhanh quá hoặc dùng tool click một lúc 10 phát, hệ thống gửi yêu cầu song song. Nếu em dùng Database Lock thì nặng quá, mà lỡ hệ thống gọi sang một API bên thứ ba để trừ quà thì DB Lock cũng đâu có khóa cái API đó được?"
Đây là một case kinh điển của bài toán Concurrency (Bất đồng bộ/Đồng thời) ở tầng Application chứ không chỉ riêng tầng Database. Và câu trả lời chuẩn bài nhất cho cậu em lúc đó chính là: Cache::lock (Atomic Lock / Distributed Lock).
Hôm nay, hãy cùng mình tìm hiểu xem cơ chế "khóa" siêu nhẹ này hoạt động như thế nào và tại sao nó lại là cứu cánh cho các hệ thống High Traffic nhé!
1. Cache::lock là gì?
Nếu Database Lock dùng chính công cụ của hệ quản trị CSDL để khóa dòng/khóa bảng, thì Cache::lock lợi dụng tốc độ bàn thờ và tính chất Single-threaded (Đơn luồng) của các hệ thống Caching (phổ biến nhất là Redis) để tạo ra một cái khoá tượng trưng.
Để dễ hình dung, hãy tưởng tượng ứng dụng của bạn là một văn phòng.
- Có một căn phòng họp đặc biệt (tính năng nhạy cảm như Trừ tiền, Nhận quà).
- Trước cửa phòng họp có một cái rổ chứa duy nhất một chiếc chìa khóa (nằm trên Redis).
- Khi Request A đến, nó nhanh tay thò vào rổ lấy chìa khóa đi vào phòng họp (Cache::lock()->get()).
- Khi Request B, C đến sau, thò tay vào rổ thấy trống trơn. B và C hiểu rằng: "Có ông đang làm rồi, mình đi ra ngoài báo lỗi hoặc đứng đợi thôi".
- Khi Request A xử lý xong, nó trả chìa khóa lại vào rổ (Cache::lock()->release()).
2. Sự khác biệt giữa DB Lock và Cache Lock
Tại sao không dùng DB Lock cho mọi thứ mà phải sinh ra Cache Lock làm gì? Hãy nhìn bảng so sánh dưới đây:
| Tiêu chí | Database Lock (Row/Table Lock) | Cache Lock (Atomic Lock) |
|---|---|---|
| Nơi quản lý | Dưới RDBMS (MySQL, PostgreSQL...) | Trên bộ nhớ RAM (Redis, Memcached) |
| Tốc độ | Chậm hơn (tốn chi phí I/O đĩa, giữ kết nối DB) | Cực kỳ nhanh (đọc/ghi trên RAM chỉ mất vài mili-giây) |
| Phạm vi bảo vệ | Chỉ bảo vệ được dữ liệu trong DB. | Bảo vệ được mọi logic code (gửi email, gọi API ngoài, chạy Job...) |
| Chi phí hệ thống | Cao. Giữ Lock lâu dễ gây nghẽn cổ chai DB. | Rất thấp. Giảm tải tối đa cho Database. |
3. Các kịch bản thực chiến "Chỉ có thể là Cache Lock"
Kịch bản 1: Chống Double-Click / Tránh lặp Request User mua hàng, do mạng lag nên họ tức giận bấm nút "Thanh Toán" liên tục 5 lần. Nếu code không xử lý, 5 request này sẽ chạy song song.
Dùng Cache Lock: Ngay khi request 1 vào, ta tạo một cái lock trên Redis với key là lock_payment_user_1 trong vòng 10 giây. 4 request sau vào thấy key này đã tồn tại, lập tức trả về lỗi: "Hệ thống đang xử lý, vui lòng không bấm lại".
Kịch bản 2: Ngăn các Background Job chạy chồng lấn nhau (Overlapping) Bạn cấu hình một cái Cronjob cứ 5 phút chạy một lần để quét và gửi email cho 1 triệu khách hàng. Bình thường Job chạy mất 3 phút (không sao). Nhưng hôm nay hệ thống mail bị chậm, Job 1 chạy mất tận 7 phút. Đến phút thứ 5, Job 2 lại được hệ thống kích hoạt. Hai Job chạy song song sẽ quét trùng dữ liệu và gửi email 2 lần cho khách hàng!
Giải pháp: Bọc logic Job trong một Cache::lock('send_email_job', 600). Job 2 bật lên thấy Job 1 vẫn đang giữ lock thì tự động kết thúc luôn, không chạy đè lên nhau nữa.
4. Những "vết sẹo" cần tránh khi chơi với Cache Lock
Nghe thì ngon ăn, nhưng dùng Cache Lock mà non tay là ăn hành ngay lập tức. Anh em cần nằm lòng 3 lưu ý xương máu sau:
1. Luôn luôn phải có TTL (Time To Live / Thời gian hết hạn)
Khi bạn xin khóa, bạn bắt buộc phải đặt thời gian hết hạn cho nó (ví dụ: khoá trong 5 giây).
Tại sao? Tưởng tượng Request A lấy được chìa khóa, đang chạy nửa chừng thì server bị sập (Crash) hoặc dính lỗi ngoại lệ (Exception) chết đột tử khi chưa kịp chạy lệnh release() trả khóa. Nếu không có TTL, chiếc chìa khóa đó sẽ biến mất vĩnh viễn trong rổ, tính năng đó sẽ bị nghẽn mãi mãi (Deadlock). Có TTL thì sau 5 giây khóa tự rã, hệ thống tự phục hồi.
2. Thời gian Lock phải lớn hơn thời gian xử lý Logic
Nếu bạn lock trong 2 giây, nhưng đoạn code bên trong của bạn gọi API ngoài mất tận 3 giây mới xong. Ở giây thứ 2.1, lock đã tự rã trên Redis, một request khác nhảy vào lấy được lock và tiếp tục chạy, trong khi request cũ vẫn chưa xong. Thế là toang! Khóa như không khóa.
3. Cẩn thận với hệ thống Distributed (Phân tán nhiều cụm Redis)
Nếu hệ thống của bạn siêu lớn, dùng một cụm Redis Cluster gồm nhiều node. Cơ chế set lock thông thường có thể bị lỗi đồng bộ giữa các node (Node A nhận lock nhưng chưa kịp đồng bộ sang Node B thì Node A sập). Đối với những case tài chính cực kỳ nghiêm ngặt trên hệ thống phân tán, hãy tìm hiểu thuật toán Redlock để đảm bảo an toàn tuyệt đối.
Đúc kết kinh nghiệm
Cache::lock là một giải pháp thanh thoát, tinh tế và cực kỳ mạnh mẽ để xử lý Concurrency ở tầng ứng dụng, giúp "chặn đứng" những request thừa thãi trước khi chúng kịp mò xuống làm khổ Database.
Hãy dùng DB Lock khi bạn cần bảo vệ tính toàn vẹn dữ liệu nghiêm ngặt tại tầng lưu trữ. Và hãy dùng Cache Lock khi bạn cần bảo vệ hiệu năng hệ thống và các logic nghiệp vụ phi Database.
Chúc anh em ứng dụng thành công và có những đêm ngon giấc không bị gọi vì bug lặp dữ liệu! Happy Coding! 🚀💻
All Rights Reserved