[Microservices Series] Bài 15: Caching phân tán với Redis – Tấm khiên thép bảo vệ Database mùa cao điểm
Chào anh em, mình đây! Ở bài 14, chúng ta đã có Prometheus và Grafana làm "tai mắt" để báo động mỗi khi hệ thống có dấu hiệu quá tải. Nhưng biết là một chuyện, còn chống đỡ được hay không lại là chuyện khác.
Trong kiến trúc Microservices, bạn có thể dễ dàng cấu hình Kubernetes tự động nhân bản (scale up) Service Bán Vé từ 5 lên 50 containers để hứng bão traffic. Nhưng ác mộng nằm ở chỗ: Cả 50 cái containers đó đều chọc chung vào một con Database (PostgreSQL/MySQL). App Server thì phình to được, chứ con Database rập khuôn quan hệ (Relational DB) thì việc scale là cực kỳ khó khăn và tốn kém.
Kết quả là App chưa kịp chết thì Database đã "hít khói" và sập nguồn vì quá tải Connection. Để giải bài toán điểm nghẽn (Bottleneck) chí tử này, chúng ta cần một tấm khiên thép chặn trước Database: Distributed Caching với Redis.
1. Tại sao phải là "Phân tán" (Distributed)?
Nhiều anh em code Laravel quen tay hay dùng cache lưu ra file, hoặc lưu thẳng vào RAM của server (array cache, APCu). Trong Monolith chạy trên 1 server, cách này rất ổn. Nhưng ở Microservices, nó sinh ra 2 thảm họa:
-
Dữ liệu "ông nói gà, bà nói vịt": Bạn có 10 containers Service Bán Vé. Container số 1 vừa update giá vé trong RAM của nó, nhưng 9 containers kia không biết gì cả. Khách hit vào container 1 thì thấy giá mới, hit vào container 2 lại thấy giá cũ.
-
Mất trắng khi Restart: Container trên Kubernetes có thể bị kill và start lại bất cứ lúc nào. Mỗi lần như vậy, Cache trong RAM bốc hơi, hàng ngàn request lại lao thẳng vào Database để build lại Cache, gây ra hiện tượng sập DB cục bộ.
Đó là lý do chúng ta phải tách Cache ra thành một cụm server riêng biệt. Redis (hoặc Memcached) đóng vai trò là một kho chứa dữ liệu trên RAM dùng chung cho toàn bộ hệ thống. Dù bạn có 10 hay 100 containers, tất cả đều phải gọi mạng (network call) sang Redis để lấy Cache. Dữ liệu luôn đồng nhất 100%.
2. Các chiến thuật Caching "Quốc dân"
Đừng bạ đâu cũng nhét vào Cache. Chúng ta chỉ cache những dữ liệu Đọc nhiều, ít thay đổi (Heavy Read, Low Write). Có 2 chiến thuật phổ biến nhất:
Chiến thuật 1: Cache-Aside (Lazy Loading) Đây là pattern kinh điển nhất.
-
Luồng đọc: Service gọi Redis tìm dữ liệu. Nếu có (Cache Hit) -> Trả về luôn. Nếu không có (Cache Miss) -> Service tự chọc xuống Database lấy dữ liệu, sau đó lưu một bản copy lên Redis, rồi mới trả về cho User.
-
Luồng ghi: Khi có người update dữ liệu vào Database, Service bắt buộc phải gửi lệnh xóa (Invalidate/Delete) cái key tương ứng trên Redis.
Chiến thuật 2: Write-Through Dùng khi dữ liệu quan trọng, bắt buộc Cache và DB phải luôn giống hệt nhau từng mili-giây.
- Khi có request Update, Service không ghi thẳng vào DB nữa. Nó ghi vào Redis trước, sau đó một luồng đồng bộ sẽ ghi từ Redis xuống DB. Cách này code phức tạp hơn nhưng đảm bảo an toàn tuyệt đối về mặt nhất quán dữ liệu.
3. Thực chiến "giải cứu" Database tại hệ thống AFC
Trong hệ thống điều hành Metro Line 1, có một bảng dữ liệu gọi là Bảng Cước Phí (Fare Table). Nó quy định đi từ Ga A đến Ga B tốn bao nhiêu tiền, người già được giảm bao nhiêu.
Mỗi ngày có hàng triệu lượt hành khách quẹt thẻ. Đồng nghĩa với việc con Database phải chịu hàng triệu câu query tính tiền. Nhưng ngặt nỗi, Bảng cước phí này... cả năm mới thay đổi một lần theo quyết định của Thành phố. Việc bắt DB tính đi tính lại một phép tính cố định hàng triệu lần là tội ác.
Giải pháp: Mình dùng Redis làm Distributed Cache cho toàn bộ cụm Gate Service (viết bằng Go).
-
Khi Gate đầu tiên cần tính tiền từ Ga Suối Tiên đến Ga Bến Thành, nó mò xuống DB lấy kết quả (ví dụ: 15.000đ), rồi lưu lên Redis với key:
fare:suoitien:benthanhkèm thời gian sống (TTL) là 24 tiếng. -
999.999 lượt khách tiếp theo quẹt thẻ đi cùng lộ trình đó, cụm Gate Service chỉ việc chọc vào Redis đọc ra con số 15.000đ với tốc độ dưới 1 milisecond.
-
Con PostgreSQL tĩnh lặng như tờ, tài nguyên CPU và RAM được giải phóng hoàn toàn để phục vụ cho các giao dịch nạp/trừ tiền (Write) cực kỳ quan trọng khác.
4. Những "Nỗi đau" xương máu khi xài Cache
Nếu không cẩn thận, Redis từ "thiên thần" sẽ biến thành "ác quỷ". Dưới đây là 2 hố bom anh em hay giẫm phải nhất:
Hiệu ứng bầy đàn (Cache Stampede / Thundering Herd) Bạn set TTL cho key Bảng Cước Phí là 24h. Đúng lúc 7h sáng giờ cao điểm, key này hết hạn (Expire) và bị xóa khỏi Redis. Ngay lập tức, 10.000 request từ các cổng soát vé ập tới, cùng báo Cache Miss. Cả 10.000 request này cùng lúc lao thẳng như một mũi giáo đâm lủng con Database để lấy dữ liệu build lại Cache. DB "hộc máu" và sập ngay lập tức.
- Cách giải quyết: Dùng cơ chế Distributed Lock (Mutex). Khi Cache Miss, chỉ cho phép duy nhất 1 request được đi xuống DB lấy dữ liệu. 9.999 request còn lại bị lock, bắt đứng chờ 50ms rồi quay lại Redis check lại, lúc này request đầu tiên đã mang data về đắp lên Cache rồi.
Quên set TTL (Làm đầy RAM) RAM của Redis đắt đỏ hơn ổ cứng HDD/SSD rất nhiều. Nhiều anh em code quên set tham số TTL (Time-To-Live), khiến dữ liệu nằm chết dí trên RAM mãi mãi. Đến một ngày Redis cạn kiệt bộ nhớ (OOM - Out of Memory), nó sẽ bắt đầu xóa loạn xạ hoặc từ chối phục vụ, kéo sập toàn bộ hệ thống. Quy tắc vàng: Mọi key trên Redis ĐỀU PHẢI CÓ TTL.
Tạm kết
Với Redis, anh em đã bọc cho Database một lớp khiên thép chống đạn cực kỳ vững chắc, sẵn sàng đón nhận những đợt bão traffic mùa cao điểm.
Đến đây, các dữ liệu dạng Text và số liệu (Database/Cache) đã được xử lý êm đẹp. Nhưng hệ thống của chúng ta không chỉ có chữ. Sẽ ra sao nếu hành khách cần upload hình ảnh CCCD để đăng ký làm thẻ tháng? Hàng chục triệu bức ảnh, file PDF báo cáo, file CSV dung lượng lớn sẽ được lưu ở đâu?
Chắc chắn bạn không thể lưu ảnh vào trong container Docker (bị mất khi restart), cũng không thể ném vào Database (làm chậm DB).
Ở bài tiếp theo (Bài 16), chúng ta sẽ rời khỏi các máy chủ nội bộ để khám phá Object Storage (S3/MinIO) – Thoát khỏi cái bóng của thư mục public/uploads truyền thống.
Dự án hiện tại của anh em có đang dùng Cache không? Đã bao giờ anh em nếm mùi cay đắng khi quên xóa Cache lúc update dữ liệu khiến khách hàng gào thét chưa? Cùng kể khổ dưới comment nhé!
All rights reserved