← Bài trước | Series hub | Tiếp theo →
Chương 2: 3 Điểm Yếu Tử Huyệt Nhất Của Cache
Caching là tấm khiên phòng ngự tối thượng bảo vệ các cơ sở dữ liệu trong hệ thống phân tán. Tuy nhiên, nếu triển khai cẩu thả, cache lại chính là nguyên nhân trực tiếp kéo sập toàn bộ hệ thống của bạn. Trong chương này, chúng ta sẽ mổ xẻ ba hiện tượng kinh điển về caching và cách chống lại chúng bằng Golang.
1. Cache Penetration (Xuyên Thủng Cache)
Answer-first: Cache penetration (xuyên thủng cache) xảy ra khi những kẻ tấn công liên tục truy vấn các ID không hề tồn tại, bỏ qua tầng cache một cách mượt mà. Hãy ngăn chặn bằng cách cache cả các giá trị NULL hoặc áp dụng Bloom Filters ngay ở tầng memory (bộ nhớ).
Cơ Chế: Một kẻ tấn công hay một lỗi logic (logic bug) nào đó điên cuồng dội request vào các ID không có thật (ví dụ: ID = -1 hoặc một mã UUID ngẫu nhiên). Chính vì dữ liệu hoàn toàn không tồn tại dưới Database, nó KHÔNG BAO GIỜ được ghi lên Cache. Hậu quả là, mọi request độc hại cứ thế “xuyên thủng” tầng cache và đâm thẳng vào DB. Ở ngưỡng 10.000 RPS, Database sẽ nhanh chóng cạn kiệt connection pool và đột tử.
Giải Pháp:
- Cache Các Giá Trị Null: Nếu DB trả về rỗng, hãy ép Redis lưu một giá trị
NULLhoặcNot_Foundvới một khoảng TTL ngắn (ví dụ: 60 giây). Những request tiếp theo sẽ lập tức bị chặn đứng tại cửa Redis. - Bloom Filters: Áp dụng Bloom Filter để xác minh xem liệu một ID “có khả năng tồn tại hay không” với mức tiêu tốn bộ nhớ gần như bằng 0. Nếu Bloom Filter phán KHÔNG, từ chối request ngay tắp lự mà khỏi tốn công chọc xuống mạng.
2. Cache Avalanche (Tuyết Lở Cache)
Answer-first: Cache Avalanche (hiệu ứng tuyết lở) diễn ra khi một khối lượng khổng lồ các keys cùng rủ nhau hết hạn trong một thời điểm, trút một cơn bão query xuống đầu DB. Hãy ngăn nó lại bằng cách cộng thêm một khoảng xê dịch ngẫu nhiên (random jitter offset) vào mốc thời gian TTL.
Cơ Chế: Hiện tượng này bộc phát khi một mẻ lớn các Cache Keys hết hạn (expires) tại cùng một phần nghìn giây. Lấy ví dụ, nếu bạn reset điểm thưởng khách hàng lúc nửa đêm và set TTL là 1 tiếng cho 1 triệu người dùng, thì đúng chính xác lúc 1:00 AM, cả 1 triệu keys sẽ cùng lúc bốc hơi. Mọi request đổ vào hệ thống lúc 1:00:01 AM sẽ ồ ạt đi xuyên qua màng cache rỗng tuếch, tụ lại thành một trận “tuyết lở” nghiền nát database.
Giải Pháp:
- TTL Jittering (Làm Rung Lắc TTL): Đừng bao giờ chốt cứng (hardcode) một thời lượng TTL chính xác. Luôn luôn cộng thêm một khoảng thời gian xê xích ngẫu nhiên (Jitter). Thay vì thiết lập chẵn 60 phút, hãy rải ngẫu nhiên TTL nằm trong khoảng từ
55cho tới65phút.
3. Cache Breakdown (Sập Nguồn Cache / Thundering Herd)
Answer-first: Cache Breakdown chực chờ bùng nổ khi duy nhất một “Hot Key” (cực kỳ đắt khách) đột ngột hết hạn, khiến hàng ngàn request đồng thời ào ạt query xuống DB. Thư viện singleflight của Golang sẽ gom nhóm hàng đống requests y chang nhau này gộp lại thành đúng một truy vấn DB duy nhất.
Cơ Chế: Trong khi Avalanche là chuyện 1 triệu keys thông thường rủ nhau bốc hơi, thì Breakdown lại là câu chuyện về 1 siêu Hot Key (ví dụ: món hàng đang Flash Sale) đột tử hết hạn. Vào đúng tíc tắc cái TTL đó nhảy về 0, có 100.000 người dùng đang đè nút refresh tải lại trang. Chạm mặt Cache Miss, cả 100.000 cái requests húc nhau tạo thành một đàn voi dữ (stampede) phi thẳng xuống database đòi nạp lại cache. DB sẽ bốc hỏa tức thì.
Giải Pháp: Golang singleflight
Đây chính là thanh gươm báu của các Go Developers. Package golang.org/x/sync/singleflight trao cho bạn quyền năng thu gọn hàng chục ngàn requests trùng lặp ép thành một lần thực thi duy nhất.
Lúc có 100.000 requests gào thét đòi tra cứu “Sản phẩm A”, singleflight sẽ ra tay:
- Giao nhiệm vụ truy vấn DB cho Goroutine chạy nhanh nhất (đứng đầu).
- Chặn họng 99.999 Goroutines còn lại, bắt tất cả phải đứng xếp hàng chờ.
- Khi Goroutine đầu đàn lấy được dữ liệu về, nó tự mang kết quả phân phát (chia sẻ RAM) trực tiếp cho 99.999 anh em đang chầu chực.
Chỉ đúng 1 truy vấn DB thực tế được chạy, thay vì 100.000!
import "golang.org/x/sync/singleflight"
var requestGroup singleflight.Group
func GetProductInfo(id string) (*Product, error) {
if prod := getFromCache(id); prod != nil {
return prod, nil
}
// Dùng singleflight để dập tắt đàn voi dữ Thundering Herd
v, err, _ := requestGroup.Do(id, func() (interface{}, error) {
product, dbErr := fetchFromDB(id)
if dbErr == nil {
setToCache(id, product, 60*time.Minute)
}
return product, dbErr
})
if err != nil {
return nil, err
}
return v.(*Product), nil
}
Bằng cách hợp bích singleflight (hàng phòng ngự cấp node) chung với Redis Distributed Locks (hàng phòng ngự cấp cluster), bạn đã dựng lên một pháo đài bất khả xâm phạm bảo vệ database của mình.