Redis Cache
Redis Cache
Section titled “Redis Cache”go-zero’s cache package wraps Redis with read-through, write-through, and cache-aside patterns, plus built-in stampede protection via singleflight. It is used internally by every goctl-generated model that has a cache layer.
Configuration
Section titled “Configuration”Redis: Host: 127.0.0.1:6379 Type: node # "node" for single-instance, "cluster" for Redis Cluster Pass: ""For Redis Cluster or multiple cache nodes with weighted traffic splitting:
CacheRedis: - Host: redis-1:6379 Type: node Weight: 50 - Host: redis-2:6379 Type: node Weight: 50import ( "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/sqlx")
// Single Redis nodecacheConf := cache.CacheConf{ {RedisConf: c.Redis, Weight: 100},}
// goctl-generated models wire this for you:conn := sqlx.NewMysql(c.DataSource)userModel := model.NewUserModel(conn, cacheConf)Read-Through with Take
Section titled “Read-Through with Take”Take is the core read-through operation. It:
- Checks Redis for the key
- On cache miss, runs the loader (inside a singleflight group) to prevent stampede
- Serialises the result to JSON and stores it in Redis with a random jitter on the TTL
- Returns the value
var user model.Usererr := userModel.FindOne(ctx, userId) // goctl model calls Take internallyFor manual use outside of generated models:
stats := cache.NewStat("user") // named stat group for metricsc := cache.New(cacheConf, nil, stats, model.ErrNotFound)
var user Usererr = c.TakeCtx(ctx, &user, fmt.Sprintf("user:%d", id), func(v any) error { row, err := db.FindOne(ctx, id) if err != nil { return err } *v.(*User) = *row return nil})Write-Through
Section titled “Write-Through”Write the updated object to Redis immediately after the database write:
// After updating the DB record:err = c.SetWithExpire(fmt.Sprintf("user:%d", user.Id), &user, time.Hour)goctl-generated Update methods call DelCacheCtx instead (cache-aside), which is simpler and avoids consistency edge cases:
// goctl generated pattern — delete cache, let next read rebuild itfunc (m *defaultUserModel) Update(ctx context.Context, data *User) error { _, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (sql.Result, error) { return conn.ExecCtx(ctx, updateSql, data.Username, data.Id) }, m.formatPrimary(data.Id)) return err}Batch Invalidation
Section titled “Batch Invalidation”// Delete multiple keys atomicallyerr = c.DelCtx(ctx, fmt.Sprintf("user:%d", id), fmt.Sprintf("user:name:%s", username), // also delete secondary index key)TTL and Jitter
Section titled “TTL and Jitter”go-zero adds a random jitter (±10% by default) to every cache entry’s TTL to avoid thundering herd at expiry — the scenario where all entries for the same type expire simultaneously and cause a DB spike.
| TTL setting | Actual TTL range |
|---|---|
| 1 hour | 54 min – 66 min |
| 10 min | 9 min – 11 min |
Stats and Metrics
Section titled “Stats and Metrics”The cache.Stat object tracks:
| Counter | Description |
|---|---|
Total | Total cache requests |
Hit | Cache hits |
Miss | Cache misses |
DbFails | DB loader errors |
Expose to Prometheus:
stats := cache.NewStat("user")// metrics are exported via go-zero's built-in /metrics endpoint// when prometheus is configured in app.yamlBest Practices
Section titled “Best Practices”- Always delete the cache key after a database write rather than updating it (cache-aside). This avoids race conditions between the cache write and the DB write.
- Use short TTL + negative caching together to balance freshness and protection against penetration.
- Monitor the hit rate (
Hit / Total). A hit rate below 90% usually indicates TTL is too short or the key space is too large. - For write-heavy data (updated > every few seconds), consider skipping the cache entirely for that field to avoid excessive invalidation overhead.