Điều kiện tiên quyết: Đây là Phần 3 của Khóa Học System Design. Hãy nghiền ngẫm Phần 2: Cân Bằng Tải L4/L7 để hiểu thấu cái mớ bòng bong tầng traffic trước khi nhảy cắm đầu xuống cái vực thẳm caching.
Answer-first: Đỉnh cao của nghệ thuật xài bùa (caching strategy selection) phụ thuộc vào hai chữ: bạn chịu đựng được mức sai số đồng bộ (consistency window) bao lâu và cái nết đọc/ghi (read/write access pattern) của cái đống dữ liệu đó thế nào. Ghi-Xuyên-Thấu (Write-Through) là đo ni đóng giày cho mớ sổ sách tiền bạc; Ghi-Lùi-Sau (Write-Behind) lại là chân ái của đám đếm view đếm click hay gom rác phân tích (analytics); Ghi-Vào-Rồi-Đọc (Cache-Aside) thì nghiễm nhiên là trùm cuối (default) cho mấy cái API suốt ngày ngửa cổ chờ đọc dồn dập.
Cái Hội Chứng Đàn Voi Giẫm Đạp (Cache Stampede) Xảy Ra Như Nào?
Answer-first: Hội Chứng Voi Giẫm Đạp (Cache Stampede - hay thundering herd) nổ tung khi một cái chìa khóa (key) đang độ cực hot bỗng dưng hết hạn (expires) và cả một bầy đàn goroutines hùng hậu cùng lúc hụt chân phát hiện ra cái cache đã bốc hơi (cache miss) — ngay tắp lự, nguyên cái bầy đó quay xe giẫm đạp nhau lao vào vắt kiệt cục cơ sở dữ liệu (query the database simultaneously). Cơn đại hồng thủy của hàng ngàn cái câu truy vấn y xì đúc này đủ sức đánh sập hoàn toàn (exceed) cái sức chứa của bể kết nối (connection pool capacity) rồi kéo theo một chuỗi sụp đổ domino (cascading failure).
Giải Phẫu Cơn Ác Mộng (Anatomy of the Problem)
sequenceDiagram
participant G1 as Goroutine 1
participant G2 as Goroutine 2
participant G3 as Goroutine 3
participant Cache as Redis Cache
participant DB as PostgreSQL
G1->>Cache: Đọc product:123 (GET)
Cache-->>G1: MISS (Trắng tay)
G2->>Cache: Đọc product:123
Cache-->>G2: MISS
G3->>Cache: Đọc product:123
Cache-->>G3: MISS
Note over G1,DB: ❌ Nguyên bầy 3 con goroutines đồng loạt lao vào cắn xé DB
G1->>DB: Lôi cổ hết FROM products WHERE id=123 (SELECT *)
G2->>DB: Lôi cổ hết FROM products WHERE id=123
G3->>DB: Lôi cổ hết FROM products WHERE id=123
Note over DB: Thằng DB khóc thét ôm hận gánh N câu truy vấn sinh đôi đè chung 1 cái key
Đang giữa cơn cuồng phong Flash Sale của Shopee, một món hàng siêu hot chỉ cần lọt lưới (cache-missing) chóc một phát là đủ kéo theo hàng vạn con goroutines → nã hàng vạn cú truy vấn vào DB → DB sùi bọt mép tạch luôn (crashes).
Thuốc Giải 1: Đánh Chặn Trùng Lặp Chuyến Bay (Singleflight - In-Process Deduplication)
Gói golang.org/x/sync/singleflight đứng ra bảo kê (ensures) luật giang hồ: đối với chung một cái key, thì chỉ có đúng duy nhất một thằng hàm (function) được phép thò mặt ra chạy; đám lính tò te râu ria theo sau phải xếp hàng đứng hóng (block) rồi chực chờ bú ké kết quả của thằng đi đầu (share that result):
package cache
import (
"context"
"errors"
"fmt"
"time"
"golang.org/x/sync/singleflight"
)
type ProductCacheService struct {
sfGroup singleflight.Group
store map[string]string // Ra production thì tráo cái này bằng Redis client nghen
}
// GetOrFetch mổ cò chặn đứng đám DB calls trùng lặp bằng singleflight + bọc thép chống ứ đọng timeout (timeout protection)
func (c *ProductCacheService) GetOrFetch(
ctx context.Context,
productID string,
timeout time.Duration,
fetchDB func() (string, error),
) (string, error) {
cacheKey := fmt.Sprintf("product:%s", productID)
// Lối đi tắt (Fast path): bốc trúng cache hit
if val, ok := c.store[cacheKey]; ok {
return val, nil
}
// Đường gồ ghề (Slow path): chặn lách trùng lặp gọi DB bằng singleflight
// Thằng DoChan này chả thèm ngáng đường (non-blocking) — nó chẳng rảnh để ghì cổ giam cái OS thread của con goroutine lại đâu
resultChan := c.sfGroup.DoChan(cacheKey, func() (interface{}, error) {
return fetchDB()
})
// Màn đua tốc độ tử thần giữa cục context timeout với con DB lề mề
select {
case <-ctx.Done():
return "", fmt.Errorf("cắt điện rút ống context (context cancelled): %w", ctx.Err())
case <-time.After(timeout):
return "", errors.New("chờ DB lòi con mắt tới hết giờ (database fetch timeout exceeded)")
case result := <-resultChan:
if result.Err != nil {
return "", result.Err
}
data := result.Val.(string)
c.store[cacheKey] = data
// Nếu result.Shared == true tức là con goroutine hóng hớt này đang mút ké mồ hôi nước mắt của thằng khác (sharing another's result)
return data, nil
}
}
[!NOTE] Cái
result.Sharednó lòi ratruechừng nào con goroutine ăn bám (current goroutine) đưa tay hứng ké (received a result) mớ đồ ăn nhả ra từ mồm một thằng chạy trước (another goroutine’s execution) (chớ hông phải tự nó è cổ ra chạy). Lôi mớ của nợ này ra mà ghi sổ đo lường hiệu suất chặn (deduplication metrics): coi xem được nhiêu % mớ request xài chung vs bao nhiêu phần trăm là vác mặt đi làm thật.
[!WARNING] Giới hạn lãnh địa của Singleflight: Đòn triệt hạ trùng lặp này chỉ phát huy uy lực nội trong cùng một luồng tiến trình (within a single process). Một khi rải rác đẻ ra cả đám replicas (multiple replicas), từng con pod vẫn điếc lác thân ai nấy lo gửi y nguyên nháy DB query độc lập. Phải kẹp chung với ổ khóa Redis
SETNXlock hoặc mớ bùa XFetch thì mới bọc lót chéo cánh (cross-process protection) được.
Ghi-Xuyên-Thấu (Write-Through) vs Ghi-Lùi-Sau (Write-Behind) — Ai Mới Là Chân Ái?
Answer-first: Trò Ghi-Xuyên-Thấu (Write-Through) dập liên thanh (writes synchronously) đè cổ cùng lúc cả cache lẫn DB nội trong một cái request — đảm bảo đồng bộ đét (strong consistency) nhưng trả cái giá đắt lòi mắt về độ rề rà khi ghi (higher write latency). Trò Ghi-Lùi-Sau (Write-Behind) thì quăng đại đồ vô cache xài tạm trước, rồi từ từ thong thả tuồn hàng (flushes) vào DB ở tít đằng xa (asynchronously) — chạy ghi lướt như gió vèo vèo, ngặt nỗi mang tiếng đem con bỏ chợ ôm nguy cơ bốc hơi dữ liệu (risk of data loss) nếu khốn nạn thay cái node cache đột tử (crashes) ngỏm củ tỏi trước khi hốt hàng xong (flush completes).
Mổ Xẻ Sạch Sành Sanh 5 Thế Võ Caching
| Thế Võ (Pattern) | Luồng Cắm Đồ (Write Flow) | Luồng Bốc Đồ (Read Flow) | Tính Nhất Quán (Consistency) | Đợi Ghi (Write Latency) | Trắng Tay Mất Trắng (Data Loss Risk) | Đồ Nuôi Khách VIP (Best For) |
|---|---|---|---|---|---|---|
| Kẹp-Hông (Cache-Aside) | App → Phang thẳng DB | App → Cache → Đứt thì bốc DB (on miss) | Sẽ Đều (Eventual) | Thấp (Low) | Thấp (Low) | Mặc định xài (Default) — trùm gánh API đói đọc (read-heavy) |
| Đọc-Xuyên (Read-Through) | App → Phang thẳng DB | Cache tự giác mót đồ (auto-fetches) khi hụt (miss) | Sẽ Đều (Eventual) | Thấp (Low) | Thấp (Low) | Đút túi ORM/đám library |
| Ghi-Xuyên (Write-Through) | App → Cache → DB (dập cùng lúc - sync) | App → Cache | Chuẩn đét (Strong) | Cao (High) | Gần như chẳng (Very Low) | Sổ xố, tính tiền, hóa đơn (Financial records) |
| Ghi-Sau (Write-Behind) | App → Cache → Trễ chuyến DB (async buffer) | App → Cache | Sẽ Đều (Eventual) | Nhanh vèo vèo (Very Low) | Bay Lỗ Mũi (High) | Đo view, thống kê, rác sự kiện (Analytics, view counts) |
| Ghi-Lách (Write-Around) | App → DB (làm lơ bỏ rơi - bypassing cache) | App → Cache → DB | Chuẩn đét (Strong) | Thấp (Low) | Thấp (Low) | Cái thứ đồ nhét kho ghi chôn giấu (Write-once data) |
Khui Hộp Chiêu Kẹp-Hông (Cache-Aside Implementation - trò lố phổ biến nhất)
package cache
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
type ProductRepository struct {
rdb *redis.Client
db interface{ QueryProduct(id string) (*Product, error) }
ttl time.Duration
}
// GetProduct — Xuất chiêu Cache-Aside
func (r *ProductRepository) GetProduct(ctx context.Context, id string) (*Product, error) {
key := fmt.Sprintf("product:%s", id)
// 1. Dòm ngó mò vô cache trước
cached, err := r.rdb.Get(ctx, key).Result()
if err == nil {
var product Product
if unmarshalErr := json.Unmarshal([]byte(cached), &product); unmarshalErr == nil {
return &product, nil // Đớp trúng (Cache HIT)
}
}
// 2. Chết hụt (Cache MISS) — Mò xuống đè đầu DB
product, err := r.db.QueryProduct(id)
if err != nil {
return nil, fmt.Errorf("cục DB ói mửa (database error): %w", err)
}
// 3. Tống hàng vô nhồi lại cache kèm hạn chót (TTL)
data, _ := json.Marshal(product)
r.rdb.Set(ctx, key, data, r.ttl)
return product, nil
}
// UpdateProduct — phế vứt luôn cache (invalidate) khi nhét hàng vô (an toàn hơn ba cái trò đi sửa lại cache update)
func (r *ProductRepository) UpdateProduct(ctx context.Context, product *Product) error {
key := fmt.Sprintf("product:%s", product.ID)
if err := r.db.(interface{ UpdateProduct(p *Product) error }).UpdateProduct(product); err != nil {
return fmt.Errorf("ghi DB lết xác (db write failed): %w", err)
}
// Thẳng tay xẻo xóa luôn key chớ chẳng thèm sửa chữa gì (Delete key instead of updating) — né được trò cọ quẹt giẫm đạp đua ghi (write race condition)
r.rdb.Del(ctx, key)
return nil
}
[!TIP] Giết chớ hông cứu (Invalidate, don’t update): Sau khi quăng vô lò ghi (write operation), phang chết (delete) mẹ cái cục cache key thay vì nắn nót đi bôi vẽ gọt tỉa (writing the new value). Lỡ thằng DB gật đầu trơn tru mà cái nháy nhét cache bị trượt ngã, thì 2 nguồn tin (two sources) sẽ đá nhau bôm bốp (diverge). Rút củi đáy nồi (Deletion) là đường lùi an toàn nhất — đường cùng rủi ro bét nhất (worst case) cũng chỉ ăn thêm chóc 1 cái hụt chân (cache miss) xách giỏ chui xuống DB đòi lại thôi.
Mổ Ruột Redis LRU vs LFU (Redis LRU vs LFU Internals)
Answer-first: Redis thực ra chả bợ cái LRU đồ xịn (true LRU) lẫn LFU đồ xịn nào cả — tụi nó rặt là hàng rỏm ước lượng bốc xác suất (probabilistic approximations) xài trò rải rác lấy mẫu (sampling) đặng giật giật cái ngai vàng dọn chỗ đuổi khách O(1) (O(1) eviction). LRU ngửi mùi mớ đồ mới cầm nhặt lên vuốt ve gần đây (recency-based workloads); còn LFU vác súng bảo vệ mớ hàng đua đòi cháy khét (hot keys) khỏi cái màn đá chóp nhốt vào sọt rác lúc nhồi nhét giữa bão săn sale flash sale.
Redis LRU — Bốc Thăm Rải Rác Bằng Xác Suất (Probabilistic Sampling)
Redis lười nhớp chẳng chịu đội cái gánh nặng nuôi báo cô 1 mớ xâu chuỗi hai đầu đồ sộ (doubly-linked list) cho cái LRU mác xịn. Bù lại, mỗi lúc cháy kho chật chỗ đòi phải đá chóp ai đó (eviction is needed):
- Hốt trọn ổ bốc thăm lôi đầu ra $N$ cái chìa khóa mốc khô bất kỳ (default
maxmemory-samples = 5). - Sút văng ra bãi rác (Evict) cái key nào xui xẻo ngậm cái thời gian đóng bụi (idle time) (thời gian từ lúc chó ngó tới gần nhất) to đầu nhất.
- Cái đồng hồ đo bụi (Idle time) này bị nhét rúc trong cái bụng trường Redis Object clock (24-bit, độ chi tiết 1 giây).
Nhổng cái thông số maxmemory-samples tới mốc 10–20 làm mắt tụi nó tỏ thêm tí (improves accuracy) nhưng bắt phải móc bóp nộp sưu trả phí cống nạp CPU tuyến tính (linear CPU cost).
Redis LFU — Con Đếm Morris (Morris Counter - Trò Nhồi Logarit)
LFU mang vào mâm cái Con Đếm Morris (Morris Counter) — một cái máy đếm ma giáo bùa chú xác suất (probabilistic counter) bóp mồm bóp miệng tiết kiệm RAM bằng cách chẳng thèm đếm từng cắc từng hào (exact counts):
$$P_{\text{increment}} = \frac{1}{(\text{bộ_đếm} - \text{LFU_INIT_VAL}) \times \text{lfu_log_factor} + 1}$$
- Con đếm chỉ thèm nhón nhích tăng lên theo xác suất tuột dốc (decreasing probability) cắm đầu (chạy trượt dài theo thang đo logarit - logarithmic scaling).
- Mớ con đếm cũng bị hụt hơi rụng lả tả xìu xuống qua năm tháng (decays over time - bóp mũi bằng cái nút
lfu-decay-time).
# redis.conf — độ cấu hình đỉnh chóp của LFU (optimal LFU) dọn cỗ cho trận Flash Sale sập sàn
maxmemory-policy allkeys-lfu # Tròng cái vòng kim cô LFU lên đầu TOÀN BỘ đám khóa (all keys)
lfu-log-factor 10 # Độ ngông của Logarithm (Logarithm base) — số càng khủng = độ tinh ranh phán đoán tần suất chót vót càng đanh đá
lfu-decay-time 1 # Trói cổ tụt điểm (Decay counter) sau N phút đóng mạng nhện (minutes idle)
| Tiêu Chí So Kèo (Criterion) | LRU | LFU |
|---|---|---|
| Đá chóp dựa theo (Evict based on) | Kẻ bị ghẻ lạnh bỏ quên lâu nhất (Least recently accessed) | Kẻ bị hắt hủi vắng mặt nhất (Least frequently accessed) |
| Che chở mớ khóa cháy hàng (Hot key protection) | Có khả năng vác dao chém lầm nếu lỡ đứt quãng sờ vắng (briefly not accessed) | Vững như bàn thạch nhở mớ khiên tần suất dày đặc (high frequency count) |
| Gương mặt mới ra mắt (New key behavior) | Hàng vương giả chễm chệ an tọa che chắn bao vây (Protected - high recency) | Yếu nhớt mỏng manh (Vulnerable - bộ đếm tần suất lẹt đẹt rúc = 0) |
| Chỗ chơi đúng bài (Best workload) | Nồi lẩu thập cẩm (General-purpose), chọc tuồn tuần tự (sequential access) | Gánh xiếc hàng đua nghẹt thở (Hot-key workloads - Flash Sale) |
| Cấu Hình (Config) | allkeys-lru | allkeys-lfu |
Tuyệt Kỹ XFetch Algorithm — Trò Ăn Gian Làm Mới Sớm (Probabilistic Early Expiration)
Answer-first: XFetch nổ tung chôn vùi cái mả hội chứng cache stampede bằng cách nhón gót đâm lén, cởi mở (allowing) tuồn hàng mớm rỉa chạy lụi background tính toán (background pre-computation) rào trước đón sau ngay trước thềm mốc hết hạn (expiry), cộng sinh với cái xác suất bốc thăm ngẫu hứng nhảy dựng liên hồi một khi cái vòng kim cô TTL gần rớt thảm thiết cạn đáy. Chả có bố con ổ khóa nào hết (No locking), chả thèm ới gọi vác tù và hàng xóm cãi vã nhau chi mệt nhọc (no coordination needed).
Bùa Chú Công Thức Toán XFetch (XFetch Mathematical Formula)
$$\text{TớiLúcThayĐồMớiChưa (ShouldRefresh)} = \left[\text{giờ_hiện_tại (currentTime)} - \left(\beta \times \delta \times \ln(\text{rand}())\right) > \text{đáo_hạn (expiryTime)}\right]$$
Ngả bài bóc phốt mớ quỷ:
- $\beta$: Hằng số múa (Scale constant - mặc định nằm chờ = 1.0) — chỉnh càng ngáo, mồi lửa (refresh) ném ra sớm chừng đó.
- $\delta$: Cú ngã ngựa đo thời gian dạo nọ (Last fetch duration - tính bằng mili-giây) — hiện hình nặn ra (reflects) bao công sức hộc máu tính toán lại (recomputation cost).
- $\ln(\text{rand}())$: Mọc ra cái sừng âm (Negative - bởi rand ∈ (0,1)) — đẻ ra cò mồi nổ lách tách bất thình lình (stochastic trigger).
Khi kho TTL đang rủng rỉnh căng đầy: currentTime ≪ expiryTime → chốt sai (condition false) → nằm ườn ra chẳng làm gì (no refresh).
Chừng bóp cạn mớ TTL: đường chân trời hy vọng để cú chốt (condition) thành hình nhích dần leo dốc hừng hực (increases continuously).
package cache
import (
"math"
"math/rand"
"time"
)
type XFetchEntry struct {
Value string
ExpiryTime time.Time
Delta time.Duration // Hao tổn máu đi đoạt lại món đồ này (Time taken to fetch)
Beta float64 // Nằm sàn 1.0 (Typically 1.0)
}
// ShouldRefresh thè lưỡi bói xem cái mớ hàng này có nên xắn tay vào đôn đốc làm mới hay không (proactively refreshed)
func (xf *XFetchEntry) ShouldRefresh() bool {
if xf.Value == "" {
return true // Tay không bắt giặc — vác xác cắn răng đi lấy thôi (must fetch)
}
randVal := rand.Float64()
if randVal <= 0 {
randVal = 1e-9 // Chặn cửa nẻ họng cái đám ảo ma ln(0) = -Infinity
}
deltaMs := float64(xf.Delta.Milliseconds())
adjustedNow := time.Now().Add(
time.Duration(-xf.Beta*deltaMs*math.Log(randVal)) * time.Millisecond,
)
return adjustedNow.After(xf.ExpiryTime)
}
// GetOrRefresh ới cái ngón nghề XFetch ra chốt thử nên hay không rước cục mới về (decide whether to refresh cache)
func GetOrRefresh(entry *XFetchEntry, fetchFn func() (string, time.Duration, error)) (string, error) {
if !entry.ShouldRefresh() {
return entry.Value, nil // Cache xài xá láng (Cache is still good)
}
start := time.Now()
value, ttl, err := fetchFn()
if err != nil {
if entry.Value != "" {
return entry.Value, nil // Cắn rứt mớm rác thiu đỡ vậy nếu sụp bẫy (Serve stale on error - graceful degradation)
}
return "", err
}
entry.Value = value
entry.ExpiryTime = time.Now().Add(ttl)
entry.Delta = time.Since(start) // Găm cái cọc nhớ vết thương đau lòng lần này đặng nhét mớ XFetch chặng sau
return value, nil
}
Bậc Thang Caching Chia Đuôi (Tiered Cache): Ổ nhà (Local) → Chợ Redis → Kho Database
Cỡ như động sập sàn Shopee Flash Sale, thậm chí con Redis bự chà bá cũng ỳ ạch cúp đuôi thành cái rốn ách tắc (bottleneck) ngậm 1 cục nghẹn họng khi dồn hàng triệu đạn pháo requests mỗi giây nã vào ráo trọi một cái ổ khóa đỏ lòm chói lọi (single hot key). Rút kiếm dẹp loạn (The solution) bắt buộc giơ súng dựng rào một vách ngăn cục bộ (tiered local cache) ngay chóc trong ổ (at each application node):
[!TIP] Đụng vô cái màn băm nát chia trát rải thảm khóa cache rụng khắp một mớ ổ vựa Redis shards, cái thuật toán dẫn đường (routing algorithm) là mạng sống tử huyệt. Đú đởn trò Băm cưa đôi cưa ba kiểu dư số Modulo (
key_hash % shard_count) thì xác định nhà nát tan hoang rụng rời dời ổ khổng lồ (massive key migrations) khi dốc thêm cục shard mới. Với mớ nồi niêu xoong chảo Redis dàn trận production, ngẩng cao đầu lôi Băm Nhất Quán (Consistent Hashing) ra xoa dịu xé nát chắp vá đắp đổi (minimize remapping) — mổ moi ruột gan rạch ròi ở Phần 9: Băm Nhất Quán & Các Nốt Ảo Tung Chảo (Consistent Hashing & Virtual Nodes).
package cache
import (
"sync"
"time"
)
type localEntry struct {
value string
expiresAt time.Time
}
// Lưới bọc lót tầng lớp TieredCache: Tầng 1 L1 (Nai lưng luồng trong sync.Map) → Tầng 2 L2 (Redis) → Tầng 3 L3 (Chui lỗ Database)
type TieredCache struct {
localCache sync.Map
localTTL time.Duration // Vòng đời tàn úa (Short TTL) — 1–5 giây cho cái hang local
}
func (t *TieredCache) Get(key string) (string, bool) {
if raw, ok := t.localCache.Load(key); ok {
entry := raw.(*localEntry)
if time.Now().Before(entry.expiresAt) {
return entry.value, true
}
t.localCache.Delete(key)
}
return "", false
}
func (t *TieredCache) Set(key, value string) {
t.localCache.Store(key, &localEntry{
value: value,
expiresAt: time.Now().Add(t.localTTL),
})
}
🔥 [Trích Ngôn Production Pattern]: Cái Bức Tường Lửa Trị Đàn Thú Điên Của Shopee (Shopee Thundering Herd Protection) Bệnh trạng: Nửa đêm canh ba rình săn hàng flash sale: 500,000 nhát chọc ngoáy chen lấn bâu xé điên cuồng dồn tụ nã thẳng mặt con
product:flash-item-999. Rủi thay cái hũ vàng Cache TTL nứt đáy bể toang sau cú đấm ngập mặt (first burst). Thủ Phạm (Root Cause): Redis há mồm giơ tay xin hàng chẳng thèm đỡ 500k quả nã trút mỗi giây nổ rát trên vỏn vẹn chóc 1 cái mặt khóa (single key). Chiêu Trị Bệnh: Ép nhồi vào cắm chốt lòi ra một cáisync.Mapchui rúc tại từng cái ngóc ngách pod (local in-process) trói chung với cái bảng án tử TTL = 1 cắc giây. Rốt cuộc chỉ chừa đường cho đúng 1 cháu goroutine/mỗi pod/mỗi giây được phép móp méo ngóc đầu qua hàng rào sang cầu khẩn Redis. Suy cho cùng Redis ngứa háng đón mớ rác của vạch trần ~100 thằng pods, chứ chẳng phải đối mặt rặn ẻ ôm đầu gánh 500k đám du khách ồ ạt. Trái Ngọt (Impact): Lũ rác DB queries xẹp lép văng thảm thiết từ mốc 500k/giây gãy cánh xuống lẹt đẹt ~100. Gánh nợ của Redis cắt phăng tan rụng lả tả 99.98%. (Nguồn rò rỉ: Shopee Engineering Blog, 2021)
Hỏi Nhanh Đáp Gọn (FAQ)
Cái giống quái đản Đàn Voi Giẫm Đạp Cache Stampede là cái thá gì và diệt tận gốc kiểu mẹ nào?
Cache Stampede bùng cháy hệt ngọn lửa lúc 1 cục hàng nổ tung hot hòn họt tới số mãn kiếp (expires) rồi dắt tay 1 đám cả bầy goroutines nhao nhao túm tụm thò cổ dòm mặt đồng loạt há hốc vồ hụt cái mâm rỗng tuyếch (miss simultaneously) — để rồi ôm bực tức túa về bủa vây hội đồng phang nát cửa cái hầm DB. 3 lằn ranh hỏa tuyến lá chắn chống giặc: (1) Chặn Dòng Chảy (Singleflight) — lột sạch vứt sọt ba cái mớ sao y bản chính chung đụng nhà (in-process deduplication), giá đổ máu O(1), đớp nhẹ gánh tóm gọn 95% tình huống; (2) Rình Sớm (XFetch) — xác suất quăng neo kéo lại làm mới chặng đầu, dẹp cất ba cái rào khóa đằng đẵng (no locks needed); (3) Chốt Chặn (Redis SETNX lock) — hất cẳng đâm chém chéo ngoe độc thủ chiếm giường (cross-process mutual exclusion).
Căng Náo Ghi-Xuyên-Thấu (Write-Through) vs Ghi-Lùi-Sau (Write-Behind) — Trọng Dụng Đứa Nào?
Write-Through (Ghi-Xuyên): Dập cắm một lúc thông quan ôm chầm cả đám cache + DB. Bất chấp không bao giờ lo lủng sổ mất rác (No data loss), rước sầu ôm cái hận đợi chờ DB (write latency = DB latency). Mang vào trùm xài: lưu sớ hóa đơn, xé lệnh chốt sổ, hay hầm rác nào mà văng 1 dấu phẩy viết bừa (losing a write) là xách vali dọn nhà ra đường (unacceptable).
Write-Behind (Ghi-Lùi): Vác quăng mớ hàng vô cache, tuồn thong thả từ tốn dội rửa về DB (asynchronously). Chớp mắt đớp xong nhẹ tựa lông hồng (latency is very low) vấp phải cái nguy to tướng đứt gánh bốc hơi lủng dạ (data loss) nều cái xưởng cache gục sấp mặt (crashes) đứt gân cúp mồm trước đợt tống xả tuồn về (flushing). Chén mồi ngon cho: cái trò chọc cạch mổ cò view đếm, hốt rác soi mói analytics ngáo nháo, 3 cái mâm nhào nặn sào nấu không quá nắn nót nguy cấp sinh tử (non-critical aggregations).
Bươi Móc Redis LRU vs LFU — Tra Tròng Cái Nào?
Rinh LFU (allkeys-lfu) quẳng lên mâm nếu cái đống nùi công chuyện (workload) nhà bạn lòi chóp hằn mặt ra cả cục khóa củi lửa (hot keys - mớ chóp hàng chốt sale flash sale, video rần rần hot hit) nháo nhác van xin một lá cờ xá tội miễn chém đá chóp xẻo thịt (protection from eviction). Quẹo lựa lụm LRU (allkeys-lru) thảy vô cho đám phế liệu ổ cache hàng lô nháo nhào chung chung cặn bã (general-purpose) rải lác đều tay trơn tru vớt vát (uniform access patterns). Xắn tay kẹp với quả Redis 4.0+, xài LFU cứ như sấm giật cho đám chợ xép thương mại điện tử sập sàn rầm rầm.
🔗 Bay Sang Bài Tới: Phần 4: Mở Rộng Cơ Sở Dữ Liệu & Rèn Giũa Tối Ưu Bể Kết Nối (Connection Pool) Trong Go — Phanh Phui Ngõ Phân Khúc Bãi Rác PostgreSQL (Range Partitioning), Chiêu Thức Nhào Lặn 2PC Percolator TiDB, và đòn gõ mỏ chắp vá mài dũa database/sql pool.