Skip to content

Feature/multilevel cache#5429

Draft
ljluestc wants to merge 3 commits intozeromicro:masterfrom
ljluestc:feature/multilevel-cache
Draft

Feature/multilevel cache#5429
ljluestc wants to merge 3 commits intozeromicro:masterfrom
ljluestc:feature/multilevel-cache

Conversation

@ljluestc
Copy link

Multi-Level Cache (sqlc + collection.Cache)

Closes #4797

Problem

Using Redis (sqlc) alone for SQL caching incurs a network round-trip on every read. For hot keys this overhead adds up. collection.Cache is an in-process LRU cache with zero network latency, but on its own it does not survive process restarts and cannot be shared across instances.

Solution

cache.NewMultiLevelCache layers the two together:

┌──────────┐  miss   ┌──────────┐  miss   ┌──────┐
│ L1: in-  │ ──────> │ L2: Redis│ ──────> │  DB  │
│ memory   │ <────── │ cache    │ <────── │      │
│ (fast)   │ promote │ (shared) │  query  │      │
└──────────┘         └──────────┘         └──────┘
  • Read: L1 is checked first (sub-microsecond). On an L1 miss, L2 (Redis) is queried. If found in L2, the value is promoted into L1 for subsequent requests.
  • Write / Delete: Both L1 and L2 are updated to keep them consistent.
  • Not-found caching: When the DB returns not-found, a placeholder is stored in both layers so repeated lookups for missing rows do not hit the DB again.

Changes

New files

  • core/stores/cache/multilevel.gocache.Cache implementation with two-tier L1 (in-memory) + L2 (Redis) caching
  • core/stores/cache/multilevel_test.go — 18 unit tests covering all cache operations (Get, Set, Del, Take, TakeWithExpire, not-found caching, struct values, L2→L1 promotion)

Modified files

  • core/stores/sqlc/cachedsql.go — Added two convenience constructors:
    • NewConnWithMultiLevelCache — Redis cluster + in-memory L1
    • NewNodeConnWithMultiLevelCache — single Redis node + in-memory L1
  • core/stores/sqlc/cachedsql_test.go — 4 integration tests covering constructor usage, L1 hit verification, exec cache invalidation, and not-found placeholder caching

Usage

Option A — Using sqlc convenience constructors

import (
    "time"

    "github.com/zeromicro/go-zero/core/stores/cache"
    "github.com/zeromicro/go-zero/core/stores/sqlc"
    "github.com/zeromicro/go-zero/core/stores/sqlx"
)

db := sqlx.NewMysql(datasource)

// With a single Redis node:
conn, err := sqlc.NewNodeConnWithMultiLevelCache(db, redisClient,
    []cache.MultiLevelCacheOption{
        cache.WithLocalExpire(time.Minute * 5), // L1 TTL (default: 5 min)
        cache.WithLocalLimit(10000),             // L1 max entries (default: 10000)
    },
    cache.WithExpiry(time.Hour*24*7), // L2 (Redis) TTL
)

// With a Redis cluster:
conn, err := sqlc.NewConnWithMultiLevelCache(db, cacheConf,
    []cache.MultiLevelCacheOption{
        cache.WithLocalExpire(time.Minute * 3),
        cache.WithLocalLimit(5000),
    },
    cache.WithExpiry(time.Hour*24*7),
)

Option B — Using the cache layer directly

import (
    "database/sql"
    "log"
    "time"

    "github.com/zeromicro/go-zero/core/stores/cache"
    "github.com/zeromicro/go-zero/core/stores/sqlc"
    "github.com/zeromicro/go-zero/core/stores/sqlx"
    "github.com/zeromicro/go-zero/core/syncx"
)

remote := cache.NewNode(redisClient, syncx.NewSingleFlight(),
    cache.NewStat("sqlc"), sql.ErrNoRows,
    cache.WithExpiry(time.Hour*24*7))

mlc, err := cache.NewMultiLevelCache(remote, sql.ErrNoRows,
    cache.WithLocalExpire(time.Minute*5),
    cache.WithLocalLimit(10000))
if err != nil {
    log.Fatal(err)
}

conn := sqlc.NewConnWithCache(db, mlc)

Using it for queries

Once constructed, use conn exactly like a regular sqlc.CachedConn:

var user User
err := conn.QueryRowCtx(ctx, &user, userCacheKey,
    func(ctx context.Context, conn sqlx.SqlConn, v any) error {
        return conn.QueryRowCtx(ctx, v,
            "SELECT id, name FROM user WHERE id = ?", userID)
    })

The first call hits Redis → DB, subsequent calls hit the in-memory L1 directly.

Configuration Options

Multi-level cache options (cache.MultiLevelCacheOption):

  • cache.WithLocalExpire(d time.Duration) — L1 in-memory TTL (default: 5 minutes)
  • cache.WithLocalLimit(n int) — L1 max entry count with LRU eviction (default: 10000)

Standard cache options (cache.Option) passed through to L2 (Redis):

  • cache.WithExpiry(d time.Duration) — L2 TTL (default: 7 days)
  • cache.WithNotFoundExpiry(d time.Duration) — TTL for not-found placeholders (default: 1 minute)

How to Test

# Multi-level cache unit tests (18 tests)
go test ./core/stores/cache/... -run "MultiLevel" -v -count=1

# sqlc integration tests (4 tests)
go test ./core/stores/sqlc/... -run "MultiLevel" -v -count=1

# Full test suites — verify no regressions
go test ./core/stores/cache/... ./core/stores/sqlc/... -count=1

# Static analysis
go vet ./core/stores/cache/... ./core/stores/sqlc/...

All 22 new tests pass. All existing tests unaffected:

ok  github.com/zeromicro/go-zero/core/stores/cache  0.248s
ok  github.com/zeromicro/go-zero/core/stores/sqlc   22.217s

Consistency Notes

  • L1 is per-process — different instances may briefly serve stale data until L1 expires.
  • Set WithLocalExpire to a short duration (e.g. 5–30 seconds) in multi-instance deployments.
  • Del/Exec invalidate both L1 and L2 within the same process immediately.

…s cache (L2)

Adds a multiLevelCache implementation that layers an in-memory
collection.Cache on top of the existing Redis-backed cache.Cache
to reduce Redis round-trips for hot keys.

New files:
- core/stores/cache/multilevel.go: Cache interface implementation
  with L1 in-memory + L2 remote two-tier caching
- core/stores/cache/multilevel_test.go: comprehensive tests

Updated files:
- core/stores/sqlc/cachedsql.go: convenience constructors
  NewConnWithMultiLevelCache and NewNodeConnWithMultiLevelCache

Closes zeromicro#4797
…i-level cache

- 4 new integration tests in sqlc covering constructor, query, exec invalidation, and not-found caching
- doc/multilevel-cache.md with architecture diagram, quick start examples, and configuration guide
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

How to use sqlcache and collection.Cache together?

1 participant