0

[System Design Version 1 - Bài 8] Nghệ thuật Caching: Redis/Memcached. Chiến lược Cache và những "cơn ác mộng" Penetration, Breakdown, Avalanche

Chào anh em, nếu được hỏi làm sao để hệ thống chạy nhanh hơn, 99% các lập trình viên sẽ buột miệng thốt ra câu thần chú: "Dùng Cache đi!".

Đúng vậy, so với việc phải chui xuống ổ SSD để lục lọi dữ liệu tốn hàng chục mili-giây, việc lấy data trực tiếp từ RAM (bộ nhớ trong) qua Redis hay Memcached chỉ tốn chưa tới 1 mili-giây. Cache giống như liều doping chích thẳng vào hệ thống, giúp nó phản hồi nhanh như chớp.

Nhưng... nếu Cache dễ xài như vậy thì senior và junior đã không hưởng mức lương khác nhau. Caching không chỉ là việc set(key, value) rồi get(key). Dùng Cache mà không hiểu chiến lược, bạn sẽ tự tay chôn sống hệ thống của mình trong những đợt tải cao.

Hôm nay, chúng ta sẽ lật tẩy nghệ thuật Caching và những cơn ác mộng mang tên Penetration, Breakdown và Avalanche.

1. Cuộc chiến vương quyền: Redis vs. Memcached

Trước khi đi sâu vào chiến lược, hãy điểm qua hai gương mặt vàng trong làng Caching.

  • Memcached: Kẻ tiên phong. Cực kỳ đơn giản, siêu nhanh, thuần túy lưu trữ Key-Value trên RAM. Cúp điện là bay sạch data.
  • Redis: Vị vua hiện tại. Không chỉ lưu Key-Value, Redis hỗ trợ đủ loại cấu trúc dữ liệu (List, Set, Hash, Sorted Set...). Hơn thế nữa, Redis có cơ chế snapshot (lưu xuống đĩa cứng) để phục hồi data khi sập nguồn, và hỗ trợ Cluster cực tốt.

Trong 90% các dự án hiện đại, Redis là sự lựa chọn mặc định.

2. Hai chiến lược Cache kinh điển (Caching Strategies)

Khi hệ thống có cả Database và Cache, luồng dữ liệu sẽ đi như thế nào?

A. Cache-Aside (Lazy Loading) - Chiến lược "Lười biếng"

Đây là pattern phổ biến nhất, phù hợp cho các hệ thống đọc nhiều hơn ghi (Read-heavy).

  • Luồng Đọc: Ứng dụng hỏi Cache trước. Nếu có (Cache Hit), trả về luôn. Nếu không có (Cache Miss), ứng dụng tự chạy xuống Database lấy data, sau đó nhét ngược lại vào Cache, rồi mới trả về cho user.
  • Luồng Ghi: Ứng dụng ghi trực tiếp vào Database, sau đó xóa (Invalidate) key tương ứng trong Cache. Lần đọc tiếp theo sẽ tự động xảy ra Cache Miss và nạp data mới.

Ví dụ: Khi hành khách quẹt thẻ qua cổng Gate của ga Metro, hệ thống sẽ check số dư thẻ trong Redis trước. Nếu chưa có, nó mới gọi API về DB trung tâm để lấy số dư, mở cổng, rồi lưu tạm số dư đó vào Redis để chặng về khách quẹt thẻ sẽ nhanh hơn.

B. Write-Through - Chiến lược "Song hành"

Phù hợp khi dữ liệu bắt buộc phải luôn luôn đồng bộ giữa Cache và DB.

  • Luồng Ghi: Ứng dụng ghi thẳng vào Cache. Sau đó, Cache (hoặc một lớp middleware) lập tức đồng bộ cục data đó xuống Database rồi mới báo thành công.
  • Ưu điểm: Data không bao giờ bị "thiu" (stale data).
  • Nhược điểm: Tốc độ ghi bị chậm đi do phải đợi cả thao tác ghi RAM và ghi Disk hoàn tất.

3. Ba cơn ác mộng "Nhức nhối" khi dùng Cache

Gắn Redis vào không có nghĩa là kê cao gối ngủ. Khi traffic bùng nổ, ba thảm họa sau đây sẽ rình rập hệ thống của bạn.

Cơn ác mộng 1: Cache Penetration (Xuyên thủng Cache)

  • Hiện tượng: Một gã hacker hoặc một đoạn code lỗi liên tục query vào một ID không hề tồn tại (ví dụ: truy vấn ID thẻ -1 hoặc sản phẩm ID = 999999999).
  • Hậu quả: Vì ID không tồn tại, Cache luôn luôn Miss. Hệ thống tiếp tục phi thẳng xuống Database query. Database cũng không có nên không có gì để lưu lại lên Cache. Cứ thế, hàng vạn request "xuyên thủng" qua màng lọc Cache và nã đạn trực tiếp vào Database, làm DB sập nguồn.
  • Cách giải quyết: 1. Cache Null Value: Nếu DB trả về null, hãy lưu chính cái null đó vào Redis với một TTL (Time-to-live) ngắn (ví dụ 1 phút). Lần sau gọi ID ảo đó, Redis sẽ chặn lại ngay.
  1. Bloom Filter: Một thuật toán cấu trúc dữ liệu cực kỳ lợi hại, giúp xác định nhanh: "Key này CHẮC CHẮN KHÔNG tồn tại, hoặc CÓ THỂ tồn tại". Lọc ngay request ảo từ vòng gửi xe.

Cơn ác mộng 2: Cache Breakdown (Vỡ Cache tại một điểm)

  • Hiện tượng: Hệ thống của bạn đang có một đợt Flash Sale mỹ phẩm cực khủng. Một thỏi son (Hot Key) đang được hàng trăm ngàn người truy cập cùng lúc. Bất thình lình, Key này trong Redis bị hết hạn (Expired/TTL).
  • Hậu quả: Tại đúng tích tắc key đó bốc hơi, hàng trăm ngàn request đồng loạt rủ nhau Cache Miss. Tất cả chúng cùng một lúc đâm sầm xuống Database để build lại Cache. Database "đột tử" ngay lập tức vì quá tải (CPU 100%).
  • Cách giải quyết: 1. Mutex Lock (Khóa đồng bộ): Khi xảy ra Cache Miss, chỉ cho phép DUY NHẤT một request được phép chạy xuống Database. Các request khác phải đợi vài mili-giây. Khi request đầu tiên build xong Cache, các request sau chỉ việc lấy từ Cache ra.
  1. Tự động gia hạn ngầm (Logical Expiry): Không set TTL cho key đó trên Redis, mà lưu thêm một trường expire_time trong value. Ứng dụng check thấy sắp hết hạn sẽ trả về data cũ cho user, đồng thời âm thầm ném một job vào Background để cập nhật lại data mới từ DB.

Cơn ác mộng 3: Cache Avalanche (Bão tuyết / Sạt lở Cache)

  • Hiện tượng: Rất nhiều key trong Redis được thiết lập cùng một thời gian sống (ví dụ: lúc 0h đêm hằng ngày, batch job nạp một đống data và set TTL = 1 tiếng). Đến đúng 1h sáng, HÀNG LOẠT key đồng loạt hết hạn. Hoặc thảm họa hơn: Server Redis bị sập.
  • Hậu quả: Lớp khiên bảo vệ bị thủng lỗ chỗ hoặc biến mất hoàn toàn. Toàn bộ traffic của hàng ngàn tính năng đổ ụp xuống Database như một trận sạt lở tuyết.
  • Cách giải quyết:
  1. Thêm Jitter (Độ lệch ngẫu nhiên) vào TTL: Thay vì set mọi key sống đúng 60 phút, hãy set thành 60 phút + random(1, 5) phút. Các key sẽ rụng lác đác chứ không rụng cùng lúc.
  2. High Availability (HA) cho Redis: Chạy Redis Cluster hoặc Sentinel để nếu Master chết, Slave tự động lên thay, đảm bảo Cache không bao giờ "sập toàn tập".

Lời kết

Cache là một con dao pha tuyệt vời nhưng đòi hỏi người kỹ sư phải cầm đằng chuôi. Khi bạn nắm vững những khái niệm trên, bạn sẽ không còn sợ những đêm thức trắng debug hệ thống nữa.

Nhưng anh em thử nghĩ xem, kể cả khi dùng Cache cực kỳ hoàn hảo, Database của chúng ta vẫn phải lưu trữ dữ liệu gốc. Nếu lượng dữ liệu đó phình to đến mức một con server vật lý mạnh nhất thế giới cũng không chứa nổi thì sao?

👉 Ở bài tiếp theo, chúng ta sẽ bắt tay vào việc "băm vằm" Database: "Scale Database: Replication (Master-Slave), Sharding (Phân mảnh dữ liệu) và những cạm bẫy khi chia nhỏ database."

Anh em đã từng bị sập server vì cái tội "Cache Miss" chưa? Hãy kể lại câu chuyện của mình dưới phần comment nhé. Nhớ upvote để ủng hộ series này!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.