[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
# Production Patterns
|
||||
|
||||
## Stale-While-Revalidate
|
||||
|
||||
Return stale data immediately while refreshing in the background. Two time thresholds:
|
||||
|
||||
1. **TTL** — after this, entries become "stale" and trigger async background refresh via loaders
|
||||
2. **Stale duration** — after TTL + stale, entries are hard-expired and removed
|
||||
|
||||
```go
|
||||
refreshLoader := func(keys []string) (map[string]*Config, error) {
|
||||
return fetchConfigsFromDB(keys)
|
||||
}
|
||||
|
||||
cache := hot.NewHotCache[string, *Config](hot.WTinyLFU, 1_000).
|
||||
WithTTL(5 * time.Minute). // stale after 5min
|
||||
WithRevalidation(1 * time.Minute, refreshLoader). // hard-expire after 6min total
|
||||
WithRevalidationErrorPolicy(hot.KeepOnError). // keep stale value if refresh fails
|
||||
WithJitter(0.1, 30*time.Second). // spread expirations
|
||||
WithJanitor().
|
||||
Build()
|
||||
defer cache.StopJanitor()
|
||||
```
|
||||
|
||||
**Timeline for an entry set at T=0 with this config:**
|
||||
|
||||
- T=0 to T=5min: fresh — returned directly
|
||||
- T=5min to T=6min: stale — returned immediately, background refresh triggered
|
||||
- T>6min: expired — removed, next `Get()` blocks on loader
|
||||
|
||||
**Error policies:**
|
||||
|
||||
- `hot.KeepOnError` — if background refresh fails, keep the stale value until hard expiry
|
||||
- `hot.DropOnError` — if refresh fails, drop the entry immediately
|
||||
|
||||
Use `KeepOnError` when stale data is better than no data (config caches, product catalogs). Use `DropOnError` when correctness matters more than availability.
|
||||
|
||||
## Sharding
|
||||
|
||||
Split the cache into N independent segments to reduce lock contention under high concurrency:
|
||||
|
||||
```go
|
||||
cache := hot.NewHotCache[string, *User](hot.WTinyLFU, 100_000).
|
||||
WithTTL(5 * time.Minute).
|
||||
WithSharding(16, func(key string) uint64 {
|
||||
h := fnv.New64a()
|
||||
h.Write([]byte(key))
|
||||
return h.Sum64()
|
||||
}).
|
||||
WithJanitor().
|
||||
Build()
|
||||
defer cache.StopJanitor()
|
||||
```
|
||||
|
||||
**Sizing guidance:**
|
||||
|
||||
- Use powers of 2 (4, 8, 16, 32) for optimal hash distribution
|
||||
- Rule of thumb: shard count ~= number of CPU cores for high-contention workloads
|
||||
- Each shard gets `capacity / shards` items
|
||||
- Over-sharding (>64 shards) adds overhead without benefit
|
||||
|
||||
## Missing Key Caching (Negative Caching)
|
||||
|
||||
Prevents repeated loader calls for keys that don't exist in the source:
|
||||
|
||||
### Dedicated missing cache (recommended)
|
||||
|
||||
Independent eviction algorithm and capacity — gives fine-grained control:
|
||||
|
||||
```go
|
||||
cache := hot.NewHotCache[string, *User](hot.WTinyLFU, 100_000).
|
||||
WithTTL(1 * time.Hour).
|
||||
WithMissingCache(hot.LFU, 10_000). // separate LFU cache for missing keys
|
||||
WithLoaders(userLoader).
|
||||
WithJanitor().
|
||||
Build()
|
||||
defer cache.StopJanitor()
|
||||
```
|
||||
|
||||
### Shared missing cache
|
||||
|
||||
Missing entries stored in the main cache — simpler but uses main cache capacity:
|
||||
|
||||
```go
|
||||
cache := hot.NewHotCache[string, *User](hot.WTinyLFU, 100_000).
|
||||
WithTTL(1 * time.Hour).
|
||||
WithMissingSharedCache().
|
||||
WithLoaders(userLoader).
|
||||
WithJanitor().
|
||||
Build()
|
||||
defer cache.StopJanitor()
|
||||
```
|
||||
|
||||
### Manual missing key marking
|
||||
|
||||
```go
|
||||
// Mark individual key as missing
|
||||
cache.SetMissing("nonexistent-user")
|
||||
cache.SetMissingWithTTL("temp-missing", 5*time.Minute)
|
||||
|
||||
// Batch mark missing
|
||||
cache.SetMissingMany([]string{"user:404", "user:405"})
|
||||
```
|
||||
|
||||
**Important:** `Keys()`, `Values()`, `All()` exclude missing entries — they only return real values.
|
||||
|
||||
## Loader Chains
|
||||
|
||||
Multiple loaders execute sequentially for L1/L2 cache patterns:
|
||||
|
||||
```go
|
||||
redisLoader := func(keys []string) (map[string]*User, error) {
|
||||
return redis.MGet(ctx, keys...)
|
||||
}
|
||||
|
||||
dbLoader := func(keys []string) (map[string]*User, error) {
|
||||
return db.GetUsersByIDs(ctx, keys)
|
||||
}
|
||||
|
||||
cache := hot.NewHotCache[string, *User](hot.WTinyLFU, 10_000).
|
||||
WithTTL(5 * time.Minute).
|
||||
WithLoaders(redisLoader, dbLoader). // Redis first, then DB for remaining
|
||||
WithJanitor().
|
||||
Build()
|
||||
defer cache.StopJanitor()
|
||||
```
|
||||
|
||||
**Chain behavior:**
|
||||
|
||||
- `redisLoader` called first with all missing keys
|
||||
- `dbLoader` called only with keys NOT found by `redisLoader`
|
||||
- If both return the same key, `dbLoader`'s value wins (later overwrites earlier)
|
||||
- Any error stops the chain — partial results from earlier loaders are discarded
|
||||
|
||||
## Copy-on-Read / Copy-on-Write
|
||||
|
||||
Required when cached values are mutable (pointers, slices, maps):
|
||||
|
||||
```go
|
||||
cache := hot.NewHotCache[string, *User](hot.WTinyLFU, 10_000).
|
||||
WithTTL(5 * time.Minute).
|
||||
WithCopyOnRead(func(u *User) *User {
|
||||
copy := *u
|
||||
return ©
|
||||
}).
|
||||
WithCopyOnWrite(func(u *User) *User {
|
||||
copy := *u
|
||||
return ©
|
||||
}).
|
||||
WithJanitor().
|
||||
Build()
|
||||
defer cache.StopJanitor()
|
||||
```
|
||||
|
||||
- **CopyOnRead** — clones at retrieval: callers get independent copies, mutations don't affect cache
|
||||
- **CopyOnWrite** — clones at storage: cache holds a snapshot, external mutations to the original don't corrupt cached value
|
||||
- Use both when callers read and write concurrently. Use only one when the mutation direction is known.
|
||||
|
||||
## Prometheus Monitoring
|
||||
|
||||
### Setup
|
||||
|
||||
```go
|
||||
cache := hot.NewHotCache[string, *User](hot.WTinyLFU, 10_000).
|
||||
WithTTL(5 * time.Minute).
|
||||
WithPrometheusMetrics("user_cache").
|
||||
WithJanitor().
|
||||
Build()
|
||||
defer cache.StopJanitor()
|
||||
|
||||
prometheus.MustRegister(cache)
|
||||
```
|
||||
|
||||
### Key PromQL Queries
|
||||
|
||||
```promql
|
||||
# Hit ratio (target: >80%)
|
||||
rate(hot_cache_hit_count{cache="user_cache"}[5m]) /
|
||||
rate(hot_cache_get_count{cache="user_cache"}[5m])
|
||||
|
||||
# Eviction rate (high = cache too small or TTL too short)
|
||||
rate(hot_cache_eviction_count{cache="user_cache"}[5m])
|
||||
|
||||
# Cache size vs capacity
|
||||
hot_cache_len{cache="user_cache"} / hot_cache_capacity{cache="user_cache"}
|
||||
```
|
||||
|
||||
**Alerts to consider:**
|
||||
|
||||
- Hit rate drops below 70% for >5 minutes — cache may be undersized
|
||||
- Eviction rate spikes — working set exceeds capacity
|
||||
- Cache size near capacity — consider increasing capacity or reviewing TTLs
|
||||
|
||||
## Warm-Up on Startup
|
||||
|
||||
Pre-populate the cache before serving traffic:
|
||||
|
||||
```go
|
||||
cache := hot.NewHotCache[string, *User](hot.WTinyLFU, 10_000).
|
||||
WithTTL(1 * time.Hour).
|
||||
WithWarmUp(func() (map[string]*User, []string, error) {
|
||||
users, err := db.GetFrequentUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
missingKeys := []string{"deleted-user-1", "deleted-user-2"}
|
||||
return users, missingKeys, nil // values + known missing keys + error
|
||||
}).
|
||||
WithJanitor().
|
||||
Build()
|
||||
defer cache.StopJanitor()
|
||||
```
|
||||
|
||||
Use `WithWarmUpWithTimeout(30*time.Second, fn)` to bound startup time.
|
||||
|
||||
## Graceful Shutdown
|
||||
|
||||
Always stop the janitor goroutine before exit:
|
||||
|
||||
```go
|
||||
cache := hot.NewHotCache[string, *User](hot.WTinyLFU, 10_000).
|
||||
WithTTL(5 * time.Minute).
|
||||
WithJanitor().
|
||||
Build()
|
||||
defer cache.StopJanitor() // clean up background goroutine
|
||||
```
|
||||
|
||||
In applications with graceful shutdown orchestration, call `cache.StopJanitor()` during the shutdown phase alongside other resource cleanup.
|
||||
Reference in New Issue
Block a user